init
This commit is contained in:
parent
0939ed4d9e
commit
cc837cf9e5
11
.env
11
.env
|
@ -18,10 +18,6 @@ APP_ENV=dev
|
|||
APP_SECRET=e6e287f176fe2c69112fc620e1801bf0
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
# MAILER_DSN=smtp://localhost
|
||||
###< symfony/mailer ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
|
@ -30,3 +26,10 @@ APP_SECRET=e6e287f176fe2c69112fc620e1801bf0
|
|||
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
|
||||
DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/swiftmailer-bundle ###
|
||||
# For Gmail as a transport, use: "gmail://username:password@localhost"
|
||||
# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode="
|
||||
# Delivery is disabled by default via "null://localhost"
|
||||
MAILER_URL=null://localhost
|
||||
###< symfony/swiftmailer-bundle ###
|
||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -14,3 +14,10 @@
|
|||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/webpack-encore-bundle ###
|
||||
/node_modules/
|
||||
/public/build/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
|
|
38
Makefile
Normal file
38
Makefile
Normal file
|
@ -0,0 +1,38 @@
|
|||
COMPOSER ?= composer
|
||||
PHP ?= php7.4
|
||||
MAGE ?= mage
|
||||
SSH ?= ssh
|
||||
WEBPACK ?= webpack
|
||||
YARN ?= yarn
|
||||
SCHEMASPY ?= schemaspy
|
||||
|
||||
all: dep asset clean
|
||||
|
||||
.ONESHELL:
|
||||
dep:
|
||||
$(COMPOSER) update --ignore-platform-reqs
|
||||
$(COMPOSER) install --ignore-platform-reqs
|
||||
$(YARN)
|
||||
|
||||
asset-watch:
|
||||
$(WEBPACK) -w
|
||||
|
||||
asset:
|
||||
$(YARN)
|
||||
$(WEBPACK)
|
||||
|
||||
clean:
|
||||
rm -fr var/cache/dev/*
|
||||
rm -fr var/cache/prod/*
|
||||
|
||||
browse-dev:
|
||||
x-www-browser https://local.tinternet
|
||||
|
||||
deploy-prod:
|
||||
$(MAGE) deploy prod
|
||||
|
||||
deploy-preprod:
|
||||
$(MAGE) deploy preprod
|
||||
|
||||
doctrine-migration:
|
||||
PHP=$(PHP) ./bin/doctrine-migrate
|
354
assets/css/admin.scss
Normal file
354
assets/css/admin.scss
Normal file
|
@ -0,0 +1,354 @@
|
|||
$theme-colors: (
|
||||
"primary": #1ab5dc,
|
||||
"primary-light": lighten(#3183aa, 40%),
|
||||
"dark-blue": #1e2430,
|
||||
);
|
||||
|
||||
$grid-gutter-width: 0px;
|
||||
$pagination-color: #343a40;
|
||||
$pagination-bg: #ffffff;
|
||||
$pagination-active-color: #ffffff;
|
||||
$pagination-active-bg: #343a40;
|
||||
|
||||
@import "~choices.js/src/styles/choices.scss";
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
@import "~@fortawesome/fontawesome-free/css/all.css";
|
||||
|
||||
#logo {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.choices__list--dropdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.choices__list--dropdown.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-toggle-hide-after {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
&-container {
|
||||
margin-top: 5%;
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
|
||||
&-form {
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
&-image {
|
||||
width: 100%;
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 71px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 71px);
|
||||
padding-top: .5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||
.sidebar-sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.thead-light {
|
||||
a, th {
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
tr.table-primary-light {
|
||||
background-color: #ecf5fa;
|
||||
}
|
||||
|
||||
.td-nowrap {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bg-dark-blue {
|
||||
background: #242b3b;
|
||||
color: #fff;
|
||||
|
||||
.nav-item-label {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
border-left: 4px solid map-get($theme-colors, 'dark-blue');
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
|
||||
.fa {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 5px;
|
||||
min-width: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
border-left: 4px solid map-get($theme-colors, 'primary');
|
||||
background: map-get($theme-colors, 'dark-blue');
|
||||
}
|
||||
}
|
||||
|
||||
&-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1130px) {
|
||||
.nav-link {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 770px) {
|
||||
.nav {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
*[data-selectable-selector] {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
*[data-selectable-selector] {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
height: 35px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding-top: 71px;
|
||||
|
||||
.nav {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 580px) {
|
||||
.body {
|
||||
margin-left: 45px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 50px;
|
||||
max-width: 100% !important;
|
||||
|
||||
.sidebar-sticky {
|
||||
width: 50px;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.table-fixed, .table-fixed > table {
|
||||
width: 100%;
|
||||
|
||||
tbody {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
thead, tbody, tr, td, th{
|
||||
display: block;
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
float: left;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
tr {
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
float: left;
|
||||
|
||||
&.sorted {
|
||||
&::before {
|
||||
content: '\f0dc';
|
||||
font-family: 'FontAwesome';
|
||||
color: #aaa;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
|
||||
.toast-wrapper {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: calc(50% - 150px);
|
||||
z-index: 1000;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-form {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.icon-margin {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.d-ib {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.list-checkbox {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.password-strenth {
|
||||
padding: 0 0 5px 0;
|
||||
margin-top: -4px;
|
||||
|
||||
.col-sm {
|
||||
height: 8px;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
&-info {
|
||||
font-size: 13px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-bell:not([disabled]) {
|
||||
[data-counter]:after {
|
||||
display: block;
|
||||
color: #fff;
|
||||
background: red;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
position: absolute;
|
||||
content: ' ';
|
||||
top: 4px;
|
||||
right: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.custom-file-label::after {
|
||||
content: "Parcourir";
|
||||
}
|
||||
|
||||
#lease_template_html {
|
||||
height: calc(100vh - 270px);
|
||||
}
|
||||
|
||||
.panel {
|
||||
&-toggler {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: block;
|
||||
|
||||
&:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*[data-collection-delete-container] {
|
||||
cursor: pointer;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
*[data-collection-add] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-image {
|
||||
width: 50%;
|
||||
}
|
BIN
assets/img/logo.png
Normal file
BIN
assets/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
31
assets/js/addons/checkbox-checker.js
Normal file
31
assets/js/addons/checkbox-checker.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-checkbox-ckecker]').click(function() {
|
||||
const wrapperName = $(this).attr('data-checkbox-ckecker');
|
||||
|
||||
if (!wrapperName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkboxes = $('*[data-checkbox-wrapper="' + wrapperName + '"] *[data-checkbox] input[type="checkbox"]');
|
||||
|
||||
$(checkboxes).each(function(i, v) {
|
||||
$(v).prop('checked', true);
|
||||
})
|
||||
})
|
||||
|
||||
$('*[data-checkbox-unckecker]').click(function() {
|
||||
const wrapperName = $(this).attr('data-checkbox-unckecker');
|
||||
|
||||
if (!wrapperName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkboxes = $('*[data-checkbox-wrapper="' + wrapperName + '"] *[data-checkbox] input[type="checkbox"]');
|
||||
|
||||
$(checkboxes).each(function(i, v) {
|
||||
$(v).prop('checked', false);
|
||||
})
|
||||
})
|
||||
};
|
8
assets/js/addons/choices.js
Normal file
8
assets/js/addons/choices.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
const Choices = require('choices.js');
|
||||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-jschoice]').each(function(key, item) {
|
||||
new Choices(item);
|
||||
});
|
||||
}
|
26
assets/js/addons/datepicker.js
Normal file
26
assets/js/addons/datepicker.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const Datepicker = require('vanillajs-datepicker')
|
||||
|
||||
const isDateSupported = () => {
|
||||
const input = document.createElement('input');
|
||||
const value = 'a';
|
||||
|
||||
input.setAttribute('type', 'date');
|
||||
input.setAttribute('value', value);
|
||||
|
||||
return input.value !== value;
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
if (isDateSupported()) {
|
||||
return
|
||||
}
|
||||
|
||||
const inputs = document.querySelectorAll('input[type="date"]')
|
||||
const size = inputs.length
|
||||
|
||||
for (var i = 0, c = inputs.length; i < c; i++) {
|
||||
new Datepicker.Datepicker(inputs[i], {
|
||||
format: 'yyyy-mm-dd'
|
||||
})
|
||||
}
|
||||
}
|
7
assets/js/addons/dbclick.js
Normal file
7
assets/js/addons/dbclick.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-dblclick]').dblclick(function(e) {
|
||||
document.location.href = $(this).attr('data-dblclick');
|
||||
})
|
||||
};
|
43
assets/js/addons/document-selector.js
Normal file
43
assets/js/addons/document-selector.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
let DocumentSelector = () => {
|
||||
let forms = $('.document-selector-form');
|
||||
let btnSubmit = $('#download-archive-form button');
|
||||
|
||||
let handler = function() {
|
||||
forms.each((fi, f) => {
|
||||
let form = $(f);
|
||||
let ids = form.find('.document-selector-ids');
|
||||
let btn = form.find('.document-selector-button');
|
||||
|
||||
ids.html('');
|
||||
let hasSelection = false;
|
||||
|
||||
$('*[data-documents] *[data-selectable-row] input[data-selectable-checkbox]').each((i, c) => {
|
||||
let checkbox = $(c);
|
||||
|
||||
if (checkbox.is(':checked')) {
|
||||
ids.append(checkbox[0].outerHTML);
|
||||
hasSelection = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSelection && btn.length) {
|
||||
btn.removeAttr('disabled');
|
||||
ids.find('input').prop('checked', true);
|
||||
} else {
|
||||
btn.attr('disabled', 'disabled');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$('*[data-documents] *[data-selectable-row]').click(function() {
|
||||
window.setTimeout(handler, 100)
|
||||
});
|
||||
|
||||
$('*[data-documents] *[data-selectable-row]').on('clicked', function() {
|
||||
window.setTimeout(handler, 100)
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = DocumentSelector;
|
23
assets/js/addons/editor.js
Normal file
23
assets/js/addons/editor.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
module.exports = function() {
|
||||
if (typeof tinymce === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
tinymce.init({
|
||||
selector: '*[data-tinymce]',
|
||||
base_url: '/nm/tinymce/',
|
||||
cache_suffix: '?v=4.1.6',
|
||||
language: 'fr_FR',
|
||||
plugins: 'print preview importcss searchreplace visualblocks visualchars fullscreen template table charmap hr pagebreak nonbreaking toc insertdatetime advlist lists wordcount textpattern noneditable help charmap quickbars',
|
||||
menubar: 'file edit view insert format tools table tc help',
|
||||
toolbar: 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap | fullscreen preview | code',
|
||||
importcss_append: true,
|
||||
image_caption: true,
|
||||
quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
|
||||
noneditable_noneditable_class: "mceNonEditable",
|
||||
toolbar_drawer: 'sliding',
|
||||
spellchecker_dialog: true,
|
||||
tinycomments_mode: 'embedded',
|
||||
contextmenu: "link image imagetools table configurepermanentpen",
|
||||
});
|
||||
};
|
84
assets/js/addons/form-collection.js
Normal file
84
assets/js/addons/form-collection.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
const DeleteHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
const target = e.target;
|
||||
let button = $(target);
|
||||
|
||||
if (button.is('[data-collection-delete-container]')) {
|
||||
button = button.find('*[data-collection-delete]').first()
|
||||
}
|
||||
|
||||
const id = button.attr('data-collection-delete');
|
||||
const collection = button.parents('[data-collection]')
|
||||
const item = collection.find('*[data-collection-item="' + id + '"]')
|
||||
|
||||
if (confirm('Validez-vous la suppression ?')) {
|
||||
item.remove();
|
||||
collection.trigger('collection.update');
|
||||
}
|
||||
}
|
||||
|
||||
const CollectionInitilizedAndUpdated = (e) => {
|
||||
const target = $(e.target)
|
||||
|
||||
target.find('*[data-collection-empty]').toggleClass(
|
||||
'd-none',
|
||||
target.find('*[data-collection-item]').length !== 0
|
||||
);
|
||||
|
||||
target.find('*[data-collection-nonempty]').toggleClass(
|
||||
'd-none',
|
||||
target.find('*[data-collection-item]').length === 0
|
||||
);
|
||||
}
|
||||
|
||||
const FormCollection = () => {
|
||||
$('*[data-collection]').on(
|
||||
'collection.update',
|
||||
CollectionInitilizedAndUpdated
|
||||
);
|
||||
|
||||
$('*[data-collection]').on(
|
||||
'collection.init',
|
||||
CollectionInitilizedAndUpdated
|
||||
);
|
||||
|
||||
$('*[data-collection]').on(
|
||||
'click',
|
||||
'*[data-collection-delete], *[data-collection-delete-container]',
|
||||
DeleteHandler
|
||||
);
|
||||
|
||||
$('*[data-collection-add]').click((e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const collectionId = $(e.target).attr('data-collection-add')
|
||||
const collectionContainer = $('*[data-collection="' + collectionId + '"]')
|
||||
const prototypeContent = $('#' + collectionId).html()
|
||||
let name = 0
|
||||
|
||||
collectionContainer.find('*[data-collection-item]').each(function() {
|
||||
var n = parseInt($(this).attr('data-collection-item'))
|
||||
|
||||
if (n >= name) {
|
||||
name = n + 1
|
||||
}
|
||||
})
|
||||
|
||||
collectionContainer.append(prototypeContent)
|
||||
|
||||
const item = collectionContainer.children('*[data-collection-item]:last-child')
|
||||
const deleteBtn = $('<span data-collection-delete="__name__" class="fa fa-trash"></span>')
|
||||
|
||||
item.find('*[data-collection-delete-container]').first().append(deleteBtn)
|
||||
item.html(item.html().replace(/__name__/g, name))
|
||||
item.attr('data-collection-item', name)
|
||||
|
||||
collectionContainer.trigger('collection.update');
|
||||
});
|
||||
|
||||
$('*[data-collection]').trigger('collection.init');
|
||||
}
|
||||
|
||||
module.exports = FormCollection;
|
15
assets/js/addons/form-confirm.js
Normal file
15
assets/js/addons/form-confirm.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-form-confirm]').submit(function(e) {
|
||||
let message = $(this).attr('data-form-confirm');
|
||||
|
||||
if (!message) {
|
||||
message = 'Confimez-vous cette action ?';
|
||||
}
|
||||
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
})
|
||||
};
|
11
assets/js/addons/form.js
Normal file
11
assets/js/addons/form.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('.custom-file-input').on('change', function(event) {
|
||||
let inputFile = event.currentTarget;
|
||||
|
||||
$(inputFile).parent()
|
||||
.find('.custom-file-label')
|
||||
.html(inputFile.files[0].name);
|
||||
});
|
||||
};
|
13
assets/js/addons/modal.js
Normal file
13
assets/js/addons/modal.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-modal]').click((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let id = $(e.target).attr('data-modal');
|
||||
let modal = $(id);
|
||||
|
||||
modal.modal('toggle');
|
||||
});
|
||||
}
|
47
assets/js/addons/panel.js
Normal file
47
assets/js/addons/panel.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
let Pannel = () => {
|
||||
let panels = $('.panel');
|
||||
|
||||
panels.each((i, p) => {
|
||||
let panel = $(p);
|
||||
let content = panel.find('.panel-content').first();
|
||||
let togglers = panel.find('.panel-toggler');
|
||||
|
||||
togglers.each((k, t) => {
|
||||
let toggler = $(t);
|
||||
|
||||
if (!toggler.is('.fa')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.is('.active')) {
|
||||
toggler.removeClass('fa-arrow-down');
|
||||
toggler.addClass('fa-arrow-up');
|
||||
} else {
|
||||
toggler.removeClass('fa-arrow-up');
|
||||
toggler.addClass('fa-arrow-down');
|
||||
}
|
||||
})
|
||||
|
||||
togglers.click(function(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
content.toggleClass('active');
|
||||
|
||||
togglers.each((k, t) => {
|
||||
let toggler = $(t);
|
||||
|
||||
if (!toggler.is('.fa')) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggler
|
||||
.toggleClass('fa-arrow-down')
|
||||
.toggleClass('fa-arrow-up');
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Pannel;
|
82
assets/js/addons/password.js
Normal file
82
assets/js/addons/password.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
const $ = require('jquery');
|
||||
const zxcvbn = require('zxcvbn');
|
||||
|
||||
let scoreColors = [
|
||||
'danger',
|
||||
'danger',
|
||||
'warning',
|
||||
'warning',
|
||||
'success',
|
||||
];
|
||||
|
||||
let scoreInfos = {
|
||||
"This is a top-10 common password": "Parmis le top 10 des mots de passes communs",
|
||||
"This is a top-100 common password": "Parmis le top 100 des mots de passes communs",
|
||||
"This is a very common password": "Mot de passe vraiment trop commun",
|
||||
"This is similar to a commonly used password": "Similaire à un mot de passe commun",
|
||||
"A word by itself is easy to guess": "Ce mot est trop simple à deviner",
|
||||
"Names and surnames by themselves are easy to guess": "Les noms ou les surnoms sont simples à deviner",
|
||||
"Common names and surnames are easy to guess": "Les noms ou les surnoms sont simples à deviner",
|
||||
"Straight rows of keys are easy to guess": "Combinaison de touches trop simple",
|
||||
"Short keyboard patterns are easy to guess": "Combinaison de touches trop simple",
|
||||
"Repeats like \"aaa\" are easy to guess'": "Les répétitions comme \"aaa\" sont simples à deviner",
|
||||
"Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Les répétitions comme \"abcabcabc\" sont simples à deviner",
|
||||
"Sequences like abc or 6543 are easy to guess": "Les séquences comme \"abc\" ou \"6543\" sont simples à deviner",
|
||||
"Recent years are easy to guess": "Les années sont simples à deviner",
|
||||
"Dates are often easy to guess": "Les dates sont souvent simples à deviner",
|
||||
}
|
||||
|
||||
let checkPassword = function(password, confirmation, indicator, submit) {
|
||||
let result = zxcvbn(password.val());
|
||||
let score = result.score;
|
||||
let cols = indicator.children('.col-sm');
|
||||
let info = indicator.children('.password-strenth-info');
|
||||
|
||||
info.text('');
|
||||
cols.attr('class', 'col-sm');
|
||||
|
||||
for (var u = 0; u <= 5; u++) {
|
||||
let col = cols.eq(u);
|
||||
if (u <= score) {
|
||||
col.addClass('bg-' + scoreColors[score]);
|
||||
} else {
|
||||
col.addClass('bg-light');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(result)
|
||||
|
||||
info.text(scoreInfos[result.feedback.warning]);
|
||||
info.attr('class', 'col-12 password-strenth-info text-' + scoreColors[score]);
|
||||
|
||||
if (score < 4 || confirmation.val() !== password.val()) {
|
||||
submit.attr('disabled', 'disabled');
|
||||
} else {
|
||||
submit.removeAttr('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
let passwordNew = $('#form-password-new');
|
||||
let passwordConfirmation = $('#form-password-confirmation');
|
||||
let passwordSubmit = $('#form-password-submit');
|
||||
let passwordStrength = $('#form-password-strength');
|
||||
|
||||
if (passwordStrength.length) {
|
||||
passwordNew.keyup(function() {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit);
|
||||
});
|
||||
|
||||
passwordNew.change(function() {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit);
|
||||
});
|
||||
|
||||
passwordConfirmation.keyup(function() {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit);
|
||||
});
|
||||
|
||||
passwordConfirmation.change(function() {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit);
|
||||
});
|
||||
}
|
||||
};
|
44
assets/js/addons/push-state.js
Normal file
44
assets/js/addons/push-state.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-pushstate]').click((e) => {
|
||||
var url = $(e.target).attr('data-pushstate');
|
||||
|
||||
history.pushState({url: url}, null, url);
|
||||
history.replaceState({url: url}, null, url);
|
||||
});
|
||||
|
||||
let forms = $('form[data-formpushstate]');
|
||||
|
||||
let checkAndUsePushState = () => {
|
||||
let state = [window.location.pathname, window.location.search].join('');
|
||||
|
||||
$('*[data-pushstate]').each((i, v) => {
|
||||
let method = 'compare';
|
||||
|
||||
if ($(v).is('[data-pushstate-method]')) {
|
||||
method = $(v).attr('data-pushstate-method')
|
||||
}
|
||||
|
||||
var isThisOne = false;
|
||||
|
||||
if (method === 'compare' && $(v).attr('data-pushstate') === state) {
|
||||
isThisOne = true;
|
||||
}
|
||||
|
||||
if (method === 'indexOf' && state.indexOf($(v).attr('data-pushstate')) !== -1) {
|
||||
isThisOne = true;
|
||||
}
|
||||
|
||||
if (isThisOne) {
|
||||
$(v).click();
|
||||
|
||||
forms.attr('action', state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkAndUsePushState();
|
||||
|
||||
$(window).on('statechange', checkAndUsePushState, false);
|
||||
}
|
27
assets/js/addons/rest-choices.js
Normal file
27
assets/js/addons/rest-choices.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const $ = require('jquery');
|
||||
const Choices = require('choices.js');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-rest-choices]').each(function(key, item) {
|
||||
const url = $(this).attr('data-rest-choices');
|
||||
|
||||
new Choices(item, {
|
||||
searchPlaceholderValue: 'Chercher',
|
||||
}).setChoices(function() {
|
||||
return fetch(url)
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
return data.map(function(d) {
|
||||
return {
|
||||
label: d.label,
|
||||
value: d.value
|
||||
};
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function(instance) {
|
||||
});
|
||||
})
|
||||
};
|
24
assets/js/addons/table-fixed.js
Normal file
24
assets/js/addons/table-fixed.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
let resizeTbody = (tbody) => {
|
||||
tbody.height($(window).height() - tbody.offset().top - 20);
|
||||
}
|
||||
|
||||
let tableFixed = () => {
|
||||
let tables = $('table[data-table-fixed], *[data-table-fixed] > table');
|
||||
|
||||
tables.each((i, t) => {
|
||||
let table = $(t);
|
||||
table.addClass('table-fixed');
|
||||
|
||||
let tbody = table.find('tbody');
|
||||
|
||||
resizeTbody(tbody);
|
||||
|
||||
$(window).resize(function() {
|
||||
resizeTbody(tbody);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = tableFixed;
|
122
assets/js/addons/table-selectable.js
Normal file
122
assets/js/addons/table-selectable.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
const selectedClass = 'table-primary-light';
|
||||
|
||||
let toggleRow = (row, checkbox, checkboxIsClicked) => {
|
||||
row.toggleClass(selectedClass);
|
||||
|
||||
if (checkboxIsClicked) {
|
||||
checkbox.prop('checked', checkbox.prop('checked'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkbox.length) {
|
||||
checkbox.prop('checked', !checkbox.prop('checked'));
|
||||
}
|
||||
}
|
||||
|
||||
let unactiveRow = (row, checkbox) => {
|
||||
row.removeClass(selectedClass);
|
||||
|
||||
if (checkbox.length) {
|
||||
checkbox.prop('checked', false);
|
||||
}
|
||||
}
|
||||
|
||||
let activeRow = (row, checkbox) => {
|
||||
row.addClass(selectedClass);
|
||||
|
||||
if (checkbox.length) {
|
||||
checkbox.prop('checked', true);
|
||||
}
|
||||
}
|
||||
|
||||
let tableSelectable = () => {
|
||||
let tables = $('*[data-selectable]');
|
||||
|
||||
tables.each((i, t) => {
|
||||
var table = $(t);
|
||||
var rows = table.find('*[data-selectable-row]');
|
||||
let selectedIndex = null;
|
||||
|
||||
var tbody = table.find('tbody');
|
||||
|
||||
var resizer = () => {
|
||||
tbody.height($(window).height() - tbody.offset().top - 20);
|
||||
}
|
||||
|
||||
window.setInterval(resizer, 1000);
|
||||
resizer();
|
||||
$(window).resize(resizer);
|
||||
|
||||
|
||||
((rows) => {
|
||||
rows.each((i, r) => {
|
||||
let row = $(r);
|
||||
let checkbox = row.find('*[data-selectable-checkbox]');
|
||||
let selectors = row.find('*[data-selectable-selector]');
|
||||
|
||||
((row, selectors, checkbox, index) => {
|
||||
selectors.click((e) => {
|
||||
if (event.target.nodeName === 'INPUT') {
|
||||
e.stopPropagation();
|
||||
|
||||
checkbox.trigger('clicked');
|
||||
|
||||
return toggleRow(row, checkbox, true);
|
||||
}
|
||||
|
||||
if (window.event.ctrlKey) {
|
||||
e.preventDefault();
|
||||
|
||||
return toggleRow(row, checkbox);
|
||||
}
|
||||
|
||||
if (window.event.button === 0) {
|
||||
if (!window.event.ctrlKey && !window.event.shiftKey) {
|
||||
rows.each((z, r2) => {
|
||||
unactiveRow($(r2), $(r2).find('*[data-selectable-checkbox]'));
|
||||
});
|
||||
|
||||
toggleRow(row, checkbox);
|
||||
|
||||
if (row.hasClass(selectedClass)) {
|
||||
selectedIndex = index;
|
||||
} else {
|
||||
selectedIndex = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.event.shiftKey) {
|
||||
if (selectedIndex !== null) {
|
||||
rows.each((z, r2) => {
|
||||
if (selectedIndex <= index) {
|
||||
if (z >= selectedIndex && z <= index) {
|
||||
activeRow($(r2), $(r2).find('*[data-selectable-checkbox]'));
|
||||
} else {
|
||||
unactiveRow($(r2), $(r2).find('*[data-selectable-checkbox]'));
|
||||
}
|
||||
} else {
|
||||
if (z <= selectedIndex && z >= index) {
|
||||
activeRow($(r2), $(r2).find('*[data-selectable-checkbox]'));
|
||||
} else {
|
||||
unactiveRow($(r2), $(r2).find('*[data-selectable-checkbox]'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//selectedIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})(row, selectors, checkbox, i);
|
||||
});
|
||||
})(rows);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = tableSelectable;
|
11
assets/js/addons/toast.js
Normal file
11
assets/js/addons/toast.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('.toast').toast({
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 5000,
|
||||
});
|
||||
|
||||
$('.toast').toast('show');
|
||||
};
|
5
assets/js/addons/tooltip.js
Normal file
5
assets/js/addons/tooltip.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const $ = require('jquery');
|
||||
|
||||
module.exports = function() {
|
||||
$('*[data-toggle="tooltip"]').tooltip();
|
||||
};
|
28
assets/js/admin.js
Normal file
28
assets/js/admin.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const imagesContext = require.context(
|
||||
'../img',
|
||||
true, /\.(png|jpg|jpeg|gif|ico|svg|webp)$/
|
||||
);
|
||||
|
||||
imagesContext.keys().forEach(imagesContext);
|
||||
|
||||
import '../css/admin.scss';
|
||||
|
||||
require('../../node_modules/bootstrap/dist/js/bootstrap.min.js');
|
||||
// require('./addons/table-selectable.js')();
|
||||
// require('./addons/table-fixed.js')();
|
||||
// require('./addons/document-selector.js')();
|
||||
require('./addons/form-confirm.js')();
|
||||
require('./addons/form.js')();
|
||||
require('./addons/dbclick.js')();
|
||||
require('./addons/toast.js')();
|
||||
require('./addons/modal.js')();
|
||||
require('./addons/push-state.js')();
|
||||
require('./addons/password.js')();
|
||||
require('./addons/tooltip.js')();
|
||||
require('./addons/editor.js')();
|
||||
require('./addons/panel.js')();
|
||||
require('./addons/choices.js')();
|
||||
require('./addons/checkbox-checker.js')();
|
||||
require('./addons/rest-choices.js')();
|
||||
require('./addons/form-collection.js')();
|
||||
require('./addons/datepicker.js')();
|
7
bin/doctrine-migrate
Executable file
7
bin/doctrine-migrate
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
CLASS_NAME="$(echo "yes" | "$PHP" ./bin/console doctrine:migration:diff -e dev | grep -o "Version[0-9]*" | tail -n 1)"
|
||||
|
||||
if [ -n "$CLASS_NAME" ]; then
|
||||
echo "yes" | "$PHP" ./bin/console doctrine:migration:exec --up "DoctrineMigrations\\$CLASS_NAME" -e dev
|
||||
fi
|
|
@ -7,16 +7,23 @@
|
|||
"php": ">=7.2.5",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"bjeavons/zxcvbn-php": "^1.2",
|
||||
"cocur/slugify": "^4.0",
|
||||
"composer/package-versions-deprecated": "1.11.99.1",
|
||||
"doctrine/annotations": "^1.0",
|
||||
"doctrine/doctrine-bundle": "^2.2",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"knplabs/knp-paginator-bundle": "^5.4",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"sensio/framework-extra-bundle": "^5.1",
|
||||
"scheb/2fa-google-authenticator": "^5.7",
|
||||
"scheb/2fa-qr-code": "^5.7",
|
||||
"sensio/framework-extra-bundle": "^6.1",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "5.2.*",
|
||||
"symfony/console": "5.2.*",
|
||||
"symfony/dotenv": "5.2.*",
|
||||
"symfony/event-dispatcher": "5.2.*",
|
||||
"symfony/expression-language": "5.2.*",
|
||||
"symfony/flex": "^1.3.1",
|
||||
"symfony/form": "5.2.*",
|
||||
|
@ -34,10 +41,12 @@
|
|||
"symfony/security-bundle": "5.2.*",
|
||||
"symfony/serializer": "5.2.*",
|
||||
"symfony/string": "5.2.*",
|
||||
"symfony/swiftmailer-bundle": "^3.5",
|
||||
"symfony/translation": "5.2.*",
|
||||
"symfony/twig-bundle": "^5.2",
|
||||
"symfony/validator": "5.2.*",
|
||||
"symfony/web-link": "5.2.*",
|
||||
"symfony/webpack-encore-bundle": "^1.11",
|
||||
"symfony/yaml": "5.2.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
|
|
|
@ -12,4 +12,8 @@ return [
|
|||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
|
||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
3
config/packages/assets.yaml
Normal file
3
config/packages/assets.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
4
config/packages/dev/swiftmailer.yaml
Normal file
4
config/packages/dev/swiftmailer.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
# See https://symfony.com/doc/current/email/dev_environment.html
|
||||
swiftmailer:
|
||||
# send all emails to a specific address
|
||||
#delivery_addresses: ['me@example.com']
|
4
config/packages/prod/webpack_encore.yaml
Normal file
4
config/packages/prod/webpack_encore.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
#webpack_encore:
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Available in version 1.2
|
||||
#cache: true
|
15
config/packages/scheb_2fa.yaml
Normal file
15
config/packages/scheb_2fa.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
|
||||
scheb_two_factor:
|
||||
google:
|
||||
enabled: true
|
||||
issuer: "Tinternet & cie"
|
||||
server_name:
|
||||
digits: 6
|
||||
window: 1
|
||||
template: auth/2fa.html.twig
|
||||
security_tokens:
|
||||
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
|
||||
# If you're using guard-based authentication, you have to use this one:
|
||||
- Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
|
||||
# If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
|
||||
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
|
@ -1,24 +1,49 @@
|
|||
security:
|
||||
encoders:
|
||||
App\Entity\User:
|
||||
algorithm: auto
|
||||
|
||||
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-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
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_WRITER: ROLE_USER
|
||||
ROLE_ADMIN: ROLE_WRITER
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
anonymous: true
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#firewalls-authentication
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
anonymous: ~
|
||||
two_factor:
|
||||
auth_form_path: 2fa_login # The route name you have used in the routes.yaml
|
||||
check_path: 2fa_login_check # The route name you have used in the routes.yaml
|
||||
guard:
|
||||
authenticators:
|
||||
- App\Authenticator\LoginFormAuthenticator
|
||||
form_login:
|
||||
login_path: auth_login
|
||||
check_path: auth_login
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
logout:
|
||||
path: auth_logout
|
||||
target: /
|
||||
remember_me:
|
||||
secret: '%kernel.secret%'
|
||||
lifetime: 604800
|
||||
path: /
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/resetting, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
||||
- { path: ^/admin, roles: ROLE_USER }
|
||||
- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
|
|
3
config/packages/swiftmailer.yaml
Normal file
3
config/packages/swiftmailer.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
swiftmailer:
|
||||
url: '%env(MAILER_URL)%'
|
||||
spool: { type: 'memory' }
|
2
config/packages/test/swiftmailer.yaml
Normal file
2
config/packages/test/swiftmailer.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
swiftmailer:
|
||||
disable_delivery: true
|
2
config/packages/test/webpack_encore.yaml
Normal file
2
config/packages/test/webpack_encore.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
#webpack_encore:
|
||||
# strict_mode: false
|
30
config/packages/webpack_encore.yaml
Normal file
30
config/packages/webpack_encore.yaml
Normal file
|
@ -0,0 +1,30 @@
|
|||
webpack_encore:
|
||||
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
output_path: '%kernel.project_dir%/public/build'
|
||||
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||
# output_path: false
|
||||
|
||||
# Set attributes that will be rendered on all script and link tags
|
||||
script_attributes:
|
||||
defer: true
|
||||
# link_attributes:
|
||||
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
||||
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
|
||||
# preload: true
|
||||
|
||||
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
|
||||
# strict_mode: false
|
||||
|
||||
# If you have multiple builds:
|
||||
# builds:
|
||||
# pass "frontend" as the 3rg arg to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Put in config/packages/prod/webpack_encore.yaml
|
||||
# cache: true
|
|
@ -1,3 +1,16 @@
|
|||
#index:
|
||||
# path: /
|
||||
# controller: App\Controller\DefaultController::index
|
||||
|
||||
#https://symfony.com/doc/current/routing/custom_route_loader.html
|
||||
#admin_routes:
|
||||
# resource: 'admin_route_loader::loadRoutes'
|
||||
# type: service
|
||||
#
|
||||
2fa_login:
|
||||
path: /2fa
|
||||
defaults:
|
||||
_controller: "scheb_two_factor.form_controller:form"
|
||||
|
||||
2fa_login_check:
|
||||
path: /2fa_check
|
||||
|
|
7
config/routes/scheb_2fa.yaml
Normal file
7
config/routes/scheb_2fa.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
2fa_login:
|
||||
path: /2fa
|
||||
defaults:
|
||||
_controller: "scheb_two_factor.form_controller:form"
|
||||
|
||||
2fa_login_check:
|
||||
path: /2fa_check
|
31
package.json
Normal file
31
package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@symfony/stimulus-bridge": "^2.0.0",
|
||||
"@symfony/webpack-encore": "^1.0.0",
|
||||
"core-js": "^3.0.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"sass-loader": "^7.0.1",
|
||||
"stimulus": "^2.0.0",
|
||||
"webpack-notifier": "^1.6.0"
|
||||
},
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev-server": "encore dev-server",
|
||||
"dev": "encore dev",
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"bootstrap": "^4.3.1",
|
||||
"choices.js": "^9.0.1",
|
||||
"jquery": "^3.6.0",
|
||||
"popper.js": "^1.16.0",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"tinymce": "^5.2.0",
|
||||
"vanillajs-datepicker": "^1.1.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
}
|
||||
}
|
66
public/.htaccess
Normal file
66
public/.htaccess
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Use the front controller as index file. It serves as a fallback solution when
|
||||
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex index.php
|
||||
|
||||
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||
# feature in your server configuration. Uncomment the following line if you
|
||||
# install assets as symlinks or if you experience problems related to symlinks
|
||||
# when compiling LESS/Sass/CoffeScript assets.
|
||||
# Options +FollowSymlinks
|
||||
|
||||
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Determine the RewriteBase automatically and set it as environment variable.
|
||||
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||
# project in a subdirectory, the base path will be prepended to allow proper
|
||||
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||
# work in environments without path prefix as well, providing a safe, one-size
|
||||
# fits all solution. But as you do not need it in this case, you can comment
|
||||
# the following 2 lines to eliminate the overhead.
|
||||
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||
RewriteRule .* - [E=BASE:%1]
|
||||
|
||||
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||
RewriteCond %{HTTP:Authorization} .+
|
||||
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||
|
||||
# Redirect to URI without front controller to prevent duplicate content
|
||||
# (with and without `/index.php`). Only do this redirect on the initial
|
||||
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||
# endless redirect loop (request -> rewrite to front controller ->
|
||||
# redirect -> request -> ...).
|
||||
# So in case you get a "too many redirects" error or you always get redirected
|
||||
# to the start page because your Apache does not expose the REDIRECT_STATUS
|
||||
# environment variable, you have 2 choices:
|
||||
# - disable this feature by commenting the following 2 lines or
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
<IfModule mod_alias.c>
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 307 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
1
public/vendor/qrcodejs
vendored
Symbolic link
1
public/vendor/qrcodejs
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../node_modules/qrcodejs
|
98
src/Authenticator/LoginFormAuthenticator.php
Normal file
98
src/Authenticator/LoginFormAuthenticator.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Authenticator;
|
||||
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
private CsrfTokenManagerInterface $csrfTokenManager;
|
||||
|
||||
private UserPasswordEncoderInterface $passwordEncoder;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
$this->passwordEncoder = $passwordEncoder;
|
||||
}
|
||||
|
||||
public function supports(Request $request)
|
||||
{
|
||||
return 'auth_login' === $request->attributes->get('_route') && $request->isMethod('POST');
|
||||
}
|
||||
|
||||
public function getCredentials(Request $request)
|
||||
{
|
||||
$credentials = [
|
||||
'email' => $request->request->get('_username'),
|
||||
'password' => $request->request->get('_password'),
|
||||
'csrf_token' => $request->request->get('_csrf_token'),
|
||||
];
|
||||
|
||||
$request->getSession()->set(Security::LAST_USERNAME, $credentials['email']);
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
public function getUser($credentials, UserProviderInterface $userProvider)
|
||||
{
|
||||
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
|
||||
|
||||
if (!$this->csrfTokenManager->isTokenValid($token)) {
|
||||
throw new InvalidCsrfTokenException();
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
|
||||
|
||||
if (!$user) {
|
||||
// fail authentication with a custom error
|
||||
throw new CustomUserMessageAuthenticationException('Email could not be found.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function checkCredentials($credentials, UserInterface $user)
|
||||
{
|
||||
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
|
||||
{
|
||||
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
|
||||
return new RedirectResponse($targetPath);
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('admin_dashboard_index'));
|
||||
}
|
||||
|
||||
protected function getLoginUrl()
|
||||
{
|
||||
return $this->urlGenerator->generate('auth_login');
|
||||
}
|
||||
}
|
||||
|
157
src/Controller/Account/AccountAdminController.php
Normal file
157
src/Controller/Account/AccountAdminController.php
Normal file
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Account;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface as TotpAuthenticatorInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use ZxcvbnPhp\Zxcvbn;
|
||||
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||
use App\Controller\Admin\AdminController;
|
||||
use App\Manager\EntityManager;
|
||||
|
||||
/**
|
||||
* @Route("/admin/account")
|
||||
*/
|
||||
class AccountAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="admin_account")
|
||||
*/
|
||||
public function account(Request $request, TotpAuthenticatorInterface $totpAuthenticatorService): Response
|
||||
{
|
||||
$account = $this->getUser();
|
||||
|
||||
return $this->render('account/admin/edit.html.twig', [
|
||||
'account' => $account,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/2fa", name="admin_account_2fa")
|
||||
*/
|
||||
public function twoFactorAuthentication(
|
||||
Request $request,
|
||||
GoogleAuthenticatorInterface $totpAuthenticatorService,
|
||||
EntityManager $entityManager
|
||||
): Response {
|
||||
if ($request->isMethod('GET')) {
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
$account = $this->getUser();
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
$enable = (bool) $request->request->get('enable');
|
||||
$code = $request->request->get('code', '');
|
||||
$secret = $request->request->get('secret', '');
|
||||
$qrCodeContent = null;
|
||||
|
||||
if ($this->isCsrfTokenValid('2fa', $csrfToken)) {
|
||||
if ($enable && !$account->isTotpAuthenticationEnabled()) {
|
||||
if (empty($secret)) {
|
||||
$secret = $totpAuthenticatorService->generateSecret();
|
||||
|
||||
$account->setTotpSecret($secret);
|
||||
|
||||
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
|
||||
} else {
|
||||
$account->setTotpSecret($secret);
|
||||
|
||||
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
|
||||
|
||||
if (!$totpAuthenticatorService->checkCode($account, $code)) {
|
||||
$this->addFlash('error', 'Le code n\'est pas valide.');
|
||||
} else {
|
||||
$this->addFlash('success', 'Double authentification activée.');
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$enable && $account->isTotpAuthenticationEnabled()) {
|
||||
$account->setTotpSecret(null);
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
|
||||
$this->addFlash('success', 'Double authentification désactivée.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
return $this->render('account/admin/edit.html.twig', [
|
||||
'account' => $account,
|
||||
'twoFaKey' => $secret,
|
||||
'twoFaQrCodeContent' => $qrCodeContent,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/password", name="admin_account_password", methods={"POST"})
|
||||
*/
|
||||
public function password(
|
||||
Request $request,
|
||||
UserRepository $repository,
|
||||
TokenGeneratorInterface $tokenGenerator,
|
||||
UserPasswordEncoderInterface $encoder
|
||||
): Response {
|
||||
$account = $this->getUser();
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
||||
if ($this->isCsrfTokenValid('password', $csrfToken)) {
|
||||
$password = $request->request->get('password');
|
||||
|
||||
if (!$encoder->isPasswordValid($account, $password)) {
|
||||
$this->addFlash('error', 'Le formulaire n\'est pas valide.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
$password1 = $request->request->get('password1');
|
||||
$password2 = $request->request->get('password2');
|
||||
|
||||
$zxcvbn = new Zxcvbn();
|
||||
$strength = $zxcvbn->passwordStrength($password1, []);
|
||||
|
||||
if (4 === $strength['score'] && $password1 === $password2) {
|
||||
$account
|
||||
->setPassword($encoder->encodePassword(
|
||||
$account,
|
||||
$password1
|
||||
))
|
||||
->setConfirmationToken($tokenGenerator->generateToken())
|
||||
;
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($account);
|
||||
$entityManager->flush();
|
||||
$entityManager->clear();
|
||||
|
||||
$this->addFlash('success', 'Mot de passe modifié !');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlash('error', 'Le formulaire n\'est pas valide.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSection(): string
|
||||
{
|
||||
return 'account';
|
||||
}
|
||||
}
|
21
src/Controller/Admin/AdminController.php
Normal file
21
src/Controller/Admin/AdminController.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class AdminController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function render(string $view, array $parameters = [], Response $response = null): Response
|
||||
{
|
||||
$parameters['section'] = $this->getSection();
|
||||
|
||||
return parent::render($view, $parameters, $response);
|
||||
}
|
||||
|
||||
abstract protected function getSection(): string;
|
||||
}
|
163
src/Controller/Auth/AuthController.php
Normal file
163
src/Controller/Auth/AuthController.php
Normal file
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Auth;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use App\Notification\MailNotifier;
|
||||
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||
use ZxcvbnPhp\Zxcvbn;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use App\Manager\EntityManager;
|
||||
use App\Event\Account\PasswordRequestEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class AuthController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/login", name="auth_login")
|
||||
*/
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('admin_dashboard_index');
|
||||
}
|
||||
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('auth/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/resetting/request", name="auth_resetting_request")
|
||||
*/
|
||||
public function requestResetting(
|
||||
Request $request,
|
||||
UserRepository $repository,
|
||||
TokenGeneratorInterface $tokenGenerator,
|
||||
EntityManager $entityManager,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('admin_dashboard_index');
|
||||
}
|
||||
|
||||
$emailSent = false;
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
||||
if ($this->isCsrfTokenValid('resetting_request', $csrfToken)) {
|
||||
$username = trim((string) $request->request->get('username'));
|
||||
|
||||
if ($username) {
|
||||
$account = $repository->findOneByEmail($username);
|
||||
|
||||
if ($account) {
|
||||
$passwordRequestedAt = $account->getPasswordRequestedAt();
|
||||
|
||||
if (null !== $passwordRequestedAt && $passwordRequestedAt->getTimestamp() > (time() - 3600 / 2)) {
|
||||
$emailSent = true;
|
||||
}
|
||||
|
||||
if (!$emailSent) {
|
||||
$account->setConfirmationToken($tokenGenerator->generateToken());
|
||||
$account->setPasswordRequestedAt(new \DateTime('now'));
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
$eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT);
|
||||
|
||||
$emailSent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('auth/resetting_request.html.twig', [
|
||||
'email_sent' => $emailSent,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/resetting/update/{token}", name="auth_resetting_update")
|
||||
*/
|
||||
public function requestUpdate(
|
||||
string $token,
|
||||
Request $request,
|
||||
UserRepository $repository,
|
||||
TokenGeneratorInterface $tokenGenerator,
|
||||
UserPasswordEncoderInterface $encoder,
|
||||
EntityManager $entityManager
|
||||
): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('index');
|
||||
}
|
||||
|
||||
$account = $repository->findOneByConfirmationToken($token);
|
||||
$passwordUpdated = false;
|
||||
$expired = false;
|
||||
|
||||
if ($account) {
|
||||
$passwordRequestedAt = $account->getPasswordRequestedAt();
|
||||
|
||||
if (null !== $passwordRequestedAt && $passwordRequestedAt->getTimestamp() < (time() - 3600 * 2)) {
|
||||
$expired = true;
|
||||
}
|
||||
} else {
|
||||
$expired = true;
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST') && !$expired) {
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
||||
if ($this->isCsrfTokenValid('resetting_update', $csrfToken)) {
|
||||
$password = $request->request->get('password');
|
||||
$password2 = $request->request->get('password2');
|
||||
|
||||
$zxcvbn = new Zxcvbn();
|
||||
$strength = $zxcvbn->passwordStrength($password, []);
|
||||
|
||||
if (4 === $strength['score'] && $password === $password2) {
|
||||
$account
|
||||
->setPassword($encoder->encodePassword(
|
||||
$account,
|
||||
$password
|
||||
))
|
||||
->setConfirmationToken($tokenGenerator->generateToken())
|
||||
->setPasswordRequestedAt(new \DateTime('now'))
|
||||
;
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
|
||||
$passwordUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('auth/resetting_update.html.twig', [
|
||||
'password_updated' => $passwordUpdated,
|
||||
'token' => $token,
|
||||
'expired' => $expired,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/logout", name="auth_logout")
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall');
|
||||
}
|
||||
}
|
27
src/Controller/Dashboard/DashboardAdminController.php
Normal file
27
src/Controller/Dashboard/DashboardAdminController.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use App\Controller\Admin\AdminController;
|
||||
|
||||
/**
|
||||
* @Route("/admin")
|
||||
*/
|
||||
class DashboardAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="admin_dashboard_index")
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('dashboard/admin/index.html.twig', [
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getSection(): string
|
||||
{
|
||||
return 'dashboard';
|
||||
}
|
||||
}
|
7
src/Entity/Entity.php
Normal file
7
src/Entity/Entity.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
interface Entity
|
||||
{
|
||||
}
|
244
src/Entity/User.php
Normal file
244
src/Entity/User.php
Normal file
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
|
||||
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=UserRepository::class)
|
||||
* @ORM\Table(name="`user`")
|
||||
*/
|
||||
class User implements UserInterface, TwoFactorInterface, Entity
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=180, unique=true)
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private $roles = [];
|
||||
|
||||
/**
|
||||
* @var string The hashed password
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
private $password;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $displayName;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $totpSecret;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
*/
|
||||
private $passwordRequestedAt;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $confirmationToken;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default"=0})
|
||||
*/
|
||||
private $isAdmin;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A visual identifier that represents this user.
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUsername(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
// guarantee every user at least has ROLE_USER
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
if ($this->getIsAdmin()) {
|
||||
$roles[] = 'ROLE_ADMIN';
|
||||
}
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return (string) $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returning a salt is only needed, if you are not using a modern
|
||||
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getSalt(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function eraseCredentials()
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
|
||||
public function getDisplayName(): ?string
|
||||
{
|
||||
return $this->displayName;
|
||||
}
|
||||
|
||||
public function setDisplayName(?string $displayName): self
|
||||
{
|
||||
$this->displayName = $displayName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotpSecret(): ?string
|
||||
{
|
||||
return $this->totpSecret;
|
||||
}
|
||||
|
||||
public function setTotpSecret(?string $totpSecret): self
|
||||
{
|
||||
$this->totpSecret = $totpSecret;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isTotpAuthenticationEnabled(): bool
|
||||
{
|
||||
return null !== $this->getTotpSecret();
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationUsername(): string
|
||||
{
|
||||
return $this->getEmail();
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface
|
||||
{
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
public function isGoogleAuthenticatorEnabled(): bool
|
||||
{
|
||||
return $this->isTotpAuthenticationEnabled();
|
||||
}
|
||||
|
||||
public function getGoogleAuthenticatorUsername(): string
|
||||
{
|
||||
return $this->getTotpAuthenticationUsername();
|
||||
}
|
||||
|
||||
public function getGoogleAuthenticatorSecret(): ?string
|
||||
{
|
||||
return $this->getTotpSecret();
|
||||
}
|
||||
|
||||
public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void
|
||||
{
|
||||
$this->setTotpSecret($googleAuthenticatorSecret);
|
||||
}
|
||||
|
||||
public function getPasswordRequestedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->passwordRequestedAt;
|
||||
}
|
||||
|
||||
public function setPasswordRequestedAt(?\DateTimeInterface $passwordRequestedAt): self
|
||||
{
|
||||
$this->passwordRequestedAt = $passwordRequestedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConfirmationToken(): ?string
|
||||
{
|
||||
return $this->confirmationToken;
|
||||
}
|
||||
|
||||
public function setConfirmationToken(?string $confirmationToken): self
|
||||
{
|
||||
$this->confirmationToken = $confirmationToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsAdmin(): ?bool
|
||||
{
|
||||
return $this->isAdmin;
|
||||
}
|
||||
|
||||
public function setIsAdmin(bool $isAdmin): self
|
||||
{
|
||||
$this->isAdmin = $isAdmin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
28
src/Event/Account/PasswordRequestEvent.php
Normal file
28
src/Event/Account/PasswordRequestEvent.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Event\Account;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* class PasswordRequestEvent.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class PasswordRequestEvent extends Event
|
||||
{
|
||||
const EVENT = 'account_event.password_request';
|
||||
|
||||
protected User $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): USer
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
30
src/Event/EntityManager/EntityManagerEvent.php
Normal file
30
src/Event/EntityManager/EntityManagerEvent.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Event\EntityManager;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use App\Entity\Entity;
|
||||
|
||||
/**
|
||||
* class EntityEvent.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class EntityManagerEvent extends Event
|
||||
{
|
||||
const CREATE_EVENT = 'entity_manager_event.create';
|
||||
const UPDATE_EVENT = 'entity_manager_event.update';
|
||||
const DELETE_EVENT = 'entity_manager_event.delete';
|
||||
|
||||
protected Entity $entity;
|
||||
|
||||
public function __construct(Entity $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEntity(): Entity
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
51
src/EventSuscriber/AccountPasswordRequestEventSubscriber.php
Normal file
51
src/EventSuscriber/AccountPasswordRequestEventSubscriber.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\EventSuscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use App\Event\EntityManager\EntityManagerEvent;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use App\Event\Account\PasswordRequestEvent;
|
||||
use App\Notification\MailNotifier;
|
||||
|
||||
/**
|
||||
* class EventListener.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class AccountPasswordRequestEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
protected MailNotifier $notifier;
|
||||
protected UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
public function __construct(MailNotifier $notifier, UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
$this->notifier = $notifier;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
PasswordRequestEvent::EVENT => 'onRequest',
|
||||
];
|
||||
}
|
||||
|
||||
public function onRequest(PasswordRequestEvent $event)
|
||||
{
|
||||
$this->notifier
|
||||
->setFrom('system@tinternet.net')
|
||||
->setSubject('[Tinternet & cie] Mot de passe perdu')
|
||||
->addRecipient($event->getUser()->getEmail())
|
||||
->notify('resetting_request', [
|
||||
'reseting_update_link' => $this->urlGenerator->generate(
|
||||
'auth_resetting_update',
|
||||
[
|
||||
'token' => $event->getUser()->getConfirmationToken(),
|
||||
],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
),
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
39
src/EventSuscriber/EntityManagerEventSubscriber.php
Normal file
39
src/EventSuscriber/EntityManagerEventSubscriber.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\EventSuscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use App\Event\EntityManager\EntityManagerEvent;
|
||||
|
||||
/**
|
||||
* class EventListener.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class EntityManagerEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
EntityManagerEvent::CREATE_EVENT => 'onCreate',
|
||||
EntityManagerEvent::UPDATE_EVENT => 'onUpdate',
|
||||
EntityManagerEvent::DELETE_EVENT => 'onDelete',
|
||||
];
|
||||
}
|
||||
|
||||
public function onCreate(EntityManagerEvent $event)
|
||||
{
|
||||
}
|
||||
|
||||
public function onUpdate(EntityManagerEvent $event)
|
||||
{
|
||||
}
|
||||
|
||||
public function onDelete(EntityManagerEvent $event)
|
||||
{
|
||||
}
|
||||
}
|
71
src/Manager/EntityManager.php
Normal file
71
src/Manager/EntityManager.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Manager;
|
||||
|
||||
use App\Entity\Entity;
|
||||
use App\Event\EntityManager\EntityManagerEvent;
|
||||
use Doctrine\ORM\EntityManager as DoctrineEntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* class EntityManager.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class EntityManager
|
||||
{
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected DoctrineEntityManager $entityManager;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function create(Entity $entity): self
|
||||
{
|
||||
$this->persist($entity);
|
||||
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::CREATE_EVENT);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function update(Entity $entity): self
|
||||
{
|
||||
$this->persist($entity);
|
||||
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::UPDATE_EVENT);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function delete(Entity $entity): self
|
||||
{
|
||||
$this->remove($entity);
|
||||
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function flush(): self
|
||||
{
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function clear(): self
|
||||
{
|
||||
$this->entityManager->clear();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function persist(Entity $entity)
|
||||
{
|
||||
$this->entityManager->persist($entity);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
338
src/Notification/MailNotifier.php
Normal file
338
src/Notification/MailNotifier.php
Normal file
|
@ -0,0 +1,338 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
use Swift_Attachment;
|
||||
use Swift_Mailer;
|
||||
use Swift_Message;
|
||||
use Twig\Environment as TwigEnvironment;
|
||||
|
||||
/**
|
||||
* class MailNotifier.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class MailNotifier
|
||||
{
|
||||
/**
|
||||
* @var Swift_Mailer
|
||||
*/
|
||||
protected $mailer;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $attachments = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $recipients = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $bccRecipients = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $subject;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $from;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $replyTo;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param BasicNotifier $basicNotifier
|
||||
* @param Swift_Mailer $mail
|
||||
*/
|
||||
public function __construct(TwigEnvironment $twig, Swift_Mailer $mailer)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setMailer(Swift_Mailer $mailer): self
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMailer(): Swift_Mailer
|
||||
{
|
||||
return $this->mailer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setRecipients(array $recipients): self
|
||||
{
|
||||
$this->recipients = $recipients;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return $this->recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setBccRecipients(array $bccRecipients): self
|
||||
{
|
||||
$this->bccRecipients = $bccRecipients;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBccRecipients(): array
|
||||
{
|
||||
return $this->bccRecipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $subject
|
||||
*
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setSubject(?string $subject): self
|
||||
{
|
||||
$this->subject = $subject;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $from
|
||||
*
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setFrom($from): self
|
||||
{
|
||||
$this->from = $from;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFrom(): ?string
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of "replyTo".
|
||||
*
|
||||
* @param string $replyTo
|
||||
*
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setReplyTo($replyTo): self
|
||||
{
|
||||
$this->replyTo = $replyTo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the value of "replyTo".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReplyTo(): ?string
|
||||
{
|
||||
return $this->replyTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function setAttachments(array $attachments): self
|
||||
{
|
||||
$this->attachments = $attachments;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAttachments(): array
|
||||
{
|
||||
return $this->attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function addRecipient(string $email, bool $isBcc = false): self
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid email "%s".', $email));
|
||||
}
|
||||
|
||||
if ($isBcc) {
|
||||
if (!in_array($email, $this->bccRecipients)) {
|
||||
$this->bccRecipients[] = $email;
|
||||
}
|
||||
} else {
|
||||
if (!in_array($email, $this->recipients)) {
|
||||
$this->recipients[] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function addRecipients(array $emails, bool $isBcc = false): self
|
||||
{
|
||||
foreach ($emails as $email) {
|
||||
$this->addRecipient($email, $isBcc);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function addRecipientByAccount(Account $account, bool $isBcc = false): self
|
||||
{
|
||||
return $this->addRecipient($account->getEmail(), $isBcc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $accounts
|
||||
*
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function addRecipientsByAccounts($accounts, bool $isBcc = false)
|
||||
{
|
||||
if (!is_array($accounts)) {
|
||||
throw new InvalidArgumentException('The "accounts" parameter must be an array or an instance of ObjectCollection');
|
||||
}
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$this->addRecipientByAccount($account, $isBcc);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function addAttachment(string $attachment): self
|
||||
{
|
||||
if (!in_array($attachment, $this->attachments)) {
|
||||
$this->attachments[] = $attachment;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function addAttachments(array $attachments): self
|
||||
{
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->addAttachment($attachment);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function init(): self
|
||||
{
|
||||
$this
|
||||
->setSubject(null)
|
||||
->setRecipients([])
|
||||
->setBccRecipients([])
|
||||
->setAttachments([])
|
||||
;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailNotifier
|
||||
*/
|
||||
public function notify(string $template, array $data = [], string $type = 'text/html'): self
|
||||
{
|
||||
$message = $this->createMessage(
|
||||
$this->twig->render(
|
||||
sprintf('mail/%s.html.twig', $template),
|
||||
$data
|
||||
),
|
||||
$type
|
||||
);
|
||||
|
||||
$this->mailer->send($message);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function createMessage(string $body, string $type = 'text/html'): Swift_Message
|
||||
{
|
||||
$message = new Swift_Message();
|
||||
|
||||
if ($this->getSubject()) {
|
||||
$message->setSubject($this->getSubject());
|
||||
}
|
||||
|
||||
if ($this->getFrom()) {
|
||||
$message->setFrom($this->getFrom());
|
||||
}
|
||||
|
||||
if ($this->getReplyTo()) {
|
||||
$message->setReplyTo($this->getReplyTo());
|
||||
}
|
||||
|
||||
if (count($this->getRecipients()) > 0) {
|
||||
$message->setTo($this->getRecipients());
|
||||
}
|
||||
|
||||
if (count($this->getBccRecipients()) > 0) {
|
||||
$message->setBcc($this->getBccRecipients());
|
||||
}
|
||||
|
||||
foreach ($this->getAttachments() as $attachment) {
|
||||
if (is_object($attachment) && $attachment instanceof Swift_Attachment) {
|
||||
$message->attach($attachment);
|
||||
} elseif (is_string($attachment) && file_exists($attachment) && is_readable($attachment) && !is_dir($attachment)) {
|
||||
$message->attach(Swift_Attachment::fromPath($attachment));
|
||||
}
|
||||
}
|
||||
|
||||
$message->setBody($body, $type);
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
67
src/Repository/UserRepository.php
Normal file
67
src/Repository/UserRepository.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?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\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to upgrade (rehash) the user's password automatically over time.
|
||||
*/
|
||||
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
|
||||
}
|
||||
|
||||
$user->setPassword($newEncodedPassword);
|
||||
$this->_em->persist($user);
|
||||
$this->_em->flush();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return User[] Returns an array of User objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->andWhere('u.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('u.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?User
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->andWhere('u.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
13
src/Security/TokenGenerator.php
Normal file
13
src/Security/TokenGenerator.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||
|
||||
class TokenGenerator implements TokenGeneratorInterface
|
||||
{
|
||||
public function generateToken(): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
109
symfony.lock
109
symfony.lock
|
@ -1,7 +1,22 @@
|
|||
{
|
||||
"bacon/bacon-qr-code": {
|
||||
"version": "2.0.3"
|
||||
},
|
||||
"beberlei/assert": {
|
||||
"version": "v3.3.0"
|
||||
},
|
||||
"bjeavons/zxcvbn-php": {
|
||||
"version": "1.2.0"
|
||||
},
|
||||
"cocur/slugify": {
|
||||
"version": "v4.0.0"
|
||||
},
|
||||
"composer/package-versions-deprecated": {
|
||||
"version": "1.11.99.1"
|
||||
},
|
||||
"dasprid/enum": {
|
||||
"version": "1.0.3"
|
||||
},
|
||||
"doctrine/annotations": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
|
@ -81,9 +96,21 @@
|
|||
"egulias/email-validator": {
|
||||
"version": "3.1.0"
|
||||
},
|
||||
"endroid/qr-code": {
|
||||
"version": "3.9.6"
|
||||
},
|
||||
"friendsofphp/proxy-manager-lts": {
|
||||
"version": "v1.0.3"
|
||||
},
|
||||
"khanamiryan/qrcode-detector-decoder": {
|
||||
"version": "1.0.4"
|
||||
},
|
||||
"knplabs/knp-components": {
|
||||
"version": "v3.0.1"
|
||||
},
|
||||
"knplabs/knp-paginator-bundle": {
|
||||
"version": "v5.4.2"
|
||||
},
|
||||
"laminas/laminas-code": {
|
||||
"version": "4.0.0"
|
||||
},
|
||||
|
@ -96,9 +123,15 @@
|
|||
"monolog/monolog": {
|
||||
"version": "2.2.0"
|
||||
},
|
||||
"myclabs/php-enum": {
|
||||
"version": "1.8.0"
|
||||
},
|
||||
"nikic/php-parser": {
|
||||
"version": "v4.10.4"
|
||||
},
|
||||
"paragonie/constant_time_encoding": {
|
||||
"version": "v2.4.0"
|
||||
},
|
||||
"phpdocumentor/reflection-common": {
|
||||
"version": "2.2.0"
|
||||
},
|
||||
|
@ -123,6 +156,25 @@
|
|||
"psr/log": {
|
||||
"version": "1.1.3"
|
||||
},
|
||||
"scheb/2fa-bundle": {
|
||||
"version": "5.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "master",
|
||||
"version": "5.0",
|
||||
"ref": "95d9da4bffdc29417c209141cae8292e40d6af19"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/scheb_2fa.yaml",
|
||||
"config/routes/scheb_2fa.yaml"
|
||||
]
|
||||
},
|
||||
"scheb/2fa-google-authenticator": {
|
||||
"version": "v5.7.0"
|
||||
},
|
||||
"scheb/2fa-qr-code": {
|
||||
"version": "v5.7.0"
|
||||
},
|
||||
"sensio/framework-extra-bundle": {
|
||||
"version": "5.2",
|
||||
"recipe": {
|
||||
|
@ -135,6 +187,24 @@
|
|||
"config/packages/sensio_framework_extra.yaml"
|
||||
]
|
||||
},
|
||||
"spomky-labs/otphp": {
|
||||
"version": "v10.0.1"
|
||||
},
|
||||
"swiftmailer/swiftmailer": {
|
||||
"version": "v6.2.7"
|
||||
},
|
||||
"symfony/apache-pack": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "master",
|
||||
"version": "1.0",
|
||||
"ref": "71599f5b0fdeeeec0fb90e9b17c85e6f5e1350c1"
|
||||
},
|
||||
"files": [
|
||||
"public/.htaccess"
|
||||
]
|
||||
},
|
||||
"symfony/asset": {
|
||||
"version": "v5.2.4"
|
||||
},
|
||||
|
@ -427,6 +497,20 @@
|
|||
"symfony/string": {
|
||||
"version": "v5.2.4"
|
||||
},
|
||||
"symfony/swiftmailer-bundle": {
|
||||
"version": "2.5",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "2.5",
|
||||
"ref": "ae4d22af30bbd484506bc1817c5a3ef72c855b93"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/dev/swiftmailer.yaml",
|
||||
"config/packages/swiftmailer.yaml",
|
||||
"config/packages/test/swiftmailer.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/test-pack": {
|
||||
"version": "v1.0.7"
|
||||
},
|
||||
|
@ -502,9 +586,34 @@
|
|||
"config/routes/dev/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/webpack-encore-bundle": {
|
||||
"version": "1.9",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.9",
|
||||
"ref": "9399a0bfc6ee7a0c019f952bca46d6a6045dd451"
|
||||
},
|
||||
"files": [
|
||||
"assets/app.js",
|
||||
"assets/bootstrap.js",
|
||||
"assets/controllers.json",
|
||||
"assets/controllers/hello_controller.js",
|
||||
"assets/styles/app.css",
|
||||
"config/packages/assets.yaml",
|
||||
"config/packages/prod/webpack_encore.yaml",
|
||||
"config/packages/test/webpack_encore.yaml",
|
||||
"config/packages/webpack_encore.yaml",
|
||||
"package.json",
|
||||
"webpack.config.js"
|
||||
]
|
||||
},
|
||||
"symfony/yaml": {
|
||||
"version": "v5.2.5"
|
||||
},
|
||||
"thecodingmachine/safe": {
|
||||
"version": "v1.3.3"
|
||||
},
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.3.0"
|
||||
},
|
||||
|
|
226
templates/account/admin/edit.html.twig
Normal file
226
templates/account/admin/edit.html.twig
Normal file
|
@ -0,0 +1,226 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% import _self as macros %}
|
||||
|
||||
{% block title %}Mon compte - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<h1 class="h2 p-4 mr-auto">Mon compte</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<form action="{{ path('admin_account_password') }}" method="post">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div class="tab-form">
|
||||
<h4>Changer mon mot de passe</h4>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="alert alert-info">
|
||||
L'indicateur doit afficher 5 traits verts pour que
|
||||
le mot de passe soit accepté.<br>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Mot de passe actuel</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input autocomplete="current-password" type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Nouveau mot de passe</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input autocomplete="new-password" type="password" name="password1" class="form-control" id="form-password-new" required>
|
||||
</div>
|
||||
|
||||
<div class="form-text text-muted">
|
||||
<small> L'indicateur doit afficher 5 traits verts pour que
|
||||
le mot de passe soit accepté.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row password-strenth" id="form-password-strength">
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-12 password-strenth-info"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Confirmation du mot de passe</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input autocomplete="off" type="password" name="password2" class="form-control" id="form-password-confirmation" required>
|
||||
</div>
|
||||
|
||||
<div class="form-text text-muted">
|
||||
<small>Les 2 mots de passe doivent correspondre.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" disabled id="form-password-submit" value="Mettre à jour">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('password') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<form action="{{ path('admin_account_2fa') }}" method="post">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div class="tab-form">
|
||||
<h4>Double authentification</h4>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
La double authentification ou vérification en deux étapes
|
||||
est une méthode par laquelle un utilisateur peut accéder à un site web
|
||||
après avoir présenté deux preuves d'identité distinctes.<br>
|
||||
|
||||
En activant la double authentification, vous devrez saisir un code généré depuis votre téléphone
|
||||
en plus de votre identifiant et votre mot de passe.
|
||||
</p>
|
||||
|
||||
{% if app.request.isMethod('GET') %}
|
||||
{% if account.isTotpAuthenticationEnabled %}
|
||||
<p class="text-success">
|
||||
<strong>Votre compte est sécurisé par une double authentification.</strong>
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="enable" value="0">
|
||||
<input type="submit" class="btn btn-primary" value="Désactiver">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="enable" value="1">
|
||||
<input type="submit" class="btn btn-primary" value="Activer">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h4>Étape 1</h4>
|
||||
|
||||
<p>
|
||||
Télécharger votre application TOTP :
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=fr" target="_blank">
|
||||
Application pour Android :
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://apps.apple.com/in/app/authy/id494168017" target="_blank">
|
||||
Application pour IOS
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Étape 2</h4>
|
||||
|
||||
{% set twoFaQrCodeContent = twoFaQrCodeContent|replace({
|
||||
'%40': '@',
|
||||
'%3A': ':',
|
||||
}) ~ '&algorithm=SHA1&digits=6&period=30' %}
|
||||
|
||||
<p>
|
||||
Scannez ce QRCode pour enregistrer Tinternet & cie :
|
||||
|
||||
<div id="qrcode"></div>
|
||||
</p>
|
||||
|
||||
<script src="{{ asset('vendor/qrcodejs/qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
new QRCode(
|
||||
document.getElementById("qrcode"),
|
||||
"{{ twoFaQrCodeContent|raw }}"
|
||||
);
|
||||
</script>
|
||||
|
||||
<h4>Étape 3</h4>
|
||||
|
||||
<p>
|
||||
Générez et saisissez un code d'authentification :
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Code de confirmation</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
name="code"
|
||||
maxlength="6"
|
||||
pattern="[0-9]*"
|
||||
inputmode="numeric"
|
||||
class="form-control"
|
||||
style="max-width: 100px"
|
||||
required
|
||||
>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="secret"
|
||||
value="{{ twoFaKey }}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
Si vous activez la double authentification, vous ne pourrez pas vous connecter sans votre téléphone.
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="enable" value="1">
|
||||
<input type="submit" class="btn btn-primary" value="Envoyer">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('2fa') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
113
templates/admin/layout.html.twig
Normal file
113
templates/admin/layout.html.twig
Normal file
|
@ -0,0 +1,113 @@
|
|||
{% apply spaceless %}
|
||||
<!DOCTYPE html>
|
||||
{% import _self as macros %}
|
||||
|
||||
{% macro active_class(expectedSection, section) %}{{ expectedSection == section ? 'active' : ''}}{% endmacro %}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
{{ include('admin/module/metas.html.twig') }}
|
||||
|
||||
<title>{% block title %}Tinternet & cie{% endblock %}</title>
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{{ include('admin/module/flashes.html.twig') }}
|
||||
|
||||
<nav class="navbar fixed-top navbar-expand-md navbar-light bg-white border-bottom">
|
||||
<a class="navbar-brand" href="{{ path('admin_dashboard_index') }}">
|
||||
<img id="logo" src="{{ asset('build/images/logo.png') }}" alt="Tinternet & cie" title="Tinternet & cie">
|
||||
|
||||
Tinternet & cie
|
||||
</a>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="collapse navbar-collapse" id="navigation">
|
||||
{{ include('admin/module/account.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navigation" aria-controls="navigation" aria-expanded="false" aria-label="Afficher navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{% block body_container %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-2 col-1 d-md-block bg-dark-blue sidebar">
|
||||
<div class="sidebar-sticky">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('dashboard', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<span class="fa fa-chart-line"></span>
|
||||
|
||||
<span class="nav-item-label">Tableau de bord</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Contenu</span>
|
||||
</h6>
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('page', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<span class="fa fa-file-alt"></span>
|
||||
|
||||
<span class="nav-item-label">Pages</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('blog_post', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<span class="fa fa-pen"></span>
|
||||
|
||||
<span class="nav-item-label">Articles</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('blog_post', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<span class="fa fa-puzzle-piece"></span>
|
||||
|
||||
<span class="nav-item-label">Catégories</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Administration</span>
|
||||
</h6>
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('user', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<span class="fa fa-user"></span>
|
||||
|
||||
<span class="nav-item-label">Utilisateurs</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="col-11 col-md-10 ml-sm-auto col-lg-10 body">
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
|
||||
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
16
templates/admin/module/account.html.twig
Normal file
16
templates/admin/module/account.html.twig
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-light dropdown-toggle dropdown-toggle-hide-after" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="fa fa-cog"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-lg-right">
|
||||
<a href="{{ path('admin_account') }}" class="dropdown-item">
|
||||
Mon compte
|
||||
</a>
|
||||
|
||||
<a href="{{ path('auth_logout') }}" class="dropdown-item">
|
||||
Déconnexion
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
51
templates/admin/module/flashes.html.twig
Normal file
51
templates/admin/module/flashes.html.twig
Normal file
|
@ -0,0 +1,51 @@
|
|||
{% set flashes = app.flashes %}
|
||||
|
||||
{% if flashes|length %}
|
||||
{% set colors = {
|
||||
'info': 'text-body',
|
||||
'notice': 'text-body',
|
||||
'success': 'text-success font-weight-bold',
|
||||
'warning': 'text-warning font-weight-bold',
|
||||
'danger': 'text-danger font-weight-bold',
|
||||
'error': 'text-danger font-weight-bold',
|
||||
} %}
|
||||
|
||||
{% set titles = {
|
||||
'notice': 'Information',
|
||||
'info': 'Information',
|
||||
'success': 'Succès',
|
||||
'warning': 'Attention',
|
||||
'danger': 'Danger',
|
||||
'error': 'Erreur',
|
||||
} %}
|
||||
|
||||
{% set borders = {
|
||||
'notice': '',
|
||||
'info': 'border border-primary',
|
||||
'success': 'border border-success',
|
||||
'warning': 'border border-warning',
|
||||
'danger': 'border border-danger',
|
||||
'error': 'border border-danger',
|
||||
} %}
|
||||
|
||||
<div aria-live="polite" aria-atomic="true" class="toast-container">
|
||||
<div class="toast-wrapper">
|
||||
{% for label, messages in flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="toast {{ borders[label] }}" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="mr-auto">{{ titles[label] }}</strong>
|
||||
<small>{{ 'now'|date('H:i') }}</small>
|
||||
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toast-body text-{{ colors[label] }}">
|
||||
{{ message|nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
14
templates/admin/module/metas.html.twig
Normal file
14
templates/admin/module/metas.html.twig
Normal file
|
@ -0,0 +1,14 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="{{ asset('favicon.png') }} " type="image/png">
|
||||
<link rel="icon" sizes="32x32" href="{{ asset('favicon-32.png') }}" type="image/png">
|
||||
<link rel="icon" sizes="64x64" href="{{ asset('favicon-64.png') }}" type="image/png">
|
||||
<link rel="icon" sizes="96x96" href="{{ asset('favicon-96.png') }}" type="image/png">
|
||||
<link rel="icon" sizes="196x196" href="{{ asset('favicon-196.png') }}" type="image/png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{ asset('apple-touch-icon.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ asset('apple-touch-icon-60x60.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ asset('apple-touch-icon-76x76.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{ asset('apple-touch-icon-114x114.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{ asset('apple-touch-icon-120x120.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{ asset('apple-touch-icon-144x144.png') }}">
|
83
templates/auth/2fa.html.twig
Normal file
83
templates/auth/2fa.html.twig
Normal file
|
@ -0,0 +1,83 @@
|
|||
{% apply spaceless %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{{ include('admin/module/metas.html.twig') }}
|
||||
|
||||
<title>{% block title %}Tinternet & cie{% endblock %}</title>
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container login-container">
|
||||
<div class="row shadow rounded">
|
||||
<div class="col-md-6">
|
||||
<div class="login-form">
|
||||
<div class="h3 pb-4">
|
||||
<span class="text-success">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
|
||||
Accès sécurisé
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ path("2fa_login_check") }}">
|
||||
<p>
|
||||
Cet accès est sécurisé par une double authentification.
|
||||
Munissez-vous de votre téléphone et générez un code de vérification
|
||||
pour cette application.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Code de vérification</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="{{ authCodeParameterName }}"
|
||||
class="form-control"
|
||||
autocomplete="one-time-code"
|
||||
pattern="[0-9]*"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
style="max-width: 100px"
|
||||
autofocus
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if isCsrfProtectionEnabled %}
|
||||
<input type="hidden" name="{{ csrfParameterName }}" value="{{ csrf_token(csrfTokenId) }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Connexion">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 bg-light d-flex">
|
||||
<div class="flex-fill align-self-center text-center p-3">
|
||||
<img class="login-image" src="{{ asset('build/images/logo.png') }}" alt="Tinternet & cie" title="Tinternet & cie">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block js %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
|
||||
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
95
templates/auth/login.html.twig
Normal file
95
templates/auth/login.html.twig
Normal file
|
@ -0,0 +1,95 @@
|
|||
{% apply spaceless %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{{ include('admin/module/metas.html.twig') }}
|
||||
|
||||
<title>{% block title %}Tinternet & cie{% endblock %}</title>
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container login-container">
|
||||
<div class="row shadow rounded">
|
||||
<div class="col-md-6">
|
||||
<div class="login-form">
|
||||
<div class="h3 pb-4">
|
||||
<span class="text-success">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
|
||||
Accès sécurisé
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
L'identifiant ou le mot de passe est incorrect.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ path("auth_login") }}">
|
||||
<div class="form-group">
|
||||
<label for="form-id">Identifiant</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-address-book"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="_username" value="{{ last_username }}" class="form-control" id="form-id" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Mot de passe</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="password" name="_password" class="form-control" id="form-password" required>
|
||||
</div>
|
||||
|
||||
<p><a href="{{ path('auth_resetting_request') }}">Mot de passe perdu</a></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" name="_remember_me" type="checkbox" id="form-remember">
|
||||
<label class="form-check-label" for="form-remember">
|
||||
Se souvenir de moi
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Connexion">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 bg-light d-flex">
|
||||
<div class="flex-fill align-self-center text-center p-3">
|
||||
<img class="login-image" src="{{ asset('build/images/logo.png') }}" alt="Tinternet & cie" title="Tinternet & cie">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block js %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
|
||||
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
80
templates/auth/resetting_request.html.twig
Normal file
80
templates/auth/resetting_request.html.twig
Normal file
|
@ -0,0 +1,80 @@
|
|||
{% apply spaceless %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{{ include('admin/module/metas.html.twig') }}
|
||||
|
||||
<title>{% block title %}Tinternet & cie{% endblock %}</title>
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container login-container">
|
||||
<div class="row shadow rounded">
|
||||
<div class="col-md-6">
|
||||
<div class="login-form">
|
||||
<div class="h3 pb-4">
|
||||
<span class="text-success">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
|
||||
Mot de passe perdu
|
||||
</div>
|
||||
|
||||
{% if email_sent %}
|
||||
<div class="alert alert-info">
|
||||
Si les informations soumises correspondent à un compte utilisateur,
|
||||
vous allez recevoir un e-mail avec en lien pour enclancher la
|
||||
procédure de changement de mot de passe.<br>
|
||||
</div>
|
||||
{% else %}
|
||||
<form method="post" action="{{ path("auth_resetting_request") }}">
|
||||
<p>
|
||||
Saisissez le nom d'utilisateur ou l'adresse e-mail
|
||||
de votre compte. Un e-mail vous sera envoyé pour
|
||||
enclancher la procédure de changement de mot de passe.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-id">Identifiant ou e-mail</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-at"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="username" class="form-control" id="form-id" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Soumettre">
|
||||
|
||||
<a class="btn btn-light" href="{{ path('auth_login') }}">Afficher la page de connexion</a></p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('resetting_request') }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 bg-light d-flex">
|
||||
<div class="flex-fill align-self-center text-center p-3">
|
||||
<img class="login-image" src="{{ asset('build/images/logo.png') }}" alt="Tinternet & cie" title="Tinternet & cie">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block js %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
|
||||
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
113
templates/auth/resetting_update.html.twig
Normal file
113
templates/auth/resetting_update.html.twig
Normal file
|
@ -0,0 +1,113 @@
|
|||
{% apply spaceless %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{{ include('admin/module/metas.html.twig') }}
|
||||
|
||||
<title>{% block title %}Tinternet & cie{% endblock %}</title>
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container login-container">
|
||||
<div class="row shadow rounded">
|
||||
<div class="col-md-6">
|
||||
<div class="login-form">
|
||||
<div class="h3 pb-4">
|
||||
<span class="text-success">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
|
||||
Nouveau mot de passe
|
||||
</div>
|
||||
|
||||
{% if expired %}
|
||||
<div class="alert alert-warning">
|
||||
Ce lien a expiré. Merci de faire une nouvelle demande sur la page
|
||||
<a href="{{ path('auth_resetting_request') }}">Mot de passe perdu</a>
|
||||
</div>
|
||||
{% elseif password_updated %}
|
||||
<div class="alert alert-info">
|
||||
Le mot de passe a été mis à jour.
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary" href="{{ path('auth_login') }}">Afficher la page de connexion</a></p>
|
||||
</p>
|
||||
{% else %}
|
||||
<form method="post" id="form-password" action="{{ path("auth_resetting_update", {token: token}) }}">
|
||||
<p>
|
||||
Veuillez saisir un nouveau mot de passe.<br>
|
||||
L'indicateur doit afficher 5 traits verts pour que
|
||||
le mot de passe soit accepté. Les 2 mots de passe
|
||||
doivent correspondre.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Nouveau mot de passe</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password" class="form-control" id="form-password-new" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row password-strenth" id="form-password-strength">
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-sm bg-light"></div>
|
||||
<div class="col-12 password-strenth-info"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-password">Confirmation du mot de passe</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-key"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password2" class="form-control" id="form-password-confirmation" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" disabled id="form-password-submit" value="Soumettre">
|
||||
|
||||
<a class="btn btn-light" href="{{ path('auth_login') }}">Afficher la page de connexion</a></p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('resetting_update') }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 bg-light d-flex">
|
||||
<div class="flex-fill align-self-center text-center p-3">
|
||||
<img class="login-image" src="{{ asset('build/images/logo.png') }}" alt="Tinternet & cie" title="Tinternet & cie">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block js %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
|
||||
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% endapply %}
|
1
templates/dashboard/admin/index.html.twig
Normal file
1
templates/dashboard/admin/index.html.twig
Normal file
|
@ -0,0 +1 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
48
templates/mail/base.html.twig
Normal file
48
templates/mail/base.html.twig
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% block css %}
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 20px 10px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
#logo svg {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3183aa;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="content">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
1
templates/mail/raw.html.twig
Normal file
1
templates/mail/raw.html.twig
Normal file
|
@ -0,0 +1 @@
|
|||
{{ content|raw }}
|
13
templates/mail/resetting_request.html.twig
Normal file
13
templates/mail/resetting_request.html.twig
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'mail/base.html.twig' %}
|
||||
|
||||
{% set message %}
|
||||
Une demande de réinitialisation de mot de passe a été réalisée sur Tinternet & cie.
|
||||
|
||||
Si vous êtes à l'origine de cette demande, cliquer sur le lien ci-dessous ou copier/coller l'adresse si le lien ne fonctionne pas.
|
||||
|
||||
<a href="{{ reseting_update_link }}">{{ reseting_update_link }}</a>
|
||||
{% endset %}
|
||||
|
||||
{% block body %}
|
||||
<p>{{ message|nl2br }}</p>
|
||||
{% endblock %}
|
72
webpack.config.js
Normal file
72
webpack.config.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
const Encore = require('@symfony/webpack-encore');
|
||||
|
||||
// Manually configure the runtime environment if not already configured yet by the "encore" command.
|
||||
// It's useful when you use tools that rely on webpack.config.js file.
|
||||
if (!Encore.isRuntimeEnvironmentConfigured()) {
|
||||
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
|
||||
}
|
||||
|
||||
Encore
|
||||
// directory where compiled assets will be stored
|
||||
.setOutputPath('public/build/')
|
||||
// public path used by the web server to access the output path
|
||||
.setPublicPath('/build')
|
||||
// only needed for CDN's or sub-directory deploy
|
||||
//.setManifestKeyPrefix('build/')
|
||||
|
||||
/*
|
||||
* ENTRY CONFIG
|
||||
*
|
||||
* Each entry will result in one JavaScript file (e.g. app.js)
|
||||
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
|
||||
*/
|
||||
.addEntry('admin', './assets/js/admin.js')
|
||||
|
||||
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
|
||||
.splitEntryChunks()
|
||||
|
||||
// will require an extra script tag for runtime.js
|
||||
// but, you probably want this, unless you're building a single-page app
|
||||
.enableSingleRuntimeChunk()
|
||||
|
||||
/*
|
||||
* FEATURE CONFIG
|
||||
*
|
||||
* Enable & configure other features below. For a full
|
||||
* list of features, see:
|
||||
* https://symfony.com/doc/current/frontend.html#adding-more-features
|
||||
*/
|
||||
.cleanupOutputBeforeBuild()
|
||||
.enableBuildNotifications()
|
||||
.enableSourceMaps(!Encore.isProduction())
|
||||
// enables hashed filenames (e.g. app.abc123.css)
|
||||
.enableVersioning(Encore.isProduction())
|
||||
|
||||
.configureBabel((config) => {
|
||||
config.plugins.push('@babel/plugin-proposal-class-properties');
|
||||
})
|
||||
|
||||
// enables @babel/preset-env polyfills
|
||||
.configureBabelPresetEnv((config) => {
|
||||
config.useBuiltIns = 'usage';
|
||||
config.corejs = 3;
|
||||
})
|
||||
|
||||
// enables Sass/SCSS support
|
||||
.enableSassLoader()
|
||||
|
||||
// uncomment if you use TypeScript
|
||||
//.enableTypeScriptLoader()
|
||||
|
||||
// uncomment if you use React
|
||||
//.enableReactPreset()
|
||||
|
||||
// uncomment to get integrity="..." attributes on your script & link tags
|
||||
// requires WebpackEncoreBundle 1.4 or higher
|
||||
//.enableIntegrityHashes(Encore.isProduction())
|
||||
|
||||
// uncomment if you're having problems with a jQuery plugin
|
||||
//.autoProvidejQuery()
|
||||
;
|
||||
|
||||
module.exports = Encore.getWebpackConfig();
|
Loading…
Reference in a new issue