This commit is contained in:
Simon Vieille 2021-03-16 10:37:12 +01:00
parent 0939ed4d9e
commit cc837cf9e5
72 changed files with 10005 additions and 17 deletions

11
.env
View File

@ -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
View File

@ -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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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);
})
})
};

View 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);
});
}

View 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'
})
}
}

View File

@ -0,0 +1,7 @@
const $ = require('jquery');
module.exports = function() {
$('*[data-dblclick]').dblclick(function(e) {
document.location.href = $(this).attr('data-dblclick');
})
};

View 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;

View 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",
});
};

View 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;

View 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
View 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
View 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
View 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;

View 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);
});
}
};

View 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);
}

View 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) {
});
})
};

View 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;

View 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
View File

@ -0,0 +1,11 @@
const $ = require('jquery');
module.exports = function() {
$('.toast').toast({
animation: true,
autohide: true,
delay: 5000,
});
$('.toast').toast('show');
};

View File

@ -0,0 +1,5 @@
const $ = require('jquery');
module.exports = function() {
$('*[data-toggle="tooltip"]').tooltip();
};

28
assets/js/admin.js Normal file
View 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
View 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

View File

@ -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"

View File

@ -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],
];

View File

@ -0,0 +1,3 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

View 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']

View 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

View 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

View File

@ -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 }

View File

@ -0,0 +1,3 @@
swiftmailer:
url: '%env(MAILER_URL)%'
spool: { type: 'memory' }

View File

@ -0,0 +1,2 @@
swiftmailer:
disable_delivery: true

View File

@ -0,0 +1,2 @@
#webpack_encore:
# strict_mode: false

View 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

View File

@ -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

View 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
View 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
View 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
View File

@ -0,0 +1 @@
../../node_modules/qrcodejs

View 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');
}
}

View 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';
}
}

View 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;
}

View 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');
}
}

View 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
View File

@ -0,0 +1,7 @@
<?php
namespace App\Entity;
interface Entity
{
}

244
src/Entity/User.php Normal file
View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
),
])
;
}
}

View 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)
{
}
}

View 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();
}
}

View 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;
}
}

View 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()
;
}
*/
}

View 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)), '+/', '-_'), '=');
}
}

View File

@ -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"
},

View 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&amp;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 &amp; 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 %}

View 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 &amp; 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 &amp; cie" title="Tinternet &amp; cie">
Tinternet &amp; 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 %}

View 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>

View 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">&times;</span>
</button>
</div>
<div class="toast-body text-{{ colors[label] }}">
{{ message|nl2br }}
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
{% endif %}

View 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') }}">

View File

@ -0,0 +1,83 @@
{% apply spaceless %}
<!DOCTYPE html>
<html>
<head>
{{ include('admin/module/metas.html.twig') }}
<title>{% block title %}Tinternet &amp; 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 &amp; cie" title="Tinternet &amp; 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 %}

View File

@ -0,0 +1,95 @@
{% apply spaceless %}
<!DOCTYPE html>
<html>
<head>
{{ include('admin/module/metas.html.twig') }}
<title>{% block title %}Tinternet &amp; 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 &amp; cie" title="Tinternet &amp; 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 %}

View File

@ -0,0 +1,80 @@
{% apply spaceless %}
<!DOCTYPE html>
<html>
<head>
{{ include('admin/module/metas.html.twig') }}
<title>{% block title %}Tinternet &amp; 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">
&nbsp;
<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 &amp; cie" title="Tinternet &amp; 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 %}

View File

@ -0,0 +1,113 @@
{% apply spaceless %}
<!DOCTYPE html>
<html>
<head>
{{ include('admin/module/metas.html.twig') }}
<title>{% block title %}Tinternet &amp; 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">
&nbsp;
<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 &amp; cie" title="Tinternet &amp; 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 %}

View File

@ -0,0 +1 @@
{% extends 'admin/layout.html.twig' %}

View 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>

View File

@ -0,0 +1 @@
{{ content|raw }}

View 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 &amp; 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
View 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();

6320
yarn.lock Normal file

File diff suppressed because it is too large Load Diff