This commit is contained in:
Hesabix 2023-01-26 11:13:53 -05:00
parent 1311917325
commit 1f9209e215
20 changed files with 760 additions and 7 deletions

4
.env
View file

@ -27,3 +27,7 @@ APP_SECRET=17902f251c557579ee832f20ed66776b
DATABASE_URL="mysql://root:136431@127.0.0.1:3306/hsx?serverVersion=8&charset=utf8mb4" DATABASE_URL="mysql://root:136431@127.0.0.1:3306/hsx?serverVersion=8&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8" # DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='*'
###< nelmio/cors-bundle ###

View file

@ -2,6 +2,9 @@
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
<excludeFolder url="file://$MODULE_DIR$/var" /> <excludeFolder url="file://$MODULE_DIR$/var" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" /> <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
@ -31,6 +34,8 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
<excludeFolder url="file://$MODULE_DIR$/vendor/laminas/laminas-code" /> <excludeFolder url="file://$MODULE_DIR$/vendor/laminas/laminas-code" />
<excludeFolder url="file://$MODULE_DIR$/vendor/friendsofphp/proxy-manager-lts" /> <excludeFolder url="file://$MODULE_DIR$/vendor/friendsofphp/proxy-manager-lts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nelmio/cors-bundle" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View file

@ -61,9 +61,16 @@
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" /> <path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/laminas/laminas-code" /> <path value="$PROJECT_DIR$/vendor/laminas/laminas-code" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/proxy-manager-lts" /> <path value="$PROJECT_DIR$/vendor/friendsofphp/proxy-manager-lts" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
</include_path> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.1"> <component name="PhpProjectSharedConfiguration" php_language_level="8.1">
<option name="suggestChangeDefaultLanguageLevel" value="false" /> <option name="suggestChangeDefaultLanguageLevel" value="false" />
</component> </component>
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
</project> </project>

View file

@ -10,13 +10,15 @@
"doctrine/doctrine-bundle": "^2.8", "doctrine/doctrine-bundle": "^2.8",
"doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14", "doctrine/orm": "^2.14",
"nelmio/cors-bundle": "^2.2",
"symfony/console": "6.2.*", "symfony/console": "6.2.*",
"symfony/dotenv": "6.2.*", "symfony/dotenv": "6.2.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/framework-bundle": "6.2.*", "symfony/framework-bundle": "6.2.*",
"symfony/runtime": "6.2.*", "symfony/runtime": "6.2.*",
"symfony/security-bundle": "6.2.*", "symfony/security-bundle": "6.2.*",
"symfony/yaml": "6.2.*" "symfony/yaml": "6.2.*",
"symfonycasts/verify-email-bundle": "^1.13"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
@ -61,7 +63,7 @@
}, },
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": true,
"require": "6.2.*", "require": "6.2.*",
"docker": true "docker": true
} }

