-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TODO: - Enable/disable TOTP - (Re)set recovery token - Recover password - Unify error handling - Unify request validation - Testing
- Loading branch information
Showing
35 changed files
with
853 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
<?php | ||
|
||
namespace App\Controller\Api; | ||
|
||
use App\Dto\PasswordDto; | ||
use App\Dto\LocalpartDto; | ||
use App\Entity\Alias; | ||
use App\Handler\AliasHandler; | ||
use App\Handler\DeleteHandler; | ||
use App\Repository\AliasRepository; | ||
use App\Exception\ValidationException; | ||
use Doctrine\ORM\EntityManagerInterface; | ||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Bundle\SecurityBundle\Security; | ||
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\Routing\Attribute\Route; | ||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; | ||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; | ||
|
||
class AliasController extends AbstractController | ||
{ | ||
private readonly AliasRepository $aliasRepository; | ||
|
||
public function __construct( | ||
private readonly Security $security, | ||
private readonly AliasHandler $aliasHandler, | ||
private readonly DeleteHandler $deleteHandler, | ||
private readonly EntityManagerInterface $manager, | ||
) { | ||
$this->aliasRepository = $manager->getRepository(Alias::class); | ||
} | ||
|
||
#[Route('/api/user/aliases', name: 'get_user_aliases', methods: ['GET'])] | ||
public function getAliases(): JsonResponse | ||
{ | ||
$user = $this->security->getUser(); | ||
if ($customAliases = $this->aliasRepository->findByUser($user, false, false)) { | ||
$customAliasData = array_map(function (Alias $alias) { | ||
return $alias->getSource(); | ||
}, $customAliases); | ||
} | ||
if ($randomAliases = $this->aliasRepository->findByUser($user, true, false)) { | ||
$randomAliasData = array_map(function (Alias $alias) { | ||
return $alias->getSource(); | ||
}, $randomAliases); | ||
} | ||
return $this->json( | ||
[ | ||
'status' => 'success', | ||
'customAliases' => $customAliasData, | ||
'randomAliases' => $randomAliasData, | ||
], | ||
200 | ||
); | ||
} | ||
|
||
/** | ||
* Creates random alias if request is empty ('{}'), | ||
* Creates custom alias if request contains "localpart" key | ||
*/ | ||
#[Route('/api/user/aliases', name: 'post_user_alias', methods: ['POST'])] | ||
public function createRandomAlias(#[MapRequestPayload] LocalpartDto $dto): JsonResponse | ||
{ | ||
$user = $this->security->getUser(); | ||
try { | ||
$alias = $this->aliasHandler->create($user, $dto->localpart); | ||
} catch (ValidationException $e) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'email address is unavailable' | ||
], 400); | ||
} | ||
if (!$alias) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'limit reached' | ||
], 400); | ||
} | ||
return $this->json([ | ||
'status' => 'success', | ||
'alias' => $alias->getSource() | ||
], 200); | ||
} | ||
|
||
/** | ||
* Delegates password validation to PasswordDto | ||
* Deletes random user-owned alias with source ${uid} if it exists | ||
*/ | ||
#[Route('/api/user/aliases/{uid}', name: 'delete_user_alias', methods: ['DELETE'])] | ||
public function getAlias(#[MapRequestPayload] PasswordDto $dto, string $uid): JsonResponse | ||
{ | ||
$user = $this->security->getUser(); | ||
/** | ||
* Return random aliases owned by authenticated user | ||
* @var Alias $alias | ||
*/ | ||
$alias = $this->aliasRepository->findOneByUserAndSource($user, $uid); | ||
if ($alias === null) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'forbidden' | ||
], 400); | ||
} | ||
if (!$alias->isRandom()) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'contact administrator for custom alias deletion' | ||
], 400); | ||
} | ||
$this->deleteHandler->deleteAlias($alias); | ||
$dto->erasePassword(); | ||
return $this->json(['status' => 'success'], 200); | ||
} | ||
} |
11 changes: 5 additions & 6 deletions
11
src/Controller/ApiLoginController.php → src/Controller/Api/LoginController.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,29 @@ | ||
<?php | ||
|
||
namespace App\Controller; | ||
namespace App\Controller\Api; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Routing\Annotation\Route; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface; | ||
|
||
class ApiLoginController extends AbstractController | ||
class LoginController extends AbstractController | ||
{ | ||
#[Route('/api/login', name: 'api_login', methods: ['POST'])] | ||
#[Route('/api/user/login', name: 'api_login', methods: ['POST'])] | ||
public function apilogin() | ||
{ | ||
} | ||
|
||
#[Route('/api/login/2fa', name: 'api_login_2fa', methods: ['POST'])] | ||
#[Route('/api/user/login/2fa', name: 'api_login_2fa', methods: ['POST'])] | ||
public function apilogin2fa(TokenInterface $token): Response | ||
{ | ||
// TODO: get this working | ||
// TODO: should be handled by firewall? | ||
if (!$token instanceof TwoFactorTokenInterface) { | ||
$error = $this->createAccessDeniedException("User not in 2fa process"); | ||
$jsonResponse = new Response(json_encode($error), Response::HTTP_BAD_REQUEST); | ||
$jsonResponse->headers->set('Content-Type', 'application/json'); | ||
return $jsonResponse; | ||
} | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
namespace App\Controller\Api; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
|
||
// TODO | ||
class RecoveryController extends AbstractController | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<?php | ||
|
||
namespace App\Controller\Api; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\Routing\Attribute\Route; | ||
use App\Dto\RegisterRequestDto; | ||
use App\Entity\User; | ||
use App\Handler\RegistrationHandler; | ||
use App\Form\Model\Registration; | ||
use App\Repository\UserRepository; | ||
use Doctrine\ORM\EntityManagerInterface; | ||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||
|
||
class RegistrationController extends AbstractController | ||
{ | ||
public function __construct( | ||
private readonly RegistrationHandler $registrationHandler, | ||
private readonly EntityManagerInterface $manager, | ||
private readonly JWTTokenManagerInterface $jwtManager, | ||
) { | ||
} | ||
|
||
#[Route('/api/user/register', name: 'post_user_register', methods: ['POST'])] | ||
public function register(#[MapRequestPayload] RegisterRequestDto $dto): JsonResponse | ||
{ | ||
if (!$this->registrationHandler->isRegistrationOpen()) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'registration closed' | ||
], 423); | ||
} | ||
// TODO Could be moved to Dto validation, but requires to initialize whole DTO class for validation with little benefit | ||
if ($dto->newPassword !== $dto->newPasswordConfirm) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'passwords do not match' | ||
], 400); | ||
} | ||
$registration = new Registration(); | ||
$registration->setVoucher($dto->voucher); | ||
$registration->setPlainPassword($dto->newPassword); | ||
$registration->setEmail($dto->email); | ||
$this->registrationHandler->handle($registration); | ||
$dto->eraseNewPassword(); | ||
|
||
if (null === $user = $this->manager->getRepository(User::class)->findByEmail($registration->getEmail())) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'unknown error when creating user' | ||
], 500); | ||
} | ||
$recoveryToken = $user->getPlainRecoveryToken(); | ||
$jwtToken = $this->jwtManager->create($user); | ||
$user->eraseCredentials(); | ||
return $this->json([ | ||
'message' => 'success', | ||
'recoveryToken' => $recoveryToken, | ||
'token' => $jwtToken, | ||
], 200); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
namespace App\Controller\Api; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
|
||
// TODO | ||
class TotpController extends AbstractController | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<?php | ||
|
||
namespace App\Controller\Api; | ||
|
||
use App\Dto\PasswordDto; | ||
use App\Dto\PasswordChangeDto; | ||
use App\Entity\User; | ||
use App\Handler\DeleteHandler; | ||
use App\Helper\PasswordUpdater; | ||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Bundle\SecurityBundle\Security; | ||
use Symfony\Component\HttpFoundation\JsonResponse; | ||
use Symfony\Component\Routing\Attribute\Route; | ||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; | ||
use App\Handler\MailCryptKeyHandler; | ||
use Doctrine\ORM\EntityManagerInterface; | ||
|
||
class UserController extends AbstractController | ||
{ | ||
public function __construct( | ||
private readonly Security $security, | ||
private readonly DeleteHandler $deleteHandler, | ||
private readonly PasswordUpdater $passwordUpdater, | ||
private readonly MailCryptKeyHandler $mailCryptKeyHandler, | ||
private readonly EntityManagerInterface $manager, | ||
) { | ||
} | ||
|
||
#[Route('/api/user', name: 'get_user', methods: ['GET'])] | ||
public function getSelf(): JsonResponse | ||
{ | ||
/** @var User $user */ | ||
$user = $this->security->getUser(); | ||
return $this->json([ | ||
'status' => 'success', | ||
'username' => $user->getEmail(), | ||
'totp_enabled' => $user->isTotpAuthenticationEnabled(), | ||
], 200); | ||
} | ||
|
||
#[Route('/api/user', name: 'patch_user', methods: ['PATCH'])] | ||
public function patchSelf(#[MapRequestPayload] PasswordChangeDto $dto): JsonResponse | ||
{ | ||
/** @var User $user */ | ||
$user = $this->security->getUser(); | ||
// TODO: move this to proper validator | ||
if ($dto->newPassword !== $dto->newPasswordConfirm) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'passwords do not match' | ||
], 400); | ||
} | ||
if ($dto->newPassword === $dto->password) { | ||
return $this->json([ | ||
'status' => 'failed', | ||
'message' => 'new password and old password are same' | ||
], 400); | ||
} | ||
$user->setPlainPassword($dto->newPassword); | ||
$this->passwordUpdater->updatePassword($user); | ||
// Reencrypt the MailCrypt key with new password | ||
if ($user->hasMailCryptSecretBox()) { | ||
$this->mailCryptKeyHandler->update($user, $dto->password); | ||
} | ||
$this->manager->flush(); | ||
|
||
$user->eraseCredentials(); | ||
$dto->eraseNewPassword(); | ||
$dto->erasePassword(); | ||
return $this->json(['status' => 'success'], 200); | ||
} | ||
|
||
/** | ||
* Delegates password validation to PasswordDto | ||
*/ | ||
#[Route('/api/user', name: 'delete_user', methods: ['DELETE'])] | ||
public function deleteSelf(#[MapRequestPayload] PasswordDto $dto): JsonResponse | ||
{ | ||
/** @var User $user */ | ||
$user = $this->security->getUser(); | ||
$this->security->logout(); | ||
$this->deleteHandler->deleteUser($user); | ||
$dto->erasePassword(); | ||
return $this->json(['status' => 'success'], 200); | ||
} | ||
} |
Oops, something went wrong.