Creation page d'accueil

master
Julien Rosset 2 months ago
parent c3c5d28aff
commit 795c89b9e6

@ -32,6 +32,7 @@
"symfony/process": "7.2.*", "symfony/process": "7.2.*",
"symfony/property-access": "7.2.*", "symfony/property-access": "7.2.*",
"symfony/property-info": "7.2.*", "symfony/property-info": "7.2.*",
"symfony/rate-limiter": "7.2.*",
"symfony/runtime": "7.2.*", "symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*", "symfony/security-bundle": "7.2.*",
"symfony/serializer": "7.2.*", "symfony/serializer": "7.2.*",

72
composer.lock generated

@ -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": "ced9115d2427916ca27414514c173fb9", "content-hash": "01c7bac1edd7f4d3c539771f00e6ae3a",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
@ -5567,6 +5567,76 @@
], ],
"time": "2025-03-06T16:27:19+00:00" "time": "2025-03-06T16:27:19+00:00"
}, },
{
"name": "symfony/rate-limiter",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/rate-limiter.git",
"reference": "bb6b14ee6c1c4d2722a30d46fb92714943526804"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/rate-limiter/zipball/bb6b14ee6c1c4d2722a30d46fb92714943526804",
"reference": "bb6b14ee6c1c4d2722a30d46fb92714943526804",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/options-resolver": "^6.4|^7.0"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
"symfony/lock": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\RateLimiter\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Wouter de Jong",
"email": "wouter@wouterj.nl"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides a Token Bucket implementation to rate limit input and output in your application",
"homepage": "https://symfony.com",
"keywords": [
"limiter",
"rate-limiter"
],
"support": {
"source": "https://github.com/symfony/rate-limiter/tree/v7.2.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-11-09T09:29:03+00:00"
},
{ {
"name": "symfony/routing", "name": "symfony/routing",
"version": "v7.2.3", "version": "v7.2.3",

@ -1,12 +1,25 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html # see https://symfony.com/doc/current/reference/configuration/framework.html
framework: framework:
secret: '%env(APP_SECRET)%' secret: '%env(APP_SECRET)%'
#csrf_protection: true
http_method_override: false
handle_all_throwables: true
# Note that the session will be started ONLY if you read or write from it. # Enables session support. Note that the session will ONLY be started if you read or write from it.
session: true # Remove or comment this section to explicitly disable session support.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
#esi: true #esi: true
#fragments: true #fragments: true
php_errors:
log: true
annotations:
enabled: false
when@test: when@test:
framework: framework:

@ -4,14 +4,34 @@ security:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers: providers:
users_in_memory: { memory: null } # Used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
main: main:
lazy: true lazy: true
provider: users_in_memory provider: app_user_provider
login_throttling:
max_attempts: 3 # per minute
form_login:
login_path: user_signIn
check_path: user_signIn
enable_csrf: true
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week, in seconds
secure: true
samesite: strict
signature_properties: [ 'password', 'validationAdministrator', 'validationDate' ]
logout:
path: user_signOut
# where to redirect after logout
# target: app_any_route
# 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

@ -1,7 +1,7 @@
framework: framework:
default_locale: en default_locale: fr
translator: translator:
default_path: '%kernel.project_dir%/translations' default_path: '%kernel.project_dir%/translations'
fallbacks: fallbacks:
- en - fr
providers: providers:

@ -1,5 +1,6 @@
twig: twig:
file_name_pattern: '*.twig' file_name_pattern: '*.twig'
form_themes: [ 'bootstrap_5_horizontal_layout.html.twig' ]
when@test: when@test:
twig: twig:

@ -1,5 +1,6 @@
framework: framework:
validation: validation:
email_validation_mode: html5
# Enables validator auto-mapping support. # Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata. # For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping: #auto_mapping:

@ -20,8 +20,17 @@ return [
'path' => './assets/datatables.js', 'path' => './assets/datatables.js',
'entrypoint' => true, 'entrypoint' => true,
], ],
'jqueryLocal' => [
'path' => './assets/jqueryLocal.js',
],
'utils' => [
'path' => './assets/utils.js',
],
'fontawesome' => [
'path' => './assets/fontawesome.js',
],
'bootstrap' => [ 'bootstrap' => [
'version' => '5.3.3', 'version' => '5.3.6',
], ],
'@popperjs/core' => [ '@popperjs/core' => [
'version' => '2.11.8', 'version' => '2.11.8',
@ -29,49 +38,32 @@ return [
'jquery' => [ 'jquery' => [
'version' => '3.7.1', 'version' => '3.7.1',
], ],
'jqueryLocal' => [
'path' => './assets/jqueryLocal.js',
],
'utils' => [
'path' => './assets/utils.js',
],
'datatables.net' => [ 'datatables.net' => [
'version' => '2.0.7', 'version' => '2.3.1',
], ],
'datatables.net-dt/css/dataTables.dataTables.min.css' => [ 'datatables.net-dt/css/dataTables.dataTables.min.css' => [
'version' => '2.0.7', 'version' => '2.3.1',
'type' => 'css', 'type' => 'css',
], ],
'datatables.net-bs5' => [ 'datatables.net-bs5' => [
'version' => '2.0.7', 'version' => '2.3.1',
], ],
'datatables.net-bs5/css/dataTables.bootstrap5.min.css' => [ 'datatables.net-bs5/css/dataTables.bootstrap5.min.css' => [
'version' => '2.0.7', 'version' => '2.3.1',
'type' => 'css', 'type' => 'css',
], ],
'fontawesome' => [
'path' => './assets/fontawesome.js',
],
'@fortawesome/fontawesome-svg-core' => [ '@fortawesome/fontawesome-svg-core' => [
'version' => '6.5.2', 'version' => '6.7.2',
], ],
'@fortawesome/free-solid-svg-icons' => [ '@fortawesome/free-solid-svg-icons' => [
'version' => '6.5.2', 'version' => '6.7.2',
], ],
'@fortawesome/fontawesome-svg-core/styles.min.css' => [ '@fortawesome/fontawesome-svg-core/styles.min.css' => [
'version' => '6.5.2', 'version' => '6.7.2',
'type' => 'css', 'type' => 'css',
], ],
'dynamicModal' => [ 'bootstrap/dist/css/bootstrap.min.css' => [
'path' => './assets/dynamicModal.js', 'version' => '5.3.6',
], 'type' => 'css',
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
], ],
]; ];

@ -0,0 +1,23 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* Controllers for "core" pages: home, etc.
*/
class CoreController extends AbstractController {
/**
* Home page
*
* @return Response The response
*/
#[Route('/', name: 'core_main')]
public function main (): Response {
return $this->render('Core/Main.html.twig');
}
}

@ -0,0 +1,45 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
/**
* Implementation for the base implementation of an entity: id, creation and last update date and time
*/
trait TEntityBase {
/**
* @var int|null The internal id
*
* @noinspection PhpPropertyNamingConventionInspection
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
/**
* Les données du trait qui doivent être inclus dans le JSON
*
* @return array Les données du trait qui doivent être inclus dans le JSON
*
* @see JsonSerializable::jsonSerialize()
*
* @noinspection PhpMethodNamingConventionInspection
*/
protected final function TEntityBase__jsonSerialize (): array {
return [
'id' => $this->getId(),
];
}
/**
* The internal id
*
* @return int|null The internal id
*/
public function getId (): ?int {
return $this->id;
}
}

@ -0,0 +1,173 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Stringable;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A registered user
*/
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[UniqueEntity(fields: ['email'], message: 'Il existe déjà un compte avec cette adresse mail')]
class User implements UserInterface, PasswordAuthenticatedUserInterface, Stringable {
use TEntityBase;
/**
* @var string The email
*/
#[ORM\Column(length: 100, unique: true)]
#[Assert\NotBlank(message: 'Veuillez saisir un email')]
#[Assert\Email(message: 'Veuillez saisir un email valide')]
private string $email;
/**
* @var string The hashed password
*/
#[ORM\Column(length: 255)]
private string $password;
/**
* @var string|null The name
*/
#[ORM\Column(length: 100, nullable: true)]
private ?string $name = null;
/**
* @var string[] The roles
*/
#[ORM\Column]
private array $roles = [];
/**
* @inheritDoc
*/
public function __toString (): string {
return $this->getName() ?? $this->getEmail();
}
/**
* The email
*
* @return string|null The email
*/
public function getEmail (): ?string {
return $this->email;
}
/**
* Change the email
*
* @param string $email The new email
*
* @return $this
*/
public function setEmail (string $email): self {
$this->email = $email;
return $this;
}
/**
* The hashed password
*
* @return string The hashed password
*
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword (): string {
return $this->password;
}
/**
* Change the hashed password
*
* @param string $password The new hashed password
*
* @return $this
*/
public function setPassword (string $password): self {
$this->password = $password;
return $this;
}
/**
* The name
*
* @return string|null The name
*/
public function getName (): ?string {
return $this->name;
}
/**
* Change the name
*
* @param string|null $name The new name
*
* @return $this
*/
public function setName (?string $name): self {
$this->name = $name;
return $this;
}
/**
* A visual identifier that represents this user
*
* @see UserInterface
*/
public function getUserIdentifier (): string {
return $this->email;
}
/**
* Has a role ?
*
* @param string $role The role
*
* @return bool True if the user has the role, else False
*/
public function hasRole (string $role): bool {
return in_array($role, $this->getRoles());
}
/**
* The roles
*
* @return string[] The roles
*
* @see UserInterface
*/
public function getRoles (): array {
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* Set the roles
*
* @param array $roles The new roles
*
* @return void
*/
public function setRoles (array $roles): void {
$this->roles = $roles;
}
/**
* Removes sensitive data from the user
*
* @see UserInterface
*/
public function eraseCredentials (): void {
}
}

@ -0,0 +1,53 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface {
public function __construct (ManagerRegistry $registry) {
parent::__construct($registry, User::class);
}
public function save (User $entity, bool $flush = false): void {
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove (User $entity, bool $flush = false): void {
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword (PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void {
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
}
$user->setPassword($newHashedPassword);
$this->save($user, true);
}
}

@ -0,0 +1,115 @@
<?php
namespace App\Service;
use App\Entity\User;
use App\Misc\FlashType;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* Service for the connected user
*/
readonly class ConnectedUserService {
/**
* @var Security The security service
*/
public Security $security;
/**
* @var RouterInterface The routing service
*/
private RouterInterface $router;
/**
* @var RequestStack The service for the request
*/
private RequestStack $request;
/**
* Initialization
*
* @param Security $security The security service
* @param RouterInterface $router The routing service
* @param RequestStack $request The service for the request
*/
public function __construct (Security $security, RouterInterface $router, RequestStack $request) {
$this->security = $security;
$this->router = $router;
$this->request = $request;
}
/**
* The connected user
*
* @return User|null The connected user
*/
public function getUser (): ?User {
$user = $this->security->getUser();
if ($user === null) {
return null;
}
if (!$user instanceof User) {
return null;
}
return $user;
}
/**
* Is the requested user the connected one ?
*
* @param User $requestedUser The requested user
*
* @return bool True if the requested user is the connected one, else false
*/
public function isRequestedUser (User $requestedUser): bool {
return $this->getUser()?->getId() !== $requestedUser->getId();
}
/**
* Check the user is NOT connected
*
* @return Response|null The response (warning and redirect) if user is connected, else Null
*/
public function checkNotConnected (): ?Response {
/** @var User|null $user */
$user = $this->getUser();
if ($user === null) {
return null;
}
/** @var Session $session */
$session = $this->request->getSession();
$session->getFlashBag()->add(
FlashType::WARNING,
"Vous êtes déjà connecté, merci de vous <a href=\"{$this->router->generate('user_signOut')}\">déconnecter</a> d'abord"
);
return new RedirectResponse($this->router->generate('core_main'), 302);
}
/**
* Check if the requested user is the connected one or if the last has administration privileges
*
* @param User $requestedUser The requested user
*
* @return void
*
* @throws AccessDeniedException If the access is denied
*/
public function checkRequestedUserAccess (User $requestedUser): void {
if (!$this->isRequestedUser($requestedUser)) {
return;
}
if (!$this->security->isGranted('ROLE_ADMIN')) {
return;
}
$exception = new AccessDeniedException();
$exception->setAttributes(['ROLE_ADMIN']);
$exception->setSubject(null);
throw $exception;
}
}

@ -0,0 +1,13 @@
{% extends '/base.html.twig' %}
{% block title %}Accueil - {{ parent() }}{% endblock %}
{% block mainContent %}
<h1>Recipe Manager</h1>
{% if app.user %}
Bienvenu {{ app.user }}
{% else %}
<p>Bienvenu sur Recipe Manager, le gestionnaire de recette de jeux vidéos.</p>
{# <p>Merci de vous <a href="{{ path('user_signIn') }}">connecter</a> ou <a href="{{ path('user_signOut') }}">créer un compte</a> pour commencer.</p>#}
{% endif %}
{% endblock %}

@ -0,0 +1,15 @@
{% block flashTag %}
{% set flashes = app.flashes %}
{% if flashes|length > 0 %}
<section id="flashes" class="d-flex flex-column mt-3">
{% for flashType, flashMessages in flashes %}
{% for flashMessage in flashMessages %}
<div class="alert alert-{{ flashType }} alert-dismissible fade show" role="{{ flashType }}">
{{ flashMessage|raw }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
</section>
{% endif %}
{% endblock %}

@ -1,17 +1,35 @@
<!DOCTYPE html> {% extends "/symfony.html.twig" %}
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}
{% endblock %}
{% block javascripts %} {% block title %}Recipe Manager{% endblock %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %} {% block headerTag %}
</head> <header class="pb-2 d-flex flex-column justify-content-start fixed-top">
<body data-turbo="false"> {% endblock %}
{% block body %}{% endblock %} {% block headerContent %}
</body> <div class="d-flex justify-content-between w-100 px-2">
</html> <!--region Website name-->
<a class="navbar-brand" href="{{ path('core_main') }}">Recipe Manager</a>
<!--endregion-->
<!--region menu-->
<nav class="navbar navbar-expand-lg py-0">
<ul class="navbar-nav">
{% if app.user %}
<!--region User menu-->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle py-0" id="dropdown-locale" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ app.user }}
</a>
<ul class="dropdown-menu" aria-labelledby="dropdown-locale">
<li><a href="{{ path('user_signOut') }}" class="dropdown-item">{{ 'user.signOut'|trans }}</a></li>
</ul>
</li>
<!--endregion-->
{% else %}
{# <li class="nav-item"><a href="{{ path('user_signIn') }}" class="nav-link py-0">{{ 'user.signIn.title'|trans }}</a></li>#}
{# <li class="nav-item"><a href="{{ path('user_signUp') }}" class="nav-link py-0">{{ 'user.signUp.title'|trans }}</a></li>#}
{% endif %}
</ul>
</nav>
<!--endregion-->
</div>
{% endblock %}

@ -0,0 +1,21 @@
{% extends "/root.twig" %}
{% block pageContent %}
<!DOCTYPE html>
{% block htmlTag %}
<html lang="{{ app.request.getLocale() }}">
{% endblock %}
{% block headTag %}
<!--suppress HtmlRequiredTitleElement -->
<head>
{% endblock %}
{% block headContent %}{% endblock %}
</head>
{% block bodyTag %}
<body>
{% endblock %}
{% block bodyContent %}{% endblock %}
</body>
</html>
{% endblock %}

@ -0,0 +1 @@
{% block pageContent %}{% endblock %}

@ -0,0 +1,63 @@
{% extends "/html.html.twig" %}
{% block headContent %}
{% block headContentMeta %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
<title>{% block title %}{% endblock %}</title>
{% block CSS %}{% endblock %}
{% block JS_head %}{% endblock %}
{% endblock %}
{% block bodyTag %}
<body id="page-{{ app.current_route }}" class="m-2">
{% endblock %}
{% block bodyContent %}
{% block headerTag %}
<header>
{% endblock %}
{% block headerContent %}{% endblock %}
</header>
{% block divBodyTag %}
<div id="div-body" class="d-flex flex-row">
{% endblock %}
{% block asideLeft %}{% endblock %}
{% block centerDivBodyTag %}
<div id="div-body-center" class="d-flex flex-column flex-grow-1">
{% endblock %}
{% block sectionTop %}{% endblock %}
{% include '/_flashes.html.twig' %}
{% block sectionBefore %}{% endblock %}
{% block mainTag %}
<main>
{% endblock %}
{% block mainContent %}{% endblock %}
</main>
{% block sectionAfter %}{% endblock %}
</div>
{% block asideRight %}{% endblock %}
</div>
{% block footerTag %}
<footer>
{% endblock %}
{% block footerContent %}{% endblock %}
</footer>
<div class="d-none">
{% block bodyHidden %}{% endblock %}
</div>
{% block JS %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
{% endblock %}
Loading…
Cancel
Save