112
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "954f3c9bd813741915237122a725f96e", "content-hash": "b769efde9eff86f833f47b14e5c80c52",
"packages": [ "packages": [
{ {
"name": "doctrine/cache", "name": "doctrine/cache",
@ -1458,6 +1458,67 @@
], ],
"time": "2022-12-08T02:08:23+00:00" "time": "2022-12-08T02:08:23+00:00"
}, },
{
"name": "nelmio/cors-bundle",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioCorsBundle.git",
"reference": "0ee5ee30b0ee08ea122d431ae6e0ddeb87f035c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/0ee5ee30b0ee08ea122d431ae6e0ddeb87f035c0",
"reference": "0ee5ee30b0ee08ea122d431ae6e0ddeb87f035c0",
"shasum": ""
},
"require": {
"symfony/framework-bundle": "^4.3 || ^5.0 || ^6.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
"symfony/phpunit-bridge": "^4.3 || ^5.0 || ^6.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Nelmio\\CorsBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nelmio",
"homepage": "http://nelm.io"
},
{
"name": "Symfony Community",
"homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors"
}
],
"description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application",
"keywords": [
"api",
"cors",
"crossdomain"
],
"support": {
"issues": "https://github.com/nelmio/NelmioCorsBundle/issues",
"source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.2.0"
},
"time": "2021-12-01T09:34:27+00:00"
},
{ {
"name": "psr/cache", "name": "psr/cache",
"version": "3.0.0", "version": "3.0.0",
@ -4584,6 +4645,53 @@
} }
], ],
"time": "2022-12-14T16:11:27+00:00" "time": "2022-12-14T16:11:27+00:00"
},
{
"name": "symfonycasts/verify-email-bundle",
"version": "v1.13.0",
"source": {
"type": "git",
"url": "https://github.com/SymfonyCasts/verify-email-bundle.git",
"reference": "eb7bc997f36ad872a0d56bf209fe37fed148b0a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/eb7bc997f36ad872a0d56bf209fe37fed148b0a7",
"reference": "eb7bc997f36ad872a0d56bf209fe37fed148b0a7",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.2.5",
"symfony/config": "^5.4 | ^6.0",
"symfony/dependency-injection": "^5.4 | ^6.0",
"symfony/deprecation-contracts": "^2.2 | ^3.0",
"symfony/http-kernel": "^5.4 | ^6.0",
"symfony/routing": "^5.4 | ^6.0"
},
"require-dev": {
"doctrine/orm": "^2.7",
"doctrine/persistence": "^2.0",
"symfony/framework-bundle": "^5.4 | ^6.0",
"symfony/phpunit-bridge": "^5.4 | ^6.0",
"vimeo/psalm": "^4.3"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"SymfonyCasts\\Bundle\\VerifyEmail\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Simple, stylish Email Verification for Symfony",
"support": {
"issues": "https://github.com/SymfonyCasts/verify-email-bundle/issues",
"source": "https://github.com/SymfonyCasts/verify-email-bundle/tree/v1.13.0"
},
"time": "2023-01-04T12:46:15+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -4748,5 +4856,5 @@
"ext-iconv": "*" "ext-iconv": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.2.0" "plugin-api-version": "2.3.0"
} }

View file

@ -6,4 +6,6 @@ return [
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
]; ];

View file

@ -0,0 +1,10 @@
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/': null

View file

