diff --git a/bin/hackathon b/bin/hackathon index 523b6ccf..9e7ffc5f 100755 --- a/bin/hackathon +++ b/bin/hackathon @@ -1,5 +1,10 @@ #!/usr/bin/env bash +# --- Path Initialization --- +# Get the directory of the script and switch to the project root (one level up) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/.." || exit 1 + # --- Configuration --- CONTAINER_PHP="php" DOCKER_COMPOSE="docker compose" diff --git a/config/autoload/project.develope.php b/config/autoload/project.develope.php index 0ce3c9df..a1b29ed5 100644 --- a/config/autoload/project.develope.php +++ b/config/autoload/project.develope.php @@ -2,7 +2,7 @@ return [ 'project' => [ - 'uri' => 'http://localhost', + 'uri' => 'http://localhost:5173', ], 'api' => [ 'access' => [ diff --git a/config/autoload/project.global.php b/config/autoload/project.global.php index 7ef504b6..2dbcc1f1 100644 --- a/config/autoload/project.global.php +++ b/config/autoload/project.global.php @@ -2,7 +2,8 @@ return [ 'project' => [ - 'uri' => 'dev.ownhackathon.de', + 'uri' => 'https:\\dev.ownhackathon.de', + 'senderEmail' => 'no-replay@ownhackathon.de', ], 'api' => [ 'access' => [ diff --git a/config/autoload/token.action.php b/config/autoload/token.action.php index 97d7be24..ec2b43e4 100644 --- a/config/autoload/token.action.php +++ b/config/autoload/token.action.php @@ -12,7 +12,7 @@ 'access' => [ 'key' => 'b:?Y@5JCWF:yi{o>irc(3$HFcR-#b\SA', 'algorithmus' => 'HS512', - 'duration' => 60 * 15, + 'duration' => 60 * 5, 'iss' => 'localhost', 'aud' => 'localhost', ], diff --git a/config/autoload/token.develope.php b/config/autoload/token.develope.php index 97d7be24..ec2b43e4 100644 --- a/config/autoload/token.develope.php +++ b/config/autoload/token.develope.php @@ -12,7 +12,7 @@ 'access' => [ 'key' => 'b:?Y@5JCWF:yi{o>irc(3$HFcR-#b\SA', 'algorithmus' => 'HS512', - 'duration' => 60 * 15, + 'duration' => 60 * 5, 'iss' => 'localhost', 'aud' => 'localhost', ], diff --git a/config/autoload/token.testing.php b/config/autoload/token.testing.php index 97d7be24..ec2b43e4 100644 --- a/config/autoload/token.testing.php +++ b/config/autoload/token.testing.php @@ -12,7 +12,7 @@ 'access' => [ 'key' => 'b:?Y@5JCWF:yi{o>irc(3$HFcR-#b\SA', 'algorithmus' => 'HS512', - 'duration' => 60 * 15, + 'duration' => 60 * 5, 'iss' => 'localhost', 'aud' => 'localhost', ], diff --git a/config/pipeline.php b/config/pipeline.php index df8f068c..254851d5 100644 --- a/config/pipeline.php +++ b/config/pipeline.php @@ -27,11 +27,11 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void { $app->pipe([ + CorsMiddleware::class, ApiErrorHandlerMiddleware::class, ServerUrlMiddleware::class, BodyParamsMiddleware::class, - CorsMiddleware::class, RouteMiddleware::class, ImplicitHeadMiddleware::class, diff --git a/public/api/docs/swagger.json b/public/api/docs/swagger.json index 328503d2..665eacbc 100644 --- a/public/api/docs/swagger.json +++ b/public/api/docs/swagger.json @@ -16,7 +16,7 @@ "Account" ], "summary": "Return of a new access token", - "operationId": "1ee7f36021506322a0e202b1fb32f278", + "operationId": "8153437a40e6c68ceb15c851fe31e2bc", "responses": { "200": { "description": "Success", @@ -47,7 +47,7 @@ "Account" ], "summary": "Activate the account", - "operationId": "db81d65a9e026816e8f2ea40f73a394b", + "operationId": "df8fdb2be361e5167853062f340f4425", "parameters": [ { "name": "token", @@ -93,7 +93,7 @@ "Account" ], "summary": "Creates a token for password reset. Sending via E-Mail", - "operationId": "faec81c539d68fbab3b442b162ad7c84", + "operationId": "1ab5a49dfe70ff9e4be96e3b4fd58802", "requestBody": { "description": "Set Password for a Account", "required": true, @@ -118,7 +118,7 @@ "Account" ], "summary": "Set Password to the account", - "operationId": "c5f1c414d69d6f318ab519d1746570e4", + "operationId": "3dfa8cc9f974f6db93df1a49b170fc75", "parameters": [ { "name": "token", @@ -163,9 +163,9 @@ "tags": [ "Account" ], - "summary": "Endpoint to register a new user account.", - "description": "Create Account", - "operationId": "8a212b9a005ff2dd5aeea1756d25bd05", + "summary": "Endpoint to register a new user account or request a password reset.", + "description": "Starts the account process for the given email address. \n\n**Security Note:** This endpoint always returns a 200 OK status to prevent user enumeration. The user will not know if an account already exists based on the API response.", + "operationId": "969568dfb3cf23c770ed1a16782476cd", "requestBody": { "description": "The email address for the new account", "required": true, @@ -179,17 +179,7 @@ }, "responses": { "200": { - "description": "Success" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpResponseMessage" - } - } - } + "description": "The request was processed successfully. Depending on the state of the account, one of the following actions will occur:\n1. **Account does not exist:** A token to activate the new account will be sent to the email address.\n2. **Account already exists:** A token to reset the password will be sent to the email address." } } } @@ -200,7 +190,7 @@ "Account" ], "summary": "Attempts to log in an account using transferred data", - "operationId": "4573a8211c6a27b677250eb46ca168ee", + "operationId": "9b6381b1663be08be716cc9ca3f04d2b", "requestBody": { "description": "Account data for authentication", "required": true, @@ -252,7 +242,7 @@ "Account" ], "summary": "Attempts to log out an account", - "operationId": "1154528e1da5f90aa3b356c63502b533", + "operationId": "fd1f895986002b7c0d208a40b3cf9dd5", "responses": { "200": { "description": "Success", @@ -283,7 +273,7 @@ "System Information" ], "summary": "Returns the current time in Unix format", - "operationId": "ffea4e020f3a4868f9f018e280509330", + "operationId": "8756d19e2e4758137f374e8015adad45", "responses": { "200": { "description": "Success", diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php index 407a94f0..29139b6e 100644 --- a/src/App/ConfigProvider.php +++ b/src/App/ConfigProvider.php @@ -2,9 +2,6 @@ namespace App; -use Envms\FluentPDO\Query; -use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; -use Laminas\ServiceManager\Factory\InvokableFactory; use App\Hydrator\AccountAccessAuthHydratorInterface; use App\Hydrator\AccountActivationHydratorInterface; use App\Hydrator\AccountHydratorInterface; @@ -12,9 +9,13 @@ use App\Service\Account\AccountService; use App\Service\Authentication\AuthenticationService; use App\Service\ClientIdentification\ClientIdentificationService; +use App\Service\Email\EmailService; +use App\Service\Email\EmailServiceFactory; use App\Service\Token\AccessTokenService; use App\Service\Token\ActivationTokenService; +use App\Service\Token\ActivationTokenServiceFactory; use App\Service\Token\PasswordTokenService; +use App\Service\Token\PasswordTokenServiceFactory; use App\Service\Token\RefreshTokenService; use App\Table\AccountAccessAuthTable; use App\Table\AccountActivationTable; @@ -33,8 +34,10 @@ use Core\Repository\TokenRepositoryInterface; use Core\Store; use Core\Utils\UuidFactoryInterface; +use Envms\FluentPDO\Query; +use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; +use Laminas\ServiceManager\Factory\InvokableFactory; use Psr\Log\LoggerInterface; -use Symfony\Component\Mailer\MailerInterface; class ConfigProvider { @@ -102,9 +105,10 @@ public function getDependencies(): array Service\Account\AccountService::class => ConfigAbstractFactory::class, Service\Authentication\AuthenticationService::class => InvokableFactory::class, Service\ClientIdentification\ClientIdentificationService::class => InvokableFactory::class, + EmailService::class => EmailServiceFactory::class, Service\Token\AccessTokenService::class => Service\Token\AccessTokenServiceFactory::class, - Service\Token\ActivationTokenService::class => ConfigAbstractFactory::class, - Service\Token\PasswordTokenService::class => ConfigAbstractFactory::class, + Service\Token\ActivationTokenService::class => ActivationTokenServiceFactory::class, + Service\Token\PasswordTokenService::class => PasswordTokenServiceFactory::class, Service\Token\RefreshTokenService::class => Service\Token\RefreshTokenServiceFactory::class, Table\AccountAccessAuthTable::class => ConfigAbstractFactory::class, Table\AccountActivationTable::class => ConfigAbstractFactory::class, @@ -227,12 +231,6 @@ public function getAbstractFactoryConfig(): array PasswordTokenService::class, UuidFactoryInterface::class, ], - Service\Token\ActivationTokenService::class => [ - MailerInterface::class, - ], - Service\Token\PasswordTokenService::class => [ - MailerInterface::class, - ], Table\AccountAccessAuthTable::class => [ Query::class, AccountAccessAuthHydratorInterface::class, diff --git a/src/App/Handler/Account/AccountRegisterHandler.php b/src/App/Handler/Account/AccountRegisterHandler.php index 7a1fd10d..cf8c737c 100644 --- a/src/App/Handler/Account/AccountRegisterHandler.php +++ b/src/App/Handler/Account/AccountRegisterHandler.php @@ -6,8 +6,6 @@ use Laminas\Diactoros\Response\JsonResponse; use OpenApi\Attributes as OA; use App\DTO\EMail\EMail; -use App\DTO\Response\HttpResponseMessage; -use Core\Enum\Message\StatusMessage; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -16,8 +14,10 @@ { #[OA\Post( path: '/account', - description: 'Create Account', - summary: 'Endpoint to register a new user account.', + description: "Starts the account process for the given email address. \n\n" . + "**Security Note:** This endpoint always returns a 200 OK status to prevent user enumeration. " . + "The user will not know if an account already exists based on the API response.", + summary: 'Endpoint to register a new user account or request a password reset.', tags: ['Account'], )] #[OA\RequestBody( @@ -27,12 +27,9 @@ )] #[OA\Response( response: HTTP::STATUS_OK, - description: StatusMessage::SUCCESS->value, - )] - #[OA\Response( - response: HTTP::STATUS_BAD_REQUEST, - description: StatusMessage::BAD_REQUEST->value, - content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + description: "The request was processed successfully. Depending on the state of the account, one of the following actions will occur:\n" . + "1. **Account does not exist:** A token to activate the new account will be sent to the email address.\n" . + '2. **Account already exists:** A token to reset the password will be sent to the email address.', )] public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php b/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php index 25155700..558ac1c3 100644 --- a/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php +++ b/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php @@ -52,7 +52,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } $accountAccessAuth = new AccountAccessAuth( - 1, + null, $account->id, 'default', $refreshToken->refreshToken, diff --git a/src/App/Service/Email/EmailService.php b/src/App/Service/Email/EmailService.php new file mode 100644 index 00000000..32881497 --- /dev/null +++ b/src/App/Service/Email/EmailService.php @@ -0,0 +1,28 @@ +from($this->senderEmail->toString()) + ->to($email->toString()) + ->subject($subject) + ->text($plainText) + ->html($html); + + $this->mailer->send($email); + } +} diff --git a/src/App/Service/Email/EmailServiceFactory.php b/src/App/Service/Email/EmailServiceFactory.php new file mode 100644 index 00000000..908fb2d7 --- /dev/null +++ b/src/App/Service/Email/EmailServiceFactory.php @@ -0,0 +1,18 @@ +get(MailerInterface::class); + $senderEmail = new Email($container->get('config')['project']['senderEmail']); + + return new EmailService($mailer, $senderEmail); + } +} diff --git a/src/App/Service/Token/ActivationTokenService.php b/src/App/Service/Token/ActivationTokenService.php index c7444652..10bd0ca7 100644 --- a/src/App/Service/Token/ActivationTokenService.php +++ b/src/App/Service/Token/ActivationTokenService.php @@ -2,29 +2,59 @@ namespace App\Service\Token; +use App\Service\Email\EmailService; use Core\Entity\Account\AccountActivationInterface; -use Symfony\Component\Mailer\MailerInterface; -use Symfony\Component\Mime\Email; use function sprintf; readonly class ActivationTokenService { public function __construct( - private MailerInterface $mailer, + private EmailService $emailService, + private string $projectUri, ) { } public function sendEmail(AccountActivationInterface $activation): void { - $text = sprintf('Your token to activate your Account: %s', $activation->token->getHex()->toString()); + $text = sprintf( + 'Hallo! + Vielen Dank für Ihr Interesse an ownHackathon. - $email = new Email() - ->from('no-reply@stormannsgal.de') - ->to($activation->email->toString()) - ->subject('Account Activation Code') - ->text($text); + Sie haben eine Registrierung mit dieser E-Mail-Adresse angefordert. + Um den Vorgang abzuschließen und Ihr Benutzerkonto zu erstellen, + bestätigen Sie bitte Ihre E-Mail-Adresse über den folgenden Link: - $this->mailer->send($email); + %s/app/account/activation/%s + + Hinweis: Erst wenn Sie diesen Link anklicken, wird Ihr Account verbindlich angelegt. Der Link ist zeitlich begrenzt gültig. + + Falls Sie diese Anfrage nicht selbst gestellt haben, können Sie diese Nachricht einfach ignorieren. + Es wird in diesem Fall kein Konto erstellt und Ihre E-Mail-Adresse wird nicht bei uns gespeichert. + + Mit freundlichen Grüßen, + Ihr Team von ownHackathon', + $this->projectUri, + $activation->token->getHex()->toString() + ); + + $html = sprintf( + '
Hallo!
+Vielen Dank für Ihr Interesse an ownHackathon.
+Sie haben eine Registrierung mit dieser E-Mail-Adresse angefordert. + Um den Vorgang abzuschließen und Ihr Benutzerkonto zu erstellen, + bestätigen Sie bitte Ihre E-Mail-Adresse über den folgenden Link:
+ +Hinweis: Erst wenn Sie diesen Link anklicken, wird Ihr Account verbindlich angelegt. + Der Link ist nur für begrenzte Zeit gültig.
+Falls Sie diese Anfrage nicht selbst gestellt haben, können Sie diese Nachricht einfach ignorieren. + Es wird in diesem Fall kein Konto erstellt und Ihre E-Mail-Adresse wird nicht dauerhaft bei uns gespeichert.
+Mit freundlichen Grüßen,
+ Ihr Team von ownHackathon
Hallo!
+Vielen Dank für Ihr Interesse an unserem Service.
+Sie haben versucht, sich mit dieser E-Mail-Adresse neu zu registrieren oder wollen Ihr Passwort zurücksetzen. + Da unter dieser Adresse bereits ein aktives Konto bei uns besteht, + senden wir Ihnen hiermit einen Link, mit dem Sie Ihr Passwort zurücksetzen können, + falls Sie den Zugriff auf Ihren Account wiederherstellen möchten.
+Klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:
+ Passwort resetten
Falls Sie keine Registrierung oder Passwortänderung veranlasst haben, + können Sie diese Nachricht einfach ignorieren. Ihr Account bleibt weiterhin mit + Ihrem bestehenden Passwort geschützt.
+Mit freundlichen Grüßen,
+ Ihr Team von ownHackathon