Compare commits
3 Commits
dbe828b8d4
...
851bb45f70
Author | SHA1 | Date |
---|---|---|
![]() |
851bb45f70 | 2 months ago |
![]() |
795c89b9e6 | 2 months ago |
![]() |
c3c5d28aff | 2 months ago |
@ -0,0 +1,3 @@
|
||||
# Recipe Manager
|
||||
|
||||
WebSite for managing game recipes
|
@ -1,10 +1,12 @@
|
||||
import './bootstrap.js';
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* This file will be included onto the page via the importmap() Twig function,
|
||||
* which should already be in your base.html.twig.
|
||||
*/
|
||||
import './styles/app.css';
|
||||
|
||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
||||
//region CSS
|
||||
import './styles/app.scss';
|
||||
//endregion
|
||||
//region Bootstrap
|
||||
import 'bootstrap';
|
||||
//endregion
|
||||
//region jQuery
|
||||
import 'jqueryLocal'; // Declare $ as a global variable, accessible in all files
|
||||
//endregion
|
||||
//region FontAwesome
|
||||
import 'fontawesome';
|
||||
//endregion
|
@ -1,5 +0,0 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
@ -1,15 +1,4 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"controllers": {},
|
||||
"entrypoints": []
|
||||
}
|
||||
|
@ -0,0 +1,148 @@
|
||||
//region Datatables.net
|
||||
import 'datatables.net';
|
||||
import 'datatables.net-dt/css/dataTables.dataTables.min.css';
|
||||
//endregion
|
||||
//region Bootstrap 5 styling for Datatables.net
|
||||
import 'datatables.net-bs5';
|
||||
import 'datatables.net-bs5/css/dataTables.bootstrap5.min.css';
|
||||
//endregion
|
||||
//region Custom style for Datatables.net
|
||||
import './styles/datatables.scss';
|
||||
//endregion
|
||||
import 'jqueryLocal';
|
||||
|
||||
$(function () {
|
||||
$('table.table-datatable').each(function () {
|
||||
const self = $(this);
|
||||
|
||||
//region Options du datatable
|
||||
let options = {
|
||||
paging: self.data('sortPaging', false),
|
||||
pageLength: self.data('sortPerPage', 50),
|
||||
processing: true,
|
||||
layout: self.data(
|
||||
'sortLayout',
|
||||
{
|
||||
topStart: null,
|
||||
topEnd: null,
|
||||
bottomStart: null,
|
||||
bottomEnd: 'paging',
|
||||
},
|
||||
),
|
||||
};
|
||||
//endregion
|
||||
//region Gestion tri initial
|
||||
let initialSort = [];
|
||||
$('> thead > tr > th[data-sort-onLoad]', this).each(function () {
|
||||
let self = $(this);
|
||||
initialSort.push(
|
||||
{
|
||||
index: self.index(),
|
||||
priority: self.data('sortOnload'),
|
||||
direction: self.data('sortOrder', 'asc').toLowerCase(),
|
||||
},
|
||||
);
|
||||
});
|
||||
initialSort.sort(function (sort1, sort2) {
|
||||
if (sort1.priority === sort2.priority) {
|
||||
return sort1.index - sort2.index;
|
||||
}
|
||||
|
||||
return sort1.priority - sort2.priority;
|
||||
});
|
||||
|
||||
if (initialSort.length > 0) {
|
||||
options.order = [];
|
||||
initialSort.forEach(function (sortOrder) {
|
||||
options.order.push([sortOrder.index, sortOrder.direction]);
|
||||
});
|
||||
}
|
||||
//endregion
|
||||
//region Gestion sens de tri (1er clic ou initial si concerné)
|
||||
let descSort = [];
|
||||
$('> thead > tr > th[data-sort-order]', this).each(function () {
|
||||
const self = $(this);
|
||||
if (self.data('sortOrder', 'asc').toLowerCase() === 'desc') {
|
||||
descSort.push(self.index());
|
||||
}
|
||||
});
|
||||
if (descSort.length > 0) {
|
||||
if (!options.hasOwnProperty('columnDefs')) {
|
||||
options.columnDefs = [];
|
||||
}
|
||||
options.columnDefs.push(
|
||||
{
|
||||
targets: descSort,
|
||||
orderSequence: ['desc', 'asc'],
|
||||
},
|
||||
);
|
||||
}
|
||||
//endregion
|
||||
//region Gestion désactivation tri
|
||||
let disabledSort = [];
|
||||
$('> thead > tr > th[data-sort]', this).each(function () {
|
||||
const self = $(this);
|
||||
if (!self.data('sort', true)) {
|
||||
disabledSort.push(self.index());
|
||||
}
|
||||
});
|
||||
if (disabledSort.length > 0) {
|
||||
if (!options.hasOwnProperty('columnDefs')) {
|
||||
options.columnDefs = [];
|
||||
}
|
||||
options.columnDefs.push(
|
||||
{
|
||||
targets: disabledSort,
|
||||
orderable: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
//endregion
|
||||
//region Gestion nom des colonnes
|
||||
const columnsName = $('> thead > tr > th[data-sort-name]', this);
|
||||
if (columnsName.length > 0) {
|
||||
if (!options.hasOwnProperty('columnDefs')) {
|
||||
options.columnDefs = [];
|
||||
}
|
||||
|
||||
columnsName.each(function () {
|
||||
const self = $(this);
|
||||
const name = self.data('sortName', '');
|
||||
if (name !== '') {
|
||||
options.columnDefs.push(
|
||||
{
|
||||
targets: self.index(),
|
||||
name: name,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
//endregion
|
||||
//region Gestion de AJAX
|
||||
const ajaxUrl = self.data('sortAjax', false);
|
||||
if (ajaxUrl !== false) {
|
||||
options.serverSide = true;
|
||||
options.ajax = {
|
||||
url: ajaxUrl,
|
||||
type: 'POST',
|
||||
//createCDATASection: '',
|
||||
cache: self.data('sortAjaxCache', false),
|
||||
};
|
||||
|
||||
options.columns = [];
|
||||
columnsName.each(function () {
|
||||
const self = $(this);
|
||||
const name = self.data('sortName', '');
|
||||
if (name !== '') {
|
||||
options.columns[self.index()] = {
|
||||
data: name,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
//endregion
|
||||
|
||||
self.DataTable(options);
|
||||
});
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import '@fortawesome/fontawesome-svg-core/styles.min.css';
|
||||
|
||||
import {library} from '@fortawesome/fontawesome-svg-core';
|
||||
import {dom} from '@fortawesome/fontawesome-svg-core';
|
||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
library.add(fas);
|
||||
dom.watch();
|
@ -0,0 +1,44 @@
|
||||
//region Import jQuery itself
|
||||
import $ from 'jquery';
|
||||
//endregion
|
||||
import Utils from 'utils';
|
||||
|
||||
window.$ = $; // Ensure $ is available everywhere
|
||||
window.jQuery = $; // Ensure global “jQuery” property available: necessary to use bootstrap event in jQuery.on ?
|
||||
|
||||
(function ($) {
|
||||
//region .data
|
||||
const jqueryDataOrig = $.fn.data;
|
||||
|
||||
/**
|
||||
* Get a DOM "data" value
|
||||
*
|
||||
* @param {string} key The "data" key
|
||||
* @param {any} defaultValue The default value
|
||||
*
|
||||
* @return {any} The "data" value
|
||||
*/
|
||||
$.fn.data = function (key, defaultValue = undefined) {
|
||||
const value = jqueryDataOrig.apply(this, [key]);
|
||||
if (Utils.isUndefined(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
//endregion
|
||||
//region .matchTag
|
||||
/**
|
||||
* Check if a jQuery element is one of the tags
|
||||
*
|
||||
* @param {string} tag The tag to check
|
||||
* @param {string} extraTags Extra tags to check
|
||||
*
|
||||
* @return {boolean} True if the element is one of the tags
|
||||
*/
|
||||
$.fn.matchTag = function (tag, ...extraTags) {
|
||||
extraTags.push(tag);
|
||||
extraTags = extraTags.map((tag) => tag.toUpperCase());
|
||||
return $.inArray(this.prop('tagName').toUpperCase(), extraTags) !== -1;
|
||||
};
|
||||
//endregion
|
||||
})($);
|
@ -0,0 +1,72 @@
|
||||
//region Fonctions de base de Bootstrap
|
||||
@import '../../vendor/twbs/bootstrap/scss/functions';
|
||||
//endregion
|
||||
//region Color theme
|
||||
//$primary : rgb(0, 0, 0);
|
||||
//$secondary : rgb(102, 102, 102);
|
||||
|
||||
//endregion
|
||||
//region Autres variables Bootstrap
|
||||
$alert-padding-y : 0.25rem;
|
||||
$alert-padding-x : 0.25rem;
|
||||
$alert-margin-bottom : 0.5rem;
|
||||
//endregion
|
||||
//region Les autres variables de Boostrap
|
||||
@import '../../vendor/twbs/bootstrap/scss/variables';
|
||||
@import '../../vendor/twbs/bootstrap/scss/variables-dark';
|
||||
//endregion
|
||||
//region Maps personnalisés
|
||||
|
||||
//endregion
|
||||
//region Reste de la configuration de Bootstrap
|
||||
@import '../../vendor/twbs/bootstrap/scss/maps';
|
||||
@import '../../vendor/twbs/bootstrap/scss/mixins';
|
||||
@import '../../vendor/twbs/bootstrap/scss/utilities';
|
||||
//endregion
|
||||
//region Layout & components de Bootstrap
|
||||
@import '../../vendor/twbs/bootstrap/scss/root';
|
||||
@import '../../vendor/twbs/bootstrap/scss/reboot';
|
||||
@import '../../vendor/twbs/bootstrap/scss/type';
|
||||
@import '../../vendor/twbs/bootstrap/scss/images';
|
||||
@import '../../vendor/twbs/bootstrap/scss/containers';
|
||||
@import '../../vendor/twbs/bootstrap/scss/grid';
|
||||
@import '../../vendor/twbs/bootstrap/scss/tables';
|
||||
@import '../../vendor/twbs/bootstrap/scss/forms';
|
||||
@import '../../vendor/twbs/bootstrap/scss/buttons';
|
||||
@import '../../vendor/twbs/bootstrap/scss/transitions';
|
||||
@import '../../vendor/twbs/bootstrap/scss/dropdown';
|
||||
@import '../../vendor/twbs/bootstrap/scss/button-group';
|
||||
@import '../../vendor/twbs/bootstrap/scss/nav';
|
||||
@import '../../vendor/twbs/bootstrap/scss/navbar';
|
||||
@import '../../vendor/twbs/bootstrap/scss/card';
|
||||
@import '../../vendor/twbs/bootstrap/scss/accordion';
|
||||
@import '../../vendor/twbs/bootstrap/scss/breadcrumb';
|
||||
@import '../../vendor/twbs/bootstrap/scss/pagination';
|
||||
@import '../../vendor/twbs/bootstrap/scss/badge';
|
||||
@import '../../vendor/twbs/bootstrap/scss/alert';
|
||||
@import '../../vendor/twbs/bootstrap/scss/progress';
|
||||
@import '../../vendor/twbs/bootstrap/scss/list-group';
|
||||
@import '../../vendor/twbs/bootstrap/scss/close';
|
||||
@import '../../vendor/twbs/bootstrap/scss/toasts';
|
||||
@import '../../vendor/twbs/bootstrap/scss/modal';
|
||||
@import '../../vendor/twbs/bootstrap/scss/tooltip';
|
||||
@import '../../vendor/twbs/bootstrap/scss/popover';
|
||||
@import '../../vendor/twbs/bootstrap/scss/carousel';
|
||||
@import '../../vendor/twbs/bootstrap/scss/spinners';
|
||||
@import '../../vendor/twbs/bootstrap/scss/offcanvas';
|
||||
@import '../../vendor/twbs/bootstrap/scss/placeholders';
|
||||
//endregion
|
||||
//region Helpers de Boostrap
|
||||
@import '../../vendor/twbs/bootstrap/scss/helpers';
|
||||
//endregion
|
||||
//region API de Bootstrap
|
||||
@import '../../vendor/twbs/bootstrap/scss/utilities/api';
|
||||
//endregion
|
||||
//region Classes complémentaires
|
||||
.text-smallCaps {
|
||||
font-variant : small-caps;
|
||||
}
|
||||
.text-overflow-ellipsis {
|
||||
text-overflow : '…';
|
||||
}
|
||||
//endregion
|
@ -0,0 +1,26 @@
|
||||
header, footer {
|
||||
background-color : var(--bs-body-bg);
|
||||
width : 100%;
|
||||
margin : 0;
|
||||
|
||||
&:not(.overlay-not-fixed) {
|
||||
position : fixed;
|
||||
z-index : 10;
|
||||
}
|
||||
}
|
||||
header {
|
||||
top : 0;
|
||||
height : 30px;
|
||||
border-bottom : 1px solid black;
|
||||
}
|
||||
footer {
|
||||
bottom : 0;
|
||||
}
|
||||
|
||||
#div-body {
|
||||
padding-top : 21px;
|
||||
padding-bottom : 0;
|
||||
}
|
||||
html {
|
||||
scroll-padding-top : 21px
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
pre.xdebug-var-dump {
|
||||
position : relative;
|
||||
background-color : rgb(255, 255, 255);
|
||||
z-index : 10000;
|
||||
margin-bottom : 0;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
body {
|
||||
background-color: skyblue;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
@import 'bootstrap';
|
||||
@import 'xdebug';
|
||||
@import 'layout';
|
||||
|
||||
.required:not(.form-check-label) {
|
||||
color : var(--bs-red);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order::after,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order::after {
|
||||
margin-top : 3px;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order,
|
||||
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order,
|
||||
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order,
|
||||
table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
|
||||
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
|
||||
position : relative;
|
||||
left : 10px;
|
||||
top : none;
|
||||
bottom : none;
|
||||
}
|
||||
|
||||
div.dt-container .dt-paging .dt-paging-button {
|
||||
padding : 0;
|
||||
margin : 0;
|
||||
}
|
||||
|
||||
table.dataTable th.dt-type-numeric,
|
||||
table.dataTable th.dt-type-date,
|
||||
table.dataTable td.dt-type-numeric,
|
||||
table.dataTable td.dt-type-date {
|
||||
text-align : left;
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
export default class Utils {
|
||||
/**
|
||||
* Une variable vaut-elle "undefined" ?
|
||||
*
|
||||
* @param {any} variable La variable à tester
|
||||
*
|
||||
* @returns {boolean} Vrai si la variable vaut "undefined", sinon Faux
|
||||
*/
|
||||
static isUndefined (variable) {
|
||||
return typeof variable === 'undefined';
|
||||
}
|
||||
/**
|
||||
* Une variable est-elle une chaîne de caractère ?
|
||||
*
|
||||
* @param {any} variable La variable à tester
|
||||
*
|
||||
* @returns {boolean} Vrai si la variable est uen chaîne de caractères, sinon Faux
|
||||
*/
|
||||
static isString (variable) {
|
||||
return typeof variable === 'string' || variable instanceof String;
|
||||
}
|
||||
/**
|
||||
* Une variable est-elle une chaine de caractère vide ou équivalent (undefined ou null) ?
|
||||
*
|
||||
* @param {any} variable La variable à tester
|
||||
*
|
||||
* @returns {boolean} Vrai si la variable est une chaine de caractères vide (ou équivalent)
|
||||
*/
|
||||
static empty_str (variable) {
|
||||
return Utils.isUndefined(variable) || variable === null || variable === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie qu'au moins une case à cocher d'une liste de cases à cocher est bien cochée
|
||||
*
|
||||
* @param {jQuery} list Une liste de case à cocher
|
||||
*
|
||||
* @returns {boolean} Vrai si au moins l'un de case à cocher est cochée.
|
||||
*/
|
||||
static checkAtLeastOnChecked (list) {
|
||||
return list.filter(':checked').length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Est-ce qu'un objet jQuery possède un attribut et qu'il est différent de Faux
|
||||
*
|
||||
* @param {jQuery} objJquery L'objet jQuery
|
||||
* @param {string} attrName Le nom de l'attribut
|
||||
*
|
||||
* @returns {boolean} Vrai si l'attribut existe et est différent de Faux.
|
||||
*/
|
||||
static hasAttr (objJquery, attrName) {
|
||||
let val = objJquery.attr(attrName);
|
||||
return !Utils.isUndefined(val) && val !== false;
|
||||
}
|
||||
/**
|
||||
* Récupère le formulaire associé à un champ
|
||||
*
|
||||
* Tient compte de l'attribut "form" si renseigné.
|
||||
*
|
||||
* @param {jQuery} input Le champ pour lequel on veut le formulaire
|
||||
*
|
||||
* @returns {jQuery} Le formulaire correspondant
|
||||
*/
|
||||
static getFormOfInput (input) {
|
||||
let form;
|
||||
|
||||
if (Utils.hasAttr(input, 'form')) {
|
||||
form = $('#' + input.attr('form'));
|
||||
}
|
||||
else {
|
||||
form = input.parents('form').first();
|
||||
}
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime les doublons d'un tableau
|
||||
*
|
||||
* @param {any[]} array Le tableau à dédoublonner
|
||||
*
|
||||
* @returns {any[]} Le tableau dédoublonné
|
||||
*/
|
||||
static array_uniq (array) {
|
||||
return Array.from(new Set(array));
|
||||
}
|
||||
|
||||
/**
|
||||
* Met la première lettre d'une chaine de caractères en majuscule
|
||||
*
|
||||
* @param {string} string La chaine de caractères
|
||||
*
|
||||
* @returns {string} La chaine de caractères avec la première lettre en majuscule
|
||||
*/
|
||||
static string_ucFirst (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la taille RÉELLE d'un élément jQuery
|
||||
*
|
||||
* Prends la plus grande valeur entre la hauteur CSS de l'élément et la hauteur réelle de ses enfants.
|
||||
*
|
||||
* Cela permet de "corriger" le problème des conteneurs plus petit que leurs enfants
|
||||
*
|
||||
* @param {jQuery} element
|
||||
*
|
||||
* @returns {int} La hauteur réelle (en px)
|
||||
*/
|
||||
static getElementRealHeight (element) {
|
||||
if (element.length <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const elementHeight = element.outerHeight(true);
|
||||
|
||||
let childrenHeight = 0;
|
||||
element.children().each(function () {
|
||||
childrenHeight = Utils.getElementRealHeight($(this));
|
||||
});
|
||||
|
||||
return parseInt(childrenHeight > elementHeight ? childrenHeight : elementHeight, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute des arguments à une URL
|
||||
*
|
||||
* @param {object} arguments Les arguments à ajouter
|
||||
* @param {string} url L'URL à laquelle ajouter les arguments ; celle actuelle (location.href) si non fournie
|
||||
*
|
||||
* @return {string} L'URL de résultat
|
||||
*/
|
||||
static addArgumentsToUrl (args, url = undefined) {
|
||||
if (Utils.isUndefined(url)) {
|
||||
url = document.location.href;
|
||||
}
|
||||
|
||||
let urlObj = new URL(url);
|
||||
for (const [argumentName, argumentValue] of Object.entries(args)) {
|
||||
urlObj.searchParams.set(argumentName, argumentValue);
|
||||
}
|
||||
|
||||
return urlObj.toString();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
default_locale: fr
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
- fr
|
||||
providers:
|
||||
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250525095556 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', available_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', delivered_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE messenger_messages
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250525104942 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(100) NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(100) DEFAULT NULL, roles JSON NOT NULL COMMENT '(DC2Type:json)', UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE user
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -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,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\User\SignUpFormType;
|
||||
use App\Misc\FlashType;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\ConnectedUserService;
|
||||
use App\Service\LoggerService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use LogicException;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Controllers for user pages
|
||||
*/
|
||||
#[Route('/user')]
|
||||
class UserController extends AbstractController {
|
||||
/**
|
||||
* @var TranslatorInterface The translator service
|
||||
*/
|
||||
private readonly TranslatorInterface $translator;
|
||||
/**
|
||||
* @var ConnectedUserService The connected user service
|
||||
*/
|
||||
private readonly ConnectedUserService $connectedUserService;
|
||||
|
||||
/**
|
||||
* Initialisation
|
||||
*
|
||||
* @param TranslatorInterface $translator The translator service
|
||||
* @param ConnectedUserService $connectedUserService The connected user service
|
||||
*/
|
||||
public function __construct (TranslatorInterface $translator, ConnectedUserService $connectedUserService) {
|
||||
$this->translator = $translator;
|
||||
$this->connectedUserService = $connectedUserService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*
|
||||
* @param Request $request The request
|
||||
* @param UserPasswordHasherInterface $userPasswordHasher The password hashing service
|
||||
* @param EntityManagerInterface $entityManager The entity manager
|
||||
*
|
||||
* @return Response The response
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
#[Route('/signUp', name: 'user_signUp')]
|
||||
public function signUp (Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response {
|
||||
if (($response = $this->connectedUserService->checkNotConnected())) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
|
||||
$form = $this->createForm(SignUpFormType::class, $user);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
//region Encode the plain password
|
||||
$user->setPassword(
|
||||
$userPasswordHasher->hashPassword(
|
||||
$user,
|
||||
$form->get('newPassword')->getData()
|
||||
)
|
||||
);
|
||||
//endregion
|
||||
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->addFlash(FlashType::SUCCESS, 'Votre compte a bien été créé.');;
|
||||
return $this->redirectToRoute('user_signIn');
|
||||
}
|
||||
|
||||
return $this->render('User/SignUp.html.twig', [
|
||||
'signUpForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign in a user
|
||||
*
|
||||
* @param AuthenticationUtils $authenticationUtils Security errors from query
|
||||
*
|
||||
* @return Response The response
|
||||
*/
|
||||
#[Route(path: '/signIn', name: 'user_signIn')]
|
||||
public function login (AuthenticationUtils $authenticationUtils): Response {
|
||||
if (($response = $this->connectedUserService->checkNotConnected())) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (($error = $authenticationUtils->getLastAuthenticationError()) !== null) {
|
||||
$this->addFlash(FlashType::ERROR, $this->translator->trans($error->getMessageKey(), $error->getMessageData(), 'security'));
|
||||
}
|
||||
return $this->render(
|
||||
'User/SignIn.html.twig',
|
||||
[
|
||||
'last_username' => $authenticationUtils->getLastUsername(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*
|
||||
* <b>NOTE :</b> dummy controller, intercepted by firewall
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Route(path: '/signOut', name: 'user_signOut')]
|
||||
public function logout (): void {
|
||||
throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
||||
}
|
||||
}
|
@ -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,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\User;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* The form for user registration (sign up)
|
||||
*/
|
||||
class SignUpFormType extends AbstractType {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function configureOptions (OptionsResolver $resolver): void {
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => User::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function buildForm (FormBuilderInterface $builder, array $options): void {
|
||||
$builder
|
||||
->add('email', null, [
|
||||
'label' => 'Email',
|
||||
])
|
||||
->add('name', null, [
|
||||
'label' => 'Nom',
|
||||
])
|
||||
->add('newPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'mapped' => false,
|
||||
'attr' => ['autocomplete' => 'new-password'],
|
||||
'invalid_message' => 'Les mots de passe doivent correspondre.',
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
[
|
||||
'message' => 'Le mot de passe est requis.',
|
||||
]
|
||||
),
|
||||
new Length(
|
||||
[
|
||||
'min' => 6,
|
||||
'minMessage' => 'Le mot de passe doit faire au moins {{ limit }} caractères.',
|
||||
'max' => 4096, // max length allowed by Symfony for security reasons
|
||||
]
|
||||
),
|
||||
],
|
||||
'first_options' => [
|
||||
'label' => 'Mot de passe',
|
||||
],
|
||||
'second_options' => [
|
||||
'label' => 'Confirmation du mot de passe',
|
||||
],
|
||||
])
|
||||
->add('agreeTerms', CheckboxType::class, [
|
||||
'mapped' => false,
|
||||
'label' => 'J\'accepte les CGU',
|
||||
'constraints' => [
|
||||
new IsTrue(
|
||||
[
|
||||
'message' => 'Vous devez accepter les CGU.',
|
||||
]
|
||||
),
|
||||
],
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'S\'enregistrer',
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Misc;
|
||||
|
||||
/**
|
||||
* Common types of flash message
|
||||
*/
|
||||
class FlashType {
|
||||
/**
|
||||
* Information message
|
||||
*/
|
||||
public const INFO = 'info';
|
||||
/**
|
||||
* Success message
|
||||
*/
|
||||
public const SUCCESS = 'success';
|
||||
/**
|
||||
* Warning message
|
||||
*/
|
||||
public const WARNING = 'warning';
|
||||
/**
|
||||
* Error message
|
||||
*/
|
||||
public const ERROR = 'danger';
|
||||
}
|
@ -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,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LoggerTrait;
|
||||
use Psr\Log\LogLevel;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Service for logging
|
||||
*/
|
||||
class LoggerService implements LoggerInterface {
|
||||
use LoggerTrait;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface The logger service base
|
||||
*/
|
||||
public readonly LoggerInterface $loggerBase;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param LoggerInterface $loggerBase The logger service base
|
||||
*/
|
||||
public function __construct (LoggerInterface $loggerBase) {
|
||||
$this->loggerBase = $loggerBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function log ($level, string|Stringable $message, array $context = []): void {
|
||||
$this->loggerBase->log($level, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an exception
|
||||
*
|
||||
* @param Throwable $exception The exception
|
||||
* @param array $context The context (automatically add the exception in "throwable")
|
||||
* @param string $level The log level
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function exception (Throwable $exception, array $context = [], string $level = LogLevel::ERROR): void {
|
||||
$context['throwable'] = $exception;
|
||||
$this->log(
|
||||
$level,
|
||||
'Exception [' . get_class($exception) . ']: ' . $exception->getMessage(),
|
||||
$context
|
||||
);
|
||||
}
|
||||
}
|
@ -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,33 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Connexion - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block mainContent %}
|
||||
<h1>Connexion</h1>
|
||||
<form method="post">
|
||||
<div class="mb-3 row">
|
||||
<label for="email" class="col-form-label col-sm-2">Email</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" value="{{ last_username }}" name="_username" id="email" class="form-control" autocomplete="email" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<label for="password" class="col-form-label col-sm-2">Mot de passe</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" name="_password" id="password" class="form-control" autocomplete="current-password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="_remember_me" id="remember_me" class="form-check-input">
|
||||
<label for="remember_me" class="form-check-label">Se souvenir de moi</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
<button class="btn btn-lg btn-primary" type="submit">Se connecter</button>
|
||||
</form>
|
||||
{% endblock %}
|
@ -0,0 +1,8 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Création compte - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block mainContent %}
|
||||
<h1>Création compte</h1>
|
||||
{{ form(signUpForm) }}
|
||||
{% 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 %}
|
@ -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…
Reference in New Issue