@ -16,7 +16,14 @@ security:
main: main:
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
json_login:
# api_login is a route we will create below
check_path: api_login
username_path: email
password_path: password
custom_authenticators:
- App\Security\ApiKeyAuthenticator
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall # https://symfony.com/doc/current/security.html#the-firewall
@ -27,7 +34,7 @@ security:
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER } - { path: ^/api/acc/*, roles: ROLE_USER }
when@test: when@test:
security: security:

View file

@ -1,7 +1,7 @@
<?php <?php
use App\Kernel; use App\Kernel;
header("Access-Control-Allow-Origin: *");
require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) { return function (array $context) {

View file

@ -11,7 +11,13 @@ class GeneralController extends AbstractController
#[Route('/', name: 'general_home')] #[Route('/', name: 'general_home')]
public function general_home(): JsonResponse public function general_home(): JsonResponse
{ {
phpinfo(); return $this->json([
'message' => 'Welcome to hesabix API.',
]);
}
#[Route('api/acc/dd', name: 'acc_dd')]
public function acc_dd(): JsonResponse
{
return $this->json([ return $this->json([
'message' => 'Welcome to hesabix API.', 'message' => 'Welcome to hesabix API.',
]); ]);

View file

@ -0,0 +1,135 @@
<?php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\UserToken;
use Exception;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use App\Entity\User;
use App\Security\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mime\Address;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use Symfony\Component\EventDispatcher\EventDispatcher,
Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken,
Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class UserController extends AbstractController
{
/**
* function to generate random strings
* @param int $length number of characters in the generated string
* @return string a new string is created with random characters of the desired length
*/
private function RandomString(int $length = 32): string
{
return substr(str_shuffle(str_repeat($x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length/strlen($x)) )),1,$length);
}
#[Route('/api/user/login', name: 'api_login')]
public function api_login(#[CurrentUser] ?User $user,EntityManagerInterface $entityManager): Response
{
if (null === $user) {
return $this->json([
'message' => 'missing credentials',
], Response::HTTP_UNAUTHORIZED);
}
$tokenString = $this->RandomString(254); // somehow create an API token for $user
$token = new UserToken();
$token->setUser($user);
$token->setToken($tokenString);
$entityManager->persist($token);
$entityManager->flush();
return $this->json([
'user' => $user->getUserIdentifier(),
'token' => $tokenString,
]);
}
#[Route('/api/user/check/login', name: 'api_user_check_login')]
public function api_user_check_login(#[CurrentUser] ?User $user,EntityManagerInterface $entityManager): Response
{
if (null === $user) {
return $this->json([
'message' => 'missing credentials',
], Response::HTTP_UNAUTHORIZED);
}
return $this->json(
['result'=>true]
);
}
#[Route('/api/user/current/info', name: 'api_user_current_info')]
public function api_user_current_info(#[CurrentUser] ?User $user,EntityManagerInterface $entityManager): Response
{
return $this->json([
'email'=>$user->getEmail(),
'fullname'=>$user->getFullName(),
'businessCount'=>count($user->getBusinesses())
]);
}
#[Route('/api/user/logout', name: 'api_user_logout')]
public function api_user_logout(Security $security,EntityManagerInterface $entityManager,Request $request): Response
{
// logout the user in on the current firewall
$security->logout(false);
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null == $apiToken) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
throw new CustomUserMessageAuthenticationException('No API token provided');
}
$tk = $entityManager->getRepository(UserToken::class)->findByApiToken($apiToken);
if (! $tk) {
throw new UserNotFoundException();
}
$entityManager->getRepository(UserToken::class)->remove($tk,true);
return $this->json(['result'=>true]);
}
#[Route('/api/user/update/info', name: 'api_user_update_info')]
public function api_user_update_info(#[CurrentUser] ?User $user,EntityManagerInterface $entityManager,Request $request): Response
{
$pameters = [];
if ($content = $request->getContent()) {
$pameters = json_decode($content, true);
}
$user->setFullName($pameters['fullname']);
$entityManager->persist($user);
$entityManager->flush();
return $this->json(['result'=>true]);
}
#[Route('/api/user/register', name: 'api_user_register')]
public function api_user_register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response
{
$user = new User();
$user->setEmail('alizadeh.babak@gmail.com');
$user->setRoles([]);
$user->setPassword(
$userPasswordHasher->hashPassword(
$user,
'123456'
)
);
$entityManager->persist($user);
$entityManager->flush();
return $this->json(['ok']);
}
}

36
src/Entity/Business.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace App\Entity;
use App\Repository\BusinessRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BusinessRepository::class)]
class Business
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'businesses')]
#[ORM\JoinColumn(nullable: false)]
private ?User $owner = null;
public function getId(): ?int
{
return $this->id;
}
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
}

View file

@ -3,6 +3,8 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@ -27,6 +29,24 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column] #[ORM\Column]
private ?string $password = null; private ?string $password = null;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserToken::class, orphanRemoval: true)]
private Collection $userTokens;
#[ORM\Column(length: 255)]
private ?string $fullName = null;
#[ORM\Column(length: 50)]
private ?string $dateRegister = null;
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Business::class, orphanRemoval: true)]
private Collection $businesses;
public function __construct()
{
$this->userTokens = new ArrayCollection();
$this->businesses = new ArrayCollection();
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -96,4 +116,88 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
// If you store any temporary, sensitive data on the user, clear it here // If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null; // $this->plainPassword = null;
} }
/**
* @return Collection<int, UserToken>
*/
public function getUserTokens(): Collection
{
return $this->userTokens;
}
public function addUserToken(UserToken $userToken): self
{
if (!$this->userTokens->contains($userToken)) {
$this->userTokens->add($userToken);
$userToken->setUser($this);
}
return $this;
}
public function removeUserToken(UserToken $userToken): self
{
if ($this->userTokens->removeElement($userToken)) {
// set the owning side to null (unless already changed)
if ($userToken->getUser() === $this) {
$userToken->setUser(null);
}
}
return $this;
}
public function getFullName(): ?string
{
return $this->fullName;
}
public function setFullName(string $fullName): self
{
$this->fullName = $fullName;
return $this;
}
public function getDateRegister(): ?string
{
return $this->dateRegister;
}
public function setDateRegister(string $dateRegister): self
{
$this->dateRegister = $dateRegister;
return $this;
}
/**
* @return Collection<int, Business>
*/
public function getBusinesses(): Collection
{
return $this->businesses;
}
public function addBusiness(Business $business): self
{
if (!$this->businesses->contains($business)) {
$this->businesses->add($business);
$business->setOwner($this);
}
return $this;
}
public function removeBusiness(Business $business): self
{
if ($this->businesses->removeElement($business)) {
// set the owning side to null (unless already changed)
if ($business->getOwner() === $this) {
$business->setOwner(null);
}
}
return $this;
}
} }

51
src/Entity/UserToken.php Normal file
View file

@ -0,0 +1,51 @@
<?php
namespace App\Entity;
use App\Repository\UserTokenRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserTokenRepository::class)]
class UserToken
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $token = null;
#[ORM\ManyToOne(inversedBy: 'userTokens')]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
public function getId(): ?int
{
return $this->id;
}
public function getToken(): ?string
{
return $this->token;
}
public function setToken(string $token): self
{
$this->token = $token;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\Business;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Business>
*
* @method Business|null find($id, $lockMode = null, $lockVersion = null)
* @method Business|null findOneBy(array $criteria, array $orderBy = null)
* @method Business[] findAll()
* @method Business[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class BusinessRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Business::class);
}
public function save(Business $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Business $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return Business[] Returns an array of Business objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('b.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Business
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\UserToken;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<UserToken>
*
* @method UserToken|null find($id, $lockMode = null, $lockVersion = null)
* @method UserToken|null findOneBy(array $criteria, array $orderBy = null)
* @method UserToken[] findAll()
* @method UserToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserTokenRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, UserToken::class);
}
public function save(UserToken $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(UserToken $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function findByApiToken($value): UserToken | null
{
return $this->createQueryBuilder('u')
->andWhere('u.token = :val')
->setParameter('val', $value)
->orderBy('u.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getOneOrNullResult()
;
}
// public function findOneBySomeField($value): ?UserToken
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response
{
return new JsonResponse([
'error' => [
'code' => $accessDeniedException->getCode(),
'message' => $accessDeniedException->getMessage(),
],
], $accessDeniedException->getCode());
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Security;
use App\Repository\UserTokenRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiKeyAuthenticator extends AbstractAuthenticator
{
/**
* @var UserTokenRepository
*/
private UserTokenRepository $userTokenRepository;
public function __construct(UserTokenRepository $userRepository)
{
$this->userTokenRepository = $userRepository;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request): ?bool
{
return $request->headers->has('X-AUTH-TOKEN');
}
public function authenticate(Request $request): Passport
{
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null == $apiToken) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
//throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(
new UserBadge($apiToken, function($apiToken) {
$tk = $this->userTokenRepository->findByApiToken($apiToken);
if (! $tk) {
throw new UserNotFoundException();
}
return $tk->getUser();
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$data = [
// you may want to customize or obfuscate the message first
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
public function start(Request $request, AuthenticationException $authException = null): JsonResponse
{
return new JsonResponse([
'error' => [
'code' => Response::HTTP_UNAUTHORIZED,
'message' => $authException?->getMessage() ?? Response::$statusTexts[Response::HTTP_UNAUTHORIZED],
],
], Response::HTTP_UNAUTHORIZED);
}
}

View file

@ -26,6 +26,18 @@
"migrations/.gitignore" "migrations/.gitignore"
] ]
}, },
"nelmio/cors-bundle": {
"version": "2.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.5",
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
},
"files": [
"config/packages/nelmio_cors.yaml"
]
},
"symfony/console": { "symfony/console": {
"version": "6.2", "version": "6.2",
"recipe": { "recipe": {
@ -102,5 +114,8 @@
"files": [ "files": [
"config/packages/security.yaml" "config/packages/security.yaml"
] ]
},
"symfonycasts/verify-email-bundle": {
"version": "v1.13.0"
} }
} }