Merge pull request '[Feature] Passage vers Symfony' (#1) from v2 into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m54s
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m54s
Reviewed-on: #1
This commit is contained in:
commit
b86d691749
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
var/
|
||||
vendor/
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
41
.env
Executable file
41
.env
Executable file
@ -0,0 +1,41 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> 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
|
||||
#
|
||||
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# DATABASE_URL="mysql://kumora:kumora@127.0.0.1:3309/kumora?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
6
.env.test
Executable file
6
.env.test
Executable file
@ -0,0 +1,6 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
23
.gitea/workflows/build-docker.yml
Normal file
23
.gitea/workflows/build-docker.yml
Normal file
@ -0,0 +1,23 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build and Push with Kaniko
|
||||
uses: aevea/action-kaniko@master
|
||||
with:
|
||||
image: camelia-studio/kumora
|
||||
username: camelia
|
||||
password: camelia
|
||||
registry: ${{ secrets.REGISTRY_SERVER }}
|
||||
tag: ${{ gitea.ref_name }}
|
||||
tag_with_latest: true
|
36
.gitignore
vendored
Executable file
36
.gitignore
vendored
Executable file
@ -0,0 +1,36 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/asset-mapper ###
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
.fleet/
|
||||
*.iml
|
||||
|
||||
|
||||
/uploads/*
|
||||
!uploads/.gitkeep
|
||||
|
||||
/public/kumora/
|
11
.htaccess
Executable file
11
.htaccess
Executable file
@ -0,0 +1,11 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Rediriger les assets
|
||||
RewriteCond %{REQUEST_URI} ^/kumora/assets/
|
||||
RewriteRule ^kumora/assets/(.*)$ assets/$1 [L]
|
||||
|
||||
# Rediriger toutes les autres requêtes vers le dossier public
|
||||
RewriteCond %{REQUEST_URI} !^/kumora/public/
|
||||
RewriteRule ^(.*)$ public/$1 [L]
|
||||
</IfModule>
|
3
.symfony.local.yaml
Executable file
3
.symfony.local.yaml
Executable file
@ -0,0 +1,3 @@
|
||||
workers:
|
||||
tailwind:
|
||||
cmd: ['symfony', 'console', 'tailwind:build', '--watch']
|
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@ -0,0 +1,41 @@
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
|
||||
ARG APP_ENV=prod
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
acl \
|
||||
file \
|
||||
gettext \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
@composer \
|
||||
apcu \
|
||||
intl \
|
||||
opcache \
|
||||
zip \
|
||||
pdo_mysql \
|
||||
;
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
COPY --link composer.* symfony.* ./
|
||||
|
||||
ENV APP_ENV=${APP_ENV}
|
||||
|
||||
|
||||
# On ajoute un if pour installer les dépendances de dev si APP_ENV est égal à dev
|
||||
RUN composer install --no-cache --prefer-dist --no-dev --optimize-autoloader --no-scripts --no-progress;
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN rm -rf var/tailwind \
|
||||
&& php bin/console importmap:install \
|
||||
&& php bin/console tailwind:build \
|
||||
&& php bin/console asset-map:compile \
|
||||
&& php bin/console cache:clear
|
9
LICENSE
9
LICENSE
@ -1,9 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 camelia-studio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
10
assets/app.js
Executable file
10
assets/app.js
Executable file
@ -0,0 +1,10 @@
|
||||
import './bootstrap.js';
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* This file will be included onto the page via the importmap() Twig function,
|
||||
* which should already be in your base.html.twig.
|
||||
*/
|
||||
import './styles/app.css';
|
||||
|
||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
5
assets/bootstrap.js
vendored
Executable file
5
assets/bootstrap.js
vendored
Executable file
@ -0,0 +1,5 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
15
assets/controllers.json
Executable file
15
assets/controllers.json
Executable file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
0
assets/controllers/.gitkeep
Executable file
0
assets/controllers/.gitkeep
Executable file
60
assets/controllers/csrf_protection_controller.js
Executable file
60
assets/controllers/csrf_protection_controller.js
Executable file
@ -0,0 +1,60 @@
|
||||
var nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
||||
var tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
document.addEventListener('submit', function (event) {
|
||||
var csrfField = event.target.querySelector('input[data-controller="csrf-protection"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
var csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
var csrfToken = csrfField.value;
|
||||
|
||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.value = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
}
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
var cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
||||
document.addEventListener('turbo:submit-start', function (event) {
|
||||
var csrfField = event.detail.formSubmission.formElement.querySelector('input[data-controller="csrf-protection"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
var csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
event.detail.formSubmission.fetchRequest.headers[csrfCookie] = csrfField.value;
|
||||
}
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
||||
document.addEventListener('turbo:submit-end', function (event) {
|
||||
var csrfField = event.detail.formSubmission.formElement.querySelector('input[data-controller="csrf-protection"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
var csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
var cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
||||
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
});
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default 'csrf-protection-controller';
|
1
assets/icons/symfony.svg
Executable file
1
assets/icons/symfony.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 257"><circle cx="128" cy="128.827" r="128" fill="#1a171b"/><path fill="#fff" d="M183.706 48.124c-12.986.453-24.32 7.61-32.757 17.51c-9.342 10.855-15.557 23.73-20.035 36.872c-8.01-6.565-14.19-15.064-27.041-18.77c-9.933-2.852-20.366-1.674-29.96 5.474c-4.545 3.395-7.676 8.527-9.165 13.351c-3.855 12.537 4.053 23.694 7.645 27.7l7.853 8.416c1.619 1.65 5.518 5.955 3.612 12.127c-2.06 6.71-10.15 11.055-18.448 8.495c-3.706-1.13-9.03-3.891-7.838-7.779c.493-1.59 1.631-2.78 2.241-4.155c.56-1.181.827-2.067.997-2.587c1.516-4.95-.555-11.39-5.857-13.025c-4.946-1.516-10.007-.315-11.969 6.054c-2.225 7.235 1.237 20.366 19.783 26.084c21.729 6.676 40.11-5.155 42.717-20.586c1.642-9.665-2.722-16.845-10.717-26.08l-6.514-7.204c-3.946-3.942-5.301-10.661-1.217-15.825c3.446-4.356 8.354-6.215 16.392-4.029c11.733 3.186 16.963 11.327 25.69 17.893c-3.603 11.819-5.958 23.682-8.09 34.32l-1.299 7.931c-6.238 32.721-11 50.688-23.375 61.003c-2.493 1.773-6.057 4.427-11.429 4.612c-2.816.087-3.726-1.85-3.765-2.694c-.067-1.977 1.599-2.883 2.706-3.773c1.654-.902 4.155-2.398 3.985-7.191c-.18-5.664-4.872-10.575-11.654-10.35c-5.08.173-12.823 4.954-12.532 13.705c.303 9.039 8.728 15.813 21.43 15.384c6.79-.233 21.952-2.997 36.895-20.76c17.392-20.362 22.256-43.705 25.915-60.79l4.084-22.556c2.269.272 4.695.453 7.334.516c21.661.457 32.496-10.763 32.657-18.924c.107-4.939-3.241-9.799-7.928-9.689c-3.355.095-7.57 2.328-8.582 6.968c-.988 4.552 6.893 8.66.733 12.65c-4.376 2.832-12.221 4.828-23.269 3.206l2.009-11.103c4.1-21.055 9.157-46.954 28.341-47.584c1.398-.071 6.514.063 6.633 3.446c.035 1.13-.245 1.418-1.568 4.005c-1.347 2.017-1.855 3.734-1.792 5.707c.185 5.376 4.273 8.909 10.185 8.696c7.916-.256 10.193-7.963 10.063-11.921c-.32-9.3-10.122-15.175-23.1-14.75"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/images/favicon.ico
Executable file
BIN
assets/images/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
BIN
assets/images/logo.png
Executable file
BIN
assets/images/logo.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 814 KiB |
3
assets/styles/app.css
Executable file
3
assets/styles/app.css
Executable file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
122
auth.php
122
auth.php
@ -1,122 +0,0 @@
|
||||
<?php
|
||||
// auth.php
|
||||
session_start();
|
||||
require_once 'Database.php';
|
||||
|
||||
class Auth {
|
||||
private $config;
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require 'config.php';
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
public function login($username, $password) {
|
||||
// Vérifier les tentatives de connexion
|
||||
$attempts = $this->db->checkLoginAttempts($username);
|
||||
if ($attempts >= $this->config['security']['max_login_attempts']) {
|
||||
return ['success' => false, 'error' => 'too_many_attempts'];
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, username, password_hash, role, description
|
||||
FROM users
|
||||
WHERE username = :username
|
||||
');
|
||||
|
||||
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
|
||||
$result = $stmt->execute();
|
||||
$user = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
// Enregistrer la tentative
|
||||
$this->db->logLoginAttempt($username);
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
$_SESSION['auth_time'] = time();
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
$_SESSION['description'] = $user['description'];
|
||||
|
||||
// Log de connexion réussie
|
||||
$this->db->logActivity($user['id'], 'login');
|
||||
|
||||
return ['success' => true, 'user' => [
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'description' => $user['description']
|
||||
]];
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => 'invalid_credentials'];
|
||||
}
|
||||
|
||||
public function isAuthenticated() {
|
||||
if (!isset($_SESSION['auth_time']) || !isset($_SESSION['user_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$elapsed = time() - $_SESSION['auth_time'];
|
||||
if ($elapsed > $this->config['security']['session_duration']) {
|
||||
$this->logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasPermission($action) {
|
||||
if (!$this->isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$role = $_SESSION['role'];
|
||||
return isset($this->config['roles'][$role][$action]) &&
|
||||
$this->config['roles'][$role][$action];
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$this->db->logActivity($_SESSION['user_id'], 'logout');
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Point d'entrée API
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$auth = new Auth();
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (isset($data['action'])) {
|
||||
switch ($data['action']) {
|
||||
case 'login':
|
||||
if (isset($data['username']) && isset($data['password'])) {
|
||||
$result = $auth->login($data['username'], $data['password']);
|
||||
echo json_encode($result);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
$auth->logout();
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'check':
|
||||
echo json_encode([
|
||||
'authenticated' => $auth->isAuthenticated(),
|
||||
'user' => $auth->isAuthenticated() ? [
|
||||
'username' => $_SESSION['username'],
|
||||
'role' => $_SESSION['role'],
|
||||
'description' => $_SESSION['description']
|
||||
] : null
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
exit;
|
||||
}
|
||||
?>
|
21
bin/console
Executable file
21
bin/console
Executable file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
23
bin/phpunit
Executable file
23
bin/phpunit
Executable file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (!ini_get('date.timezone')) {
|
||||
ini_set('date.timezone', 'UTC');
|
||||
}
|
||||
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
} else {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
}
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
}
|
118
composer.json
Executable file
118
composer.json
Executable file
@ -0,0 +1,118 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/dbal": "^3.9.3",
|
||||
"doctrine/doctrine-bundle": "^2.13.1",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3.1",
|
||||
"doctrine/orm": "^3.3.1",
|
||||
"league/flysystem": "^3.29.1",
|
||||
"oneup/flysystem-bundle": "^4.12.3",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.1",
|
||||
"phpstan/phpdoc-parser": "^2.0",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/asset-mapper": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/doctrine-messenger": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/expression-language": "7.2.*",
|
||||
"symfony/flex": "^2.4.7",
|
||||
"symfony/form": "7.2.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/http-client": "7.2.*",
|
||||
"symfony/intl": "7.2.*",
|
||||
"symfony/mailer": "7.2.*",
|
||||
"symfony/mime": "7.2.*",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/notifier": "7.2.*",
|
||||
"symfony/process": "7.2.*",
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/security-csrf": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/stimulus-bundle": "^2.22.1",
|
||||
"symfony/string": "7.2.*",
|
||||
"symfony/translation": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/uid": "7.2.*",
|
||||
"symfony/ux-icons": "^2.22.1",
|
||||
"symfony/ux-turbo": "^2.22.1",
|
||||
"symfony/ux-twig-component": "^2.22.1",
|
||||
"symfony/validator": "7.2.*",
|
||||
"symfony/web-link": "7.2.*",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"symfonycasts/tailwind-bundle": "^0.6.1",
|
||||
"tales-from-a-dev/flowbite-bundle": "^0.7.1",
|
||||
"twig/extra-bundle": "^2.12|^3.18",
|
||||
"twig/twig": "^2.12|^3.18"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"bump-after-update": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||
"importmap:install": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.2.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6.22",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/debug-bundle": "7.2.*",
|
||||
"symfony/maker-bundle": "^1.61",
|
||||
"symfony/phpunit-bridge": "^7.2",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
}
|
||||
}
|
10768
composer.lock
generated
Executable file
10768
composer.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
36
config.php
36
config.php
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
// config.php
|
||||
return [
|
||||
'db' => [
|
||||
'path' => __DIR__ . '/database.sqlite'
|
||||
],
|
||||
'security' => [
|
||||
'session_duration' => 3600,
|
||||
'max_login_attempts' => 3,
|
||||
'attempt_window' => 1800 // 30 minutes
|
||||
],
|
||||
'roles' => [
|
||||
'admin' => [
|
||||
'upload' => true,
|
||||
'download' => true,
|
||||
'delete' => true,
|
||||
'rename' => true,
|
||||
'view_logs' => true
|
||||
],
|
||||
'user' => [
|
||||
'upload' => true,
|
||||
'download' => true,
|
||||
'delete' => false,
|
||||
'rename' => false,
|
||||
'view_logs' => false
|
||||
],
|
||||
'visitor' => [
|
||||
'upload' => false,
|
||||
'download' => true,
|
||||
'delete' => false,
|
||||
'rename' => false,
|
||||
'view_logs' => false
|
||||
]
|
||||
]
|
||||
];
|
||||
?>
|
22
config/bundles.php
Executable file
22
config/bundles.php
Executable file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true],
|
||||
Oneup\FlysystemBundle\OneupFlysystemBundle::class => ['all' => true],
|
||||
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
|
||||
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||
TalesFromADev\Twig\Extra\Tailwind\Bridge\Symfony\Bundle\TalesFromADevTwigExtraTailwindBundle::class => ['all' => true],
|
||||
TalesFromADev\FlowbiteBundle\TalesFromADevFlowbiteBundle::class => ['all' => true],
|
||||
];
|
12
config/packages/asset_mapper.yaml
Executable file
12
config/packages/asset_mapper.yaml
Executable file
@ -0,0 +1,12 @@
|
||||
framework:
|
||||
asset_mapper:
|
||||
# The paths to make available to the asset mapper.
|
||||
paths:
|
||||
- assets/
|
||||
missing_import_mode: strict
|
||||
public_prefix: /kumora/assets
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
asset_mapper:
|
||||
missing_import_mode: warn
|
19
config/packages/cache.yaml
Executable file
19
config/packages/cache.yaml
Executable file
@ -0,0 +1,19 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The "app" cache stores to the filesystem by default.
|
||||
# The data in this cache should persist between deploys.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
# Namespaced pools use the above "app" backend by default
|
||||
#pools:
|
||||
#my.dedicated.cache: null
|
11
config/packages/csrf.yaml
Executable file
11
config/packages/csrf.yaml
Executable file
@ -0,0 +1,11 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
5
config/packages/debug.yaml
Executable file
5
config/packages/debug.yaml
Executable file
@ -0,0 +1,5 @@
|
||||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
54
config/packages/doctrine.yaml
Executable file
54
config/packages/doctrine.yaml
Executable file
@ -0,0 +1,54 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '16'
|
||||
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
6
config/packages/doctrine_migrations.yaml
Executable file
6
config/packages/doctrine_migrations.yaml
Executable file
@ -0,0 +1,6 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
17
config/packages/framework.yaml
Executable file
17
config/packages/framework.yaml
Executable file
@ -0,0 +1,17 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
form: { csrf_protection: { token_id: 'submit' } }
|
||||
csrf_protection:
|
||||
stateless_token_ids: ['submit', 'authenticate', 'logout']
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session: true
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
3
config/packages/mailer.yaml
Executable file
3
config/packages/mailer.yaml
Executable file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_DSN)%'
|
29
config/packages/messenger.yaml
Executable file
29
config/packages/messenger.yaml
Executable file
@ -0,0 +1,29 @@
|
||||
framework:
|
||||
messenger:
|
||||
failure_transport: failed
|
||||
|
||||
transports:
|
||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||
async:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
use_notify: true
|
||||
check_delayed_interval: 60000
|
||||
retry_strategy:
|
||||
max_retries: 3
|
||||
multiplier: 2
|
||||
failed: 'doctrine://default?queue_name=failed'
|
||||
# sync: 'sync://'
|
||||
|
||||
default_bus: messenger.bus.default
|
||||
|
||||
buses:
|
||||
messenger.bus.default: []
|
||||
|
||||
routing:
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
||||
Symfony\Component\Notifier\Message\ChatMessage: async
|
||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
62
config/packages/monolog.yaml
Executable file
62
config/packages/monolog.yaml
Executable file
@ -0,0 +1,62 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
12
config/packages/notifier.yaml
Executable file
12
config/packages/notifier.yaml
Executable file
@ -0,0 +1,12 @@
|
||||
framework:
|
||||
notifier:
|
||||
chatter_transports:
|
||||
texter_transports:
|
||||
channel_policy:
|
||||
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
||||
urgent: ['email']
|
||||
high: ['email']
|
||||
medium: ['email']
|
||||
low: ['email']
|
||||
admin_recipients:
|
||||
- { email: admin@example.com }
|
10
config/packages/oneup_flysystem.yaml
Executable file
10
config/packages/oneup_flysystem.yaml
Executable file
@ -0,0 +1,10 @@
|
||||
# Read the documentation: https://github.com/1up-lab/OneupFlysystemBundle
|
||||
oneup_flysystem:
|
||||
adapters:
|
||||
default_adapter:
|
||||
local:
|
||||
location: "%kernel.project_dir%/uploads"
|
||||
filesystems:
|
||||
default_filesystem:
|
||||
adapter: default_adapter
|
||||
alias: League\Flysystem\Filesystem
|
10
config/packages/routing.yaml
Executable file
10
config/packages/routing.yaml
Executable file
@ -0,0 +1,10 @@
|
||||
framework:
|
||||
router:
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
53
config/packages/security.yaml
Executable file
53
config/packages/security.yaml
Executable file
@ -0,0 +1,53 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto"
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
# used to reload user from session & other features (e.g. switch_user)
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
enable_csrf: true
|
||||
default_target_path: app_home
|
||||
always_use_default_target_path: true
|
||||
logout:
|
||||
path: app_logout
|
||||
# where to redirect after logout
|
||||
target: app_home
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# 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 }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# By default, password hashers are resource intensive and take time. This is
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
8
config/packages/translation.yaml
Executable file
8
config/packages/translation.yaml
Executable file
@ -0,0 +1,8 @@
|
||||
framework:
|
||||
default_locale: fr
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- fr
|
||||
- en
|
||||
providers:
|
8
config/packages/twig.yaml
Executable file
8
config/packages/twig.yaml
Executable file
@ -0,0 +1,8 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
form_themes:
|
||||
- '@TalesFromADevFlowbite/form/default.html.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
5
config/packages/twig_component.yaml
Executable file
5
config/packages/twig_component.yaml
Executable file
@ -0,0 +1,5 @@
|
||||
twig_component:
|
||||
anonymous_template_directory: 'components/'
|
||||
defaults:
|
||||
# Namespace & directory for components
|
||||
App\Twig\Components\: 'components/'
|
11
config/packages/validator.yaml
Executable file
11
config/packages/validator.yaml
Executable file
@ -0,0 +1,11 @@
|
||||
framework:
|
||||
validation:
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
18
config/packages/web_profiler.yaml
Executable file
18
config/packages/web_profiler.yaml
Executable file
@ -0,0 +1,18 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
excluded_ajax_paths: '^/((index|app(_[\w]+)?)\.php/)?(?!/kumora)(_(profiler|wdt)|css|images|js)/'
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
only_exceptions: false
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
5
config/preload.php
Executable file
5
config/preload.php
Executable file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
6
config/routes.yaml
Executable file
6
config/routes.yaml
Executable file
@ -0,0 +1,6 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
prefix: /kumora
|
4
config/routes/framework.yaml
Executable file
4
config/routes/framework.yaml
Executable file
@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
3
config/routes/security.yaml
Executable file
3
config/routes/security.yaml
Executable file
@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
8
config/routes/web_profiler.yaml
Executable file
8
config/routes/web_profiler.yaml
Executable file
@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
|
||||
prefix: /kumora/_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
|
||||
prefix: /kumora/_profiler
|
24
config/services.yaml
Executable file
24
config/services.yaml
Executable file
@ -0,0 +1,24 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
111
database.php
111
database.php
@ -1,111 +0,0 @@
|
||||
<?php
|
||||
// Database.php
|
||||
class Database {
|
||||
private $db;
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {
|
||||
$config = require 'config.php';
|
||||
$this->db = new SQLite3($config['db']['path']);
|
||||
$this->db->enableExceptions(true);
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function logActivity($userId, $actionType, $details = '') {
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO activity_logs (user_id, action_type, details, ip_address)
|
||||
VALUES (:user_id, :action_type, :details, :ip)
|
||||
');
|
||||
|
||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':action_type', $actionType, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':details', $details, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':ip', $_SERVER['REMOTE_ADDR'], SQLITE3_TEXT);
|
||||
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
public function getActivityLogs($filters = []) {
|
||||
$query = '
|
||||
SELECT
|
||||
al.*,
|
||||
u.username,
|
||||
u.role
|
||||
FROM activity_logs al
|
||||
LEFT JOIN users u ON al.user_id = u.id
|
||||
WHERE 1=1
|
||||
';
|
||||
|
||||
$params = [];
|
||||
|
||||
if (!empty($filters['action_type'])) {
|
||||
$query .= ' AND action_type = :action_type';
|
||||
$params[':action_type'] = $filters['action_type'];
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$query .= ' AND created_at >= :date_from';
|
||||
$params[':date_from'] = $filters['date_from'];
|
||||
}
|
||||
|
||||
if (!empty($filters['date_to'])) {
|
||||
$query .= ' AND created_at <= :date_to';
|
||||
$params[':date_to'] = $filters['date_to'];
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY created_at ' .
|
||||
(!empty($filters['order']) && $filters['order'] === 'asc' ? 'ASC' : 'DESC');
|
||||
|
||||
$stmt = $this->db->prepare($query);
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$stmt->bindValue($key, $value);
|
||||
}
|
||||
|
||||
$result = $stmt->execute();
|
||||
$logs = [];
|
||||
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$logs[] = $row;
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
public function checkLoginAttempts($username) {
|
||||
$config = require 'config.php';
|
||||
$window = $config['security']['attempt_window'];
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT COUNT(*) as attempts
|
||||
FROM login_attempts
|
||||
WHERE username = :username
|
||||
AND attempt_time > datetime("now", "-' . $window . ' seconds")
|
||||
');
|
||||
|
||||
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
|
||||
$result = $stmt->execute();
|
||||
$row = $result->fetchArray();
|
||||
|
||||
return $row['attempts'];
|
||||
}
|
||||
|
||||
public function logLoginAttempt($username) {
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO login_attempts (username, ip_address)
|
||||
VALUES (:username, :ip)
|
||||
');
|
||||
|
||||
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':ip', $_SERVER['REMOTE_ADDR'], SQLITE3_TEXT);
|
||||
|
||||
return $stmt->execute();
|
||||
}
|
||||
}
|
||||
?>
|
30
database.sql
30
database.sql
@ -1,30 +0,0 @@
|
||||
-- schema.sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user', 'visitor')),
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE login_attempts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
attempt_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
username VARCHAR(50)
|
||||
);
|
||||
|
||||
CREATE TABLE activity_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
action_type VARCHAR(20) NOT NULL CHECK (action_type IN ('login', 'logout', 'upload', 'download', 'delete', 'rename')),
|
||||
details TEXT,
|
||||
ip_address VARCHAR(45),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Insertion d'un utilisateur admin par défaut (mot de passe: admin123)
|
||||
INSERT INTO users (username, password_hash, role, description)
|
||||
VALUES ('admin', '$2y$10$YourHashedPasswordHere', 'admin', 'Administrateur principal');
|
40
get-file.php
40
get-file.php
@ -1,40 +0,0 @@
|
||||
<?php
|
||||
// get-file.php
|
||||
require_once 'auth.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
||||
// Vérifier l'authentification
|
||||
if (!$auth->isAuthenticated()) {
|
||||
http_response_code(401);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérifier si un fichier est spécifié
|
||||
if (!isset($_GET['file'])) {
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$filename = $_GET['file'];
|
||||
$filepath = './' . $filename;
|
||||
|
||||
// Vérifier que le fichier existe et est dans le dossier courant
|
||||
if (!file_exists($filepath) || !is_file($filepath) || dirname(realpath($filepath)) !== realpath('.')) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fichiers système à ne pas servir
|
||||
$forbidden_files = ['index.html', 'list-files.php', 'auth.php', 'config.php', 'get-file.php'];
|
||||
if (in_array($filename, $forbidden_files)) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Servir le fichier
|
||||
$mime_type = mime_content_type($filepath);
|
||||
header('Content-Type: ' . $mime_type);
|
||||
header('Content-Disposition: inline; filename="' . basename($filepath) . '"');
|
||||
readfile($filepath);
|
||||
?>
|
28
importmap.php
Executable file
28
importmap.php
Executable file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
* - "path" is a path inside the asset mapper system. Use the
|
||||
* "debug:asset-map" command to see the full list of paths.
|
||||
*
|
||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||
*
|
||||
* The "importmap:require" command can be used to add new entries to this file.
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'path' => './assets/app.js',
|
||||
'entrypoint' => true,
|
||||
],
|
||||
'@symfony/stimulus-bundle' => [
|
||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
||||
],
|
||||
'@hotwired/stimulus' => [
|
||||
'version' => '3.2.2',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '8.0.12',
|
||||
],
|
||||
];
|
778
index.html
778
index.html
@ -1,778 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Esenjin | Explorateur de fichiers</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
<style>
|
||||
/* Variables de thème */
|
||||
:root[data-theme="dark"] {
|
||||
--primary-color: #64B5F6;
|
||||
--hover-color: #42A5F5;
|
||||
--bg-color: #1a1a1a;
|
||||
--container-bg: #2d2d2d;
|
||||
--text-color: #e0e0e0;
|
||||
--border-color: #404040;
|
||||
--meta-color: #909090;
|
||||
--input-bg: #3d3d3d;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--primary-color: #4a90e2;
|
||||
--hover-color: #357abd;
|
||||
--bg-color: #f5f6fa;
|
||||
--container-bg: white;
|
||||
--text-color: #333;
|
||||
--border-color: #eee;
|
||||
--meta-color: #666;
|
||||
--input-bg: white;
|
||||
}
|
||||
|
||||
/* Global styles */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: var(--container-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: var(--container-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 2em;
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
word-break: break-word;
|
||||
margin-top: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 0.8em;
|
||||
color: var(--meta-color);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 10px 15px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.sort-select {
|
||||
padding: 10px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 1000;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
min-height: 200px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
top: 25px;
|
||||
color: var(--text-color);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-container img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.preview-container video,
|
||||
.preview-container audio {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.preview-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* formulaire de conexion */
|
||||
.login-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
background: var(--container-bg);
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-form h2 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.login-form button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.login-form button:hover {
|
||||
background: var(--hover-color);
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: #ff4444;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-controls {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 80px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
padding: 10px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-form input[type="text"],
|
||||
.login-form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.user-description {
|
||||
color: var(--meta-color);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loginContainer" class="login-container">
|
||||
<form id="loginForm" class="login-form">
|
||||
<h2>Connexion</h2>
|
||||
<div id="loginError" class="login-error">Identifiants incorrects</div>
|
||||
<input type="text" id="username" placeholder="Identifiant" required>
|
||||
<input type="password" id="password" placeholder="Mot de passe" required>
|
||||
<button type="submit">Se connecter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="user-controls">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar" id="userAvatar"></div>
|
||||
<div>
|
||||
<div class="user-name" id="userName"></div>
|
||||
<div class="user-description" id="userDescription"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="logoutButton" class="logout-button">
|
||||
<i class="fas fa-sign-out-alt"></i> Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="theme-toggle" id="themeToggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
|
||||
<div class="container">
|
||||
<h1>De bric et de broc ...</h1>
|
||||
|
||||
<div class="search-bar">
|
||||
<input type="text" class="search-input" placeholder="Rechercher des fichiers..." id="searchInput">
|
||||
<select class="sort-select" id="sortSelect">
|
||||
<option value="name">Nom</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="size">Taille</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="file-grid" id="fileGrid">
|
||||
<!-- Les fichiers seront ajoutés ici dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="previewModal" class="modal">
|
||||
<span class="modal-close" id="modalClose">×</span>
|
||||
<div class="modal-content">
|
||||
<div id="previewContainer" class="preview-container">
|
||||
<div class="loading">Chargement ...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Gestion de l'authentification
|
||||
const loginContainer = document.getElementById('loginContainer');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const loginError = document.getElementById('loginError');
|
||||
const logoutButton = document.getElementById('logoutButton');
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'check' })
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.authenticated;
|
||||
} catch (error) {
|
||||
console.error('Erreur de vérification d\'authentification:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
try {
|
||||
const response = await fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'login',
|
||||
username,
|
||||
password
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.user) {
|
||||
updateUserInfo(data.user);
|
||||
}
|
||||
return data.success;
|
||||
} catch (error) {
|
||||
console.error('Erreur de connexion:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserInfo(user) {
|
||||
const avatar = document.getElementById('userAvatar');
|
||||
const name = document.getElementById('userName');
|
||||
const description = document.getElementById('userDescription');
|
||||
|
||||
avatar.textContent = user.username.charAt(0).toUpperCase();
|
||||
name.textContent = user.username;
|
||||
description.textContent = user.description;
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const success = await login(username, password);
|
||||
|
||||
if (success) {
|
||||
hideLoginForm();
|
||||
initializeFiles();
|
||||
} else {
|
||||
loginError.style.display = 'block';
|
||||
}
|
||||
|
||||
document.getElementById('username').value = '';
|
||||
document.getElementById('password').value = '';
|
||||
});
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'check' })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.authenticated && data.user) {
|
||||
updateUserInfo(data.user);
|
||||
}
|
||||
return data.authenticated;
|
||||
} catch (error) {
|
||||
console.error('Erreur de vérification d\'authentification:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'logout' })
|
||||
});
|
||||
showLoginForm();
|
||||
} catch (error) {
|
||||
console.error('Erreur de déconnexion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoginForm() {
|
||||
loginContainer.style.display = 'flex';
|
||||
loginError.style.display = 'none';
|
||||
}
|
||||
|
||||
function hideLoginForm() {
|
||||
loginContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const password = document.getElementById('password').value;
|
||||
const success = await login(password);
|
||||
|
||||
if (success) {
|
||||
hideLoginForm();
|
||||
initializeFiles();
|
||||
} else {
|
||||
loginError.style.display = 'block';
|
||||
}
|
||||
|
||||
document.getElementById('password').value = '';
|
||||
});
|
||||
|
||||
logoutButton.addEventListener('click', logout);
|
||||
|
||||
// Vérifier l'authentification au chargement
|
||||
async function initialize() {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (isAuthenticated) {
|
||||
hideLoginForm();
|
||||
await initializeFiles();
|
||||
} else {
|
||||
showLoginForm();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle thème
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const html = document.documentElement;
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
themeToggle.innerHTML = newTheme === 'dark' ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
|
||||
});
|
||||
|
||||
// Fonction pour déterminer l'icône
|
||||
function getFileIcon(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const iconMap = {
|
||||
pdf: 'file-pdf',
|
||||
doc: 'file-word',
|
||||
docx: 'file-word',
|
||||
xls: 'file-excel',
|
||||
xlsx: 'file-excel',
|
||||
jpg: 'file-image',
|
||||
jpeg: 'file-image',
|
||||
png: 'file-image',
|
||||
gif: 'file-image',
|
||||
mp3: 'file-audio',
|
||||
wav: 'file-audio',
|
||||
mp4: 'file-video',
|
||||
zip: 'file-archive',
|
||||
rar: 'file-archive'
|
||||
};
|
||||
|
||||
return iconMap[ext] || 'file';
|
||||
}
|
||||
|
||||
// Fonction pour formater la taille
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function createFileElement(file) {
|
||||
const fileItem = document.createElement('a');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.href = '#';
|
||||
|
||||
const icon = getFileIcon(file.name);
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<i class="fa-solid fa-${icon} file-icon"></i>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-meta">${formatFileSize(file.size)}</div>
|
||||
<div class="file-meta">${file.date}</div>
|
||||
`;
|
||||
|
||||
// Gestion du clic pour la prévisualisation
|
||||
fileItem.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
previewFile(file);
|
||||
});
|
||||
|
||||
return fileItem;
|
||||
}
|
||||
|
||||
// Gestion de la prévisualisation
|
||||
const modal = document.getElementById('previewModal');
|
||||
const modalClose = document.getElementById('modalClose');
|
||||
const previewContainer = document.getElementById('previewContainer');
|
||||
|
||||
modalClose.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
previewContainer.innerHTML = '<div class="loading">Chargement...</div>';
|
||||
});
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
previewContainer.innerHTML = '<div class="loading">Chargement...</div>';
|
||||
}
|
||||
});
|
||||
|
||||
function getFileType(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
const videoTypes = ['mp4', 'webm', 'ogg'];
|
||||
const audioTypes = ['mp3', 'wav', 'ogg'];
|
||||
const documentTypes = ['pdf'];
|
||||
|
||||
if (imageTypes.includes(ext)) return 'image';
|
||||
if (videoTypes.includes(ext)) return 'video';
|
||||
if (audioTypes.includes(ext)) return 'audio';
|
||||
if (documentTypes.includes(ext)) return 'pdf';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
async function previewFile(file) {
|
||||
modal.style.display = 'block';
|
||||
const fileType = getFileType(file.name);
|
||||
const fileUrl = file.path;
|
||||
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
previewContainer.innerHTML = `
|
||||
<img src="${fileUrl}" alt="${file.name}" />
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'video':
|
||||
previewContainer.innerHTML = `
|
||||
<video controls>
|
||||
<source src="${fileUrl}" type="video/${file.name.split('.').pop()}">
|
||||
Votre navigateur ne supporte pas la lecture vidéo.
|
||||
</video>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
previewContainer.innerHTML = `
|
||||
<audio controls>
|
||||
<source src="${fileUrl}" type="audio/${file.name.split('.').pop()}">
|
||||
Votre navigateur ne supporte pas la lecture audio.
|
||||
</audio>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'pdf':
|
||||
// Utilisation de PDF.js pour les PDF
|
||||
previewContainer.innerHTML = `
|
||||
<iframe src="${fileUrl}" type="application/pdf"></iframe>
|
||||
`;
|
||||
break;
|
||||
|
||||
default:
|
||||
previewContainer.innerHTML = `
|
||||
<div class="loading">
|
||||
Ce type de fichier ne peut pas être prévisualisé.<br>
|
||||
<a href="${fileUrl}" target="_blank" style="color: var(--primary-color);">
|
||||
Télécharger le fichier
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les fichiers depuis l'API
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const response = await fetch('list-files.php');
|
||||
const files = await response.json();
|
||||
return files;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des fichiers:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation
|
||||
let allFiles = [];
|
||||
|
||||
async function initializeFiles() {
|
||||
allFiles = await loadFiles();
|
||||
displayFiles(allFiles);
|
||||
}
|
||||
|
||||
function displayFiles(files) {
|
||||
const fileGrid = document.getElementById('fileGrid');
|
||||
fileGrid.innerHTML = '';
|
||||
files.forEach(file => {
|
||||
fileGrid.appendChild(createFileElement(file));
|
||||
});
|
||||
}
|
||||
|
||||
// Recherche
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const filteredFiles = allFiles.filter(file =>
|
||||
file.name.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
displayFiles(filteredFiles);
|
||||
});
|
||||
|
||||
// Tri
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
sortSelect.addEventListener('change', (e) => {
|
||||
const sortBy = e.target.value;
|
||||
const sortedFiles = [...allFiles].sort((a, b) => {
|
||||
switch(sortBy) {
|
||||
case 'name':
|
||||
return a.name.localeCompare(b.name);
|
||||
case 'date':
|
||||
return new Date(b.date) - new Date(a.date);
|
||||
case 'size':
|
||||
return b.size - a.size;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
displayFiles(sortedFiles);
|
||||
});
|
||||
|
||||
// Modifier la fonction loadFiles pour gérer les erreurs d'authentification
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const response = await fetch('list-files.php');
|
||||
if (response.status === 401) {
|
||||
showLoginForm();
|
||||
return [];
|
||||
}
|
||||
const files = await response.json();
|
||||
return files;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des fichiers:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Lancer l'initialisation
|
||||
initialize();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
// list-files.php
|
||||
require_once 'auth.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
||||
// Vérifier l'authentification
|
||||
if (!$auth->isAuthenticated()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Non authentifié']);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
function scanDirectory($dir = '.') {
|
||||
$files = [];
|
||||
$scan = scandir($dir);
|
||||
|
||||
foreach ($scan as $file) {
|
||||
// Ignore les fichiers cachés, système et les fichiers de configuration
|
||||
if ($file[0] === '.' || in_array($file, ['index.html', 'list-files.php', 'auth.php', 'config.php'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $dir . '/' . $file;
|
||||
|
||||
if (is_file($path)) {
|
||||
$files[] = [
|
||||
'name' => $file,
|
||||
'size' => filesize($path),
|
||||
'date' => date('Y-m-d', filemtime($path)),
|
||||
'path' => 'get-file.php?file=' . rawurlencode($file)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
try {
|
||||
$files = scanDirectory('.');
|
||||
echo json_encode($files);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
0
migrations/.gitignore
vendored
Executable file
0
migrations/.gitignore
vendored
Executable file
37
migrations/Version20241229133017.php
Executable file
37
migrations/Version20241229133017.php
Executable file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20241229133017 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Ajout de la table user et messenger_messages';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE "user" (id BLOB NOT NULL --(DC2Type:uuid)
|
||||
, email VARCHAR(255) NOT NULL, roles CLOB NOT NULL --(DC2Type:json)
|
||||
, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
|
||||
$this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
|
||||
)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
$this->addSql('DROP TABLE messenger_messages');
|
||||
}
|
||||
}
|
38
phpunit.xml.dist
Executable file
38
phpunit.xml.dist
Executable file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
convertDeprecationsToExceptions="false"
|
||||
>
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
|
||||
<listeners>
|
||||
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
|
||||
</listeners>
|
||||
|
||||
<extensions>
|
||||
</extensions>
|
||||
</phpunit>
|
73
public/.htaccess
Executable file
73
public/.htaccess
Executable file
@ -0,0 +1,73 @@
|
||||
# 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 +SymLinksIfOwnerMatch
|
||||
|
||||
# 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>
|
||||
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
|
||||
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
|
||||
Options +SymLinksIfOwnerMatch
|
||||
|
||||
RewriteEngine On
|
||||
RewriteRule ^assets/(.*)$ assets/$1 [L]
|
||||
# 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 %{ENV:BASE} ^/kumora(.+)$
|
||||
RewriteRule .* - [E=BASE:%1]
|
||||
|
||||
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>
|
9
public/index.php
Executable file
9
public/index.php
Executable file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
78
renovate.json
Executable file
78
renovate.json
Executable file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":automergeDisabled",
|
||||
":dependencyDashboard",
|
||||
":disableRateLimiting",
|
||||
"docker:pinDigests",
|
||||
":prConcurrentLimitNone"
|
||||
],
|
||||
"reviewers": [
|
||||
"@skitounet"
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": [
|
||||
"\\.yaml$"
|
||||
],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>[^:]+?) depName=(?<depName>.+?)( versioning=(?<versioning>.+?))?( extractVersion=(?<extractVersion>.+?))?( registryUrl=(?<registryUrl>.+?))?\\s.+?[:=]\\s*[\"']?(?<currentValue>.+?)[\"']?\\s"
|
||||
],
|
||||
"versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver-coerced{{/if}}",
|
||||
"extractVersionTemplate": "{{#if extractVersion}}{{{extractVersion}}}{{else}}^v?(?<version>.+)${{/if}}"
|
||||
}
|
||||
],
|
||||
"ignorePaths": [
|
||||
"public/**"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["composer"],
|
||||
"matchDepTypes": ["require-dev"],
|
||||
"addLabels": ["php-dev", "automerge"],
|
||||
"groupName": "php-dev",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["composer"],
|
||||
"matchDepTypes": ["require"],
|
||||
"matchUpdateTypes": ["minor", "patch", "digest", "pin", "pinDigest"],
|
||||
"addLabels": ["php-mineur", "automerge"],
|
||||
"groupName": "php-mineur",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["composer"],
|
||||
"matchDepTypes": ["require"],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"addLabels": ["php-majeur"],
|
||||
"groupName": "php-majeur",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"addLabels": ["node-dev", "automerge"],
|
||||
"groupName": "node-dev",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchDepTypes": ["dependencies"],
|
||||
"matchUpdateTypes": ["minor", "patch", "digest", "pin", "pinDigest"],
|
||||
"addLabels": ["node-mineur", "automerge"],
|
||||
"groupName": "node-mineur",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchDepTypes": ["dependencies"],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"addLabels": ["node-majeur"],
|
||||
"groupName": "node-majeur",
|
||||
"automerge": false
|
||||
}
|
||||
]
|
||||
}
|
63
src/Command/CreateUserCommand.php
Executable file
63
src/Command/CreateUserCommand.php
Executable file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-user',
|
||||
description: 'Permet de créer un utilisateur',
|
||||
)]
|
||||
class CreateUserCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserRepository $userRepository
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$email = $io->ask('Email de l\'utilisateur');
|
||||
$password = $io->askHidden('Mot de passe de l\'utilisateur');
|
||||
$isAdmin = $io->confirm('Est-ce un administrateur ?');
|
||||
|
||||
try {
|
||||
$user = $this->userRepository->findOneBy(['email' => $email]);
|
||||
if ($user) {
|
||||
$io->error('Un utilisateur existe déjà avec cet email');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($email);
|
||||
$user->setPassword($this->passwordHasher->hashPassword($user, $password));
|
||||
$user->setRoles($isAdmin ? ['ROLE_ADMIN'] : ['ROLE_USER']);
|
||||
$user->initId();
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success('Utilisateur créé avec succès');
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Une erreur est survenue lors de la création de l\'utilisateur');
|
||||
}
|
||||
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
0
src/Controller/.gitignore
vendored
Executable file
0
src/Controller/.gitignore
vendored
Executable file
91
src/Controller/AdminController.php
Executable file
91
src/Controller/AdminController.php
Executable file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\UserAdminType;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/admin', name: 'app_admin_')]
|
||||
class AdminController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserPasswordHasherInterface $passwordEncoder,
|
||||
private readonly UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/', name: 'index')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('admin/index.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/users', name: 'user_index')]
|
||||
public function indexUsers(): Response
|
||||
{
|
||||
$users = $this->userRepository->findAll();
|
||||
return $this->render('admin/user_index.html.twig', [
|
||||
'users' => $users,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
#[Route('/users/create', name: 'user_create')]
|
||||
#[Route('/users/edit/{user}', name: 'user_edit')]
|
||||
public function editUsers(#[MapEntity(id: 'user')] ?User $user, Request $request): Response
|
||||
{
|
||||
$isNew = false;
|
||||
if (!$user) {
|
||||
$user = new User();
|
||||
$isNew = true;
|
||||
}
|
||||
|
||||
$form = $this->createForm(UserAdminType::class, $user);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$role = $form->get('role')->getData();
|
||||
$user->setRoles([$role]);
|
||||
$user->initId();
|
||||
|
||||
if ($form->has('plainPassword')) {
|
||||
$plainPassword = $form->get('plainPassword')->getData();
|
||||
$user->setPassword($this->passwordEncoder->hashPassword($user, $plainPassword));
|
||||
}
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'L\'utilisateur a bien été enregistré !');
|
||||
return $this->redirectToRoute('app_admin_user_index');
|
||||
}
|
||||
|
||||
return $this->render('admin/user_edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'user' => $user,
|
||||
'isNew' => $isNew,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/users/delete/{user}', name: 'user_delete')]
|
||||
public function deleteUser(#[MapEntity(id: 'user')] User $user): Response
|
||||
{
|
||||
$this->entityManager->remove($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'L\'utilisateur a bien été supprimé !');
|
||||
return $this->redirectToRoute('app_admin_user_index');
|
||||
}
|
||||
}
|
95
src/Controller/FilesController.php
Executable file
95
src/Controller/FilesController.php
Executable file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemReader;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/files', 'app_files_')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
class FilesController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/', name: 'index')]
|
||||
public function index(Filesystem $defaultAdapter, UrlGeneratorInterface $urlGenerator, #[MapQueryParameter('path')] string $path = ''): Response
|
||||
{
|
||||
// On retire les slashs en début et fin de chaîne
|
||||
$path = trim($path, '/');
|
||||
// On retire les chemins relatifs
|
||||
$path = str_replace('..', '', $path);
|
||||
$path = str_replace('//', '/', $path);
|
||||
|
||||
if ($path !== '' && !$defaultAdapter->directoryExists($path)) {
|
||||
throw $this->createNotFoundException("Ce dossier n'existe pas !");
|
||||
}
|
||||
|
||||
|
||||
$files = $defaultAdapter->listContents('/' . $path);
|
||||
|
||||
$realFiles = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!str_starts_with($file['path'], '.')) {
|
||||
$realFiles[] = [
|
||||
'type' => $file['type'],
|
||||
'path' => $file['path'],
|
||||
'last_modified' => $file['lastModified'],
|
||||
'size' => $file['fileSize'] ?? null,
|
||||
'url' => $file['type'] === 'file'
|
||||
? $this->generateUrl('app_files_app_file_proxy', ['filename' => $file['path']], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
: $this->generateUrl('app_files_index', ['path' => $path . '/' . $file['path']]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// On trie par type puis par nom
|
||||
usort($realFiles, static function ($a, $b) {
|
||||
if ($a['type'] === $b['type']) {
|
||||
return $a['path'] <=> $b['path'];
|
||||
} else {
|
||||
return $a['type'] <=> $b['type'];
|
||||
}
|
||||
});
|
||||
|
||||
return $this->render('files/index.html.twig', [
|
||||
'files' => $realFiles,
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/file-proxy', name: 'app_file_proxy')]
|
||||
public function fileProxy(Filesystem $defaultAdapter, #[MapQueryParameter('filename')]string $filename)
|
||||
{
|
||||
$mimetype = $defaultAdapter->mimeType($filename);
|
||||
if ($mimetype === '') {
|
||||
$mimetype = 'application/octet-stream';
|
||||
}
|
||||
|
||||
$response = new StreamedResponse(static function () use ($filename, $defaultAdapter): void {
|
||||
$outputStream = fopen('php://output', 'w');
|
||||
$fileStream = $defaultAdapter->readStream($filename);
|
||||
stream_copy_to_stream($fileStream, $outputStream);
|
||||
});
|
||||
|
||||
$response->headers->set('Content-Type', $mimetype);
|
||||
$disposition = HeaderUtils::makeDisposition(
|
||||
HeaderUtils::DISPOSITION_ATTACHMENT,
|
||||
basename($filename)
|
||||
);
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
}
|
18
src/Controller/HomeController.php
Executable file
18
src/Controller/HomeController.php
Executable file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('home/index.html.twig', [
|
||||
'controller_name' => 'HomeController',
|
||||
]);
|
||||
}
|
||||
}
|
41
src/Controller/SecurityController.php
Executable file
41
src/Controller/SecurityController.php
Executable file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/login', name: 'app_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
// get the login error if there is one
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
|
||||
// last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'app_logout')]
|
||||
public function logout(): void
|
||||
{
|
||||
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
||||
}
|
||||
}
|
0
src/Entity/.gitignore
vendored
Executable file
0
src/Entity/.gitignore
vendored
Executable file
121
src/Entity/User.php
Executable file
121
src/Entity/User.php
Executable file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: '`user`')]
|
||||
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
|
||||
#[UniqueEntity(fields: ['email'], message: 'Un compte existe déjà avec cet email')]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid', length: 180)]
|
||||
private ?Uuid $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $email = null;
|
||||
|
||||
/**
|
||||
* @var list<string> The user roles
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private array $roles = [];
|
||||
|
||||
/**
|
||||
* @var string The hashed password
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private ?string $password = null;
|
||||
|
||||
|
||||
public function initId(): void
|
||||
{
|
||||
if ($this->id !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->id = Uuid::v4();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* A visual identifier that represents this user.
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
// guarantee every user at least has ROLE_USER
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
public function setRoles(array $roles): static
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): static
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
0
src/Form/.gitkeep
Executable file
0
src/Form/.gitkeep
Executable file
56
src/Form/UserAdminType.php
Executable file
56
src/Form/UserAdminType.php
Executable file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserAdminType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Adresse email',
|
||||
])
|
||||
->add('role', ChoiceType::class, [
|
||||
'label' => 'Rôle',
|
||||
'choices' => [
|
||||
'Utilisateur' => 'ROLE_USER',
|
||||
'Administrateur' => 'ROLE_ADMIN',
|
||||
],
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
|
||||
// Si l'utilisateur est nouveau, on ajoute le champ de mot de passe
|
||||
if (!$options['data']->getId()) {
|
||||
$builder->add('plainPassword', PasswordType::class, [
|
||||
'label' => 'Mot de passe',
|
||||
'required' => true,
|
||||
'mapped' => false,
|
||||
]);
|
||||
} else {
|
||||
// On set le rôle actuel de l'utilisateur
|
||||
$builder->get('role')->setData($options['data']->getRoles()[0]);
|
||||
}
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
'label' => 'Enregistrer',
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
]);
|
||||
}
|
||||
}
|
11
src/Kernel.php
Executable file
11
src/Kernel.php
Executable file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
}
|
0
src/Repository/.gitignore
vendored
Executable file
0
src/Repository/.gitignore
vendored
Executable file
60
src/Repository/UserRepository.php
Executable file
60
src/Repository/UserRepository.php
Executable file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<User>
|
||||
*/
|
||||
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(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
|
||||
}
|
||||
|
||||
$user->setPassword($newHashedPassword);
|
||||
$this->getEntityManager()->persist($user);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return User[] Returns an array of User objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// 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()
|
||||
// ;
|
||||
// }
|
||||
}
|
23
src/Security/Authentication/AuthenticationSuccessHandler.php
Normal file
23
src/Security/Authentication/AuthenticationSuccessHandler.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
|
||||
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $tokenInterface): Response
|
||||
{
|
||||
$url = $this->urlGenerator->generate('app_home');
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
}
|
18
src/Twig/Extension/BasenameExtension.php
Executable file
18
src/Twig/Extension/BasenameExtension.php
Executable file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\BasenameExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class BasenameExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('basename', [BasenameExtensionRuntime::class, 'basename']),
|
||||
];
|
||||
}
|
||||
}
|
18
src/Twig/Extension/SizeExtension.php
Executable file
18
src/Twig/Extension/SizeExtension.php
Executable file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\SizeExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class SizeExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('show_size', [SizeExtensionRuntime::class, 'showSize']),
|
||||
];
|
||||
}
|
||||
}
|
21
src/Twig/Extension/TimeExtension.php
Executable file
21
src/Twig/Extension/TimeExtension.php
Executable file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\TimeExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class TimeExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
// If your filter generates SAFE HTML, you should add a third
|
||||
// parameter: ['is_safe' => ['html']]
|
||||
// Reference: https://twig.symfony.com/doc/3.x/advanced.html#automatic-escaping
|
||||
new TwigFilter('time_diff', [TimeExtensionRuntime::class, 'timeDiff']),
|
||||
];
|
||||
}
|
||||
}
|
14
src/Twig/Runtime/BasenameExtensionRuntime.php
Executable file
14
src/Twig/Runtime/BasenameExtensionRuntime.php
Executable file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class BasenameExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
|
||||
public function basename($value)
|
||||
{
|
||||
return \basename($value);
|
||||
}
|
||||
}
|
22
src/Twig/Runtime/SizeExtensionRuntime.php
Executable file
22
src/Twig/Runtime/SizeExtensionRuntime.php
Executable file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class SizeExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Inject dependencies if needed
|
||||
}
|
||||
|
||||
public function showSize($value)
|
||||
{
|
||||
$bytes = $value;
|
||||
$size = ['B', 'KB', 'MB', 'GB','TB'];
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
|
||||
return sprintf('%.1f', $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
|
||||
}
|
||||
}
|
40
src/Twig/Runtime/TimeExtensionRuntime.php
Executable file
40
src/Twig/Runtime/TimeExtensionRuntime.php
Executable file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class TimeExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Inject dependencies if needed
|
||||
}
|
||||
|
||||
public function timeDiff($value): string
|
||||
{
|
||||
$now = time();
|
||||
$diff = $now - $value;
|
||||
|
||||
// Moins d'une minute
|
||||
if ($diff < 60) {
|
||||
return 'Il y a ' . $diff . ' seconde' . ($diff > 1 ? 's' : '');
|
||||
}
|
||||
|
||||
// Moins d'une heure
|
||||
if ($diff < 3600) {
|
||||
$minutes = floor($diff / 60);
|
||||
return 'Il y a ' . $minutes . ' minute' . ($minutes > 1 ? 's' : '');
|
||||
}
|
||||
|
||||
// Moins d'un jour
|
||||
if ($diff < 86400) {
|
||||
$hours = floor($diff / 3600);
|
||||
return 'Il y a ' . $hours . ' heure' . ($hours > 1 ? 's' : '');
|
||||
}
|
||||
|
||||
// Plus d'un jour
|
||||
$days = floor($diff / 86400);
|
||||
return 'Il y a ' . $days . ' jour' . ($days > 1 ? 's' : '');
|
||||
}
|
||||
}
|
380
symfony.lock
Executable file
380
symfony.lock
Executable file
@ -0,0 +1,380 @@
|
||||
{
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.13",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.13",
|
||||
"ref": "8d96c0b51591ffc26794d865ba3ee7d193438a83"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine.yaml",
|
||||
"src/Entity/.gitignore",
|
||||
"src/Repository/.gitignore"
|
||||
]
|
||||
},
|
||||
"doctrine/doctrine-migrations-bundle": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.1",
|
||||
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine_migrations.yaml",
|
||||
"migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"oneup/flysystem-bundle": {
|
||||
"version": "4.12",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "4.0",
|
||||
"ref": "3ae1b83985e89138f5443bbc2d9b8c074b497d49"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/oneup_flysystem.yaml"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "9.6",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "9.6",
|
||||
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/apache-pack": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "0f18b4decdf5695d692c1d0dfd65516a07a6adf1"
|
||||
},
|
||||
"files": [
|
||||
"public/.htaccess"
|
||||
]
|
||||
},
|
||||
"symfony/asset-mapper": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
|
||||
},
|
||||
"files": [
|
||||
"assets/app.js",
|
||||
"assets/styles/app.css",
|
||||
"config/packages/asset_mapper.yaml",
|
||||
"importmap.php"
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||
},
|
||||
"files": [
|
||||
"bin/console"
|
||||
]
|
||||
},
|
||||
"symfony/debug-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/debug.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "2.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.4",
|
||||
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||
},
|
||||
"files": [
|
||||
".env",
|
||||
".env.dev"
|
||||
]
|
||||
},
|
||||
"symfony/form": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/csrf.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/cache.yaml",
|
||||
"config/packages/framework.yaml",
|
||||
"config/preload.php",
|
||||
"config/routes/framework.yaml",
|
||||
"config/services.yaml",
|
||||
"public/index.php",
|
||||
"src/Controller/.gitignore",
|
||||
"src/Kernel.php"
|
||||
]
|
||||
},
|
||||
"symfony/mailer": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "4.3",
|
||||
"ref": "09051cfde49476e3c12cd3a0e44289ace1c75a4f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/mailer.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.61",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||
}
|
||||
},
|
||||
"symfony/messenger": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.0",
|
||||
"ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/messenger.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/monolog-bundle": {
|
||||
"version": "3.10",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.7",
|
||||
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/monolog.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/notifier": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.0",
|
||||
"ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/notifier.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/phpunit-bridge": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"bin/phpunit",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/routing.yaml",
|
||||
"config/routes.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/security-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/security.yaml",
|
||||
"config/routes/security.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/stimulus-bundle": {
|
||||
"version": "2.22",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.20",
|
||||
"ref": "41c4285a926752ec2852a5cafa39e7b527c33a38"
|
||||
},
|
||||
"files": [
|
||||
"assets/bootstrap.js",
|
||||
"assets/controllers.json",
|
||||
"assets/controllers/csrf_protection_controller.js",
|
||||
"assets/controllers/hello_controller.js"
|
||||
]
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/translation.yaml",
|
||||
"translations/.gitignore"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/twig.yaml",
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/uid": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||
}
|
||||
},
|
||||
"symfony/ux-icons": {
|
||||
"version": "2.22",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.17",
|
||||
"ref": "803a3bbd5893f9584969ab8670290cdfb6a0a5b5"
|
||||
},
|
||||
"files": [
|
||||
"assets/icons/symfony.svg"
|
||||
]
|
||||
},
|
||||
"symfony/ux-turbo": {
|
||||
"version": "2.22",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.20",
|
||||
"ref": "c85ff94da66841d7ff087c19cbcd97a2df744ef9"
|
||||
}
|
||||
},
|
||||
"symfony/ux-twig-component": {
|
||||
"version": "2.22",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.13",
|
||||
"ref": "67814b5f9794798b885cec9d3f48631424449a01"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/twig_component.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/validator.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.1",
|
||||
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/web_profiler.yaml",
|
||||
"config/routes/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"symfonycasts/tailwind-bundle": {
|
||||
"version": "v0.6.1"
|
||||
},
|
||||
"tales-from-a-dev/flowbite-bundle": {
|
||||
"version": "0.7",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "0.4",
|
||||
"ref": "8c5eef17730535682128557a1016872fd3e81c33"
|
||||
}
|
||||
},
|
||||
"tales-from-a-dev/twig-tailwind-extra": {
|
||||
"version": "0.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "0.2",
|
||||
"ref": "7243ab070ed66198eb82c026684e9b9773e7b64a"
|
||||
}
|
||||
},
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.17.0"
|
||||
}
|
||||
}
|
12
tailwind.config.js
Executable file
12
tailwind.config.js
Executable file
@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./vendor/tales-from-a-dev/flowbite-bundle/templates/**/*.html.twig",
|
||||
"./assets/**/*.js",
|
||||
"./templates/**/*.html.twig",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
15
templates/admin/index.html.twig
Executable file
15
templates/admin/index.html.twig
Executable file
@ -0,0 +1,15 @@
|
||||
{% extends 'base-admin.html.twig' %}
|
||||
|
||||
{% block title %}Le cloud de Camélia-Studio{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-96 flex items-center flex-col justify-center">
|
||||
<h1 class="text-4xl font-bold mb-2 text-gray-900 dark:text-white">Bienvenue sur l'administration</h1>
|
||||
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8">Gérez facilement les accès des membres de Camélia-Studio à l'espace de stockage partagé Kumora. Ajoutez, modifiez ou retirez les utilisateurs en quelques clics.</p>
|
||||
<div class="mb-16">
|
||||
<a href="{{ path('app_files_index') }}" class="text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 dark:bg-gray-800 dark:text-white dark:border-gray-600 font-medium rounded-lg px-8 py-3">
|
||||
Gérer les utilisateurs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
30
templates/admin/user_edit.html.twig
Executable file
30
templates/admin/user_edit.html.twig
Executable file
@ -0,0 +1,30 @@
|
||||
{% extends 'base-admin.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-16 mt-4">
|
||||
<div class="block p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
|
||||
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{% if not isNew %}
|
||||
Edition de l'utilisateur {{ user.email }}
|
||||
{% else %}
|
||||
Création d'un nouvel utilisateur
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div class="mt-4">
|
||||
{{ include('partials/alerts.html.twig') }}
|
||||
</div>
|
||||
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
{% if not isNew %}
|
||||
Edition de l'utilisateur {{ user.email }}
|
||||
{% else %}
|
||||
Création d'un nouvel utilisateur
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
63
templates/admin/user_index.html.twig
Executable file
63
templates/admin/user_index.html.twig
Executable file
@ -0,0 +1,63 @@
|
||||
{% extends 'base-admin.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-16 mt-4">
|
||||
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
|
||||
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Liste des utilisateurs</h5>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_admin_user_create') }}" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Créer un utilisateur</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{{ include('partials/alerts.html.twig') }}
|
||||
</div>
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg mt-4">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Id
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Rôle
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ user.id }}
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
{{ user.email }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ user.roles[0] == 'ROLE_ADMIN' ? 'Administrateur' : 'Utilisateur' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 flex gap-2 light:text-black">
|
||||
<a href="{{ path('app_admin_user_edit', {
|
||||
user: user.id
|
||||
}) }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
|
||||
<a href="{{ path('app_admin_user_delete', {
|
||||
user: user.id
|
||||
}) }}" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
Liste des utilisateurs
|
||||
{% endblock %}
|
||||
|
18
templates/base-admin.html.twig
Executable file
18
templates/base-admin.html.twig
Executable file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="bg-white dark:bg-gray-800 dark:text-white">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kumora - {% block title %}Accueil{% endblock %}</title>
|
||||
<link rel="icon" href="{{ asset('images/favicon.ico') }}">
|
||||
{% block stylesheets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-turbo="false">
|
||||
{% include "partials/navbar-admin.html.twig" %}
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
18
templates/base.html.twig
Executable file
18
templates/base.html.twig
Executable file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="bg-white dark:bg-gray-800 dark:text-white">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kumora - {% block title %}Accueil{% endblock %}</title>
|
||||
<link rel="icon" href="{{ asset('images/favicon.ico') }}">
|
||||
{% block stylesheets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-turbo="false">
|
||||
{% include "partials/navbar.html.twig" %}
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
69
templates/files/index.html.twig
Executable file
69
templates/files/index.html.twig
Executable file
@ -0,0 +1,69 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-16">
|
||||
<h1 class="text-center text-2xl font-bold mt-4">Liste des fichiers</h1>
|
||||
<div class="mt-4">
|
||||
{{ include('partials/alerts.html.twig') }}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{% include 'partials/breadbrumb.html.twig' %}
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-x-auto mt-4">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Nom
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Taille
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Modifié le
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for file in files %}
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th class="px-6 py-4 font-medium light:text-gray-900 whitespace-nowrap flex gap-2">
|
||||
{% if file.type == 'file' %}
|
||||
<twig:ux:icon name="line-md:file-filled" class="w-6 h-6" />
|
||||
{% else %}
|
||||
<twig:ux:icon name="line-md:folder-filled" class="w-6 h-6" />
|
||||
{% endif %}
|
||||
<a class="text-blue-700 dark:text-blue-500 hover:text-blue-900 dark:hover:text-blue-700" href="{{ file.url }}">{{ file.path|basename }}</a>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
{% if file.type == 'file' %}
|
||||
{{ file.size|show_size }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ file.last_modified|time_diff }}
|
||||
</td>
|
||||
<td class="px-6 py-4 flex gap-2 light:text-black">
|
||||
{% if file.type == 'file' %}
|
||||
<a href="{{ file.url }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:download" /></a>
|
||||
<a href="#" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
|
||||
<a href="#" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
|
||||
{% else %}
|
||||
<a href="{{ file.url }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:link" /></a>
|
||||
<a href="#" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
|
||||
<a href="#" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
15
templates/home/index.html.twig
Executable file
15
templates/home/index.html.twig
Executable file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Le cloud de Camélia-Studio{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-96 flex items-center flex-col justify-center">
|
||||
<h1 class="text-4xl font-bold mb-2 text-gray-900 dark:text-white">Bienvenue sur Kumora</h1>
|
||||
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8">Notre espace de stockage en ligne, dédié aux membres de Camélia Studio, pour faciliter le partage de tous les fichiers et ressources de l'association.</p>
|
||||
<div class="mb-16">
|
||||
<a href="{{ path('app_files_index') }}" class="text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 dark:bg-gray-800 dark:text-white dark:border-gray-600 font-medium rounded-lg px-8 py-3">
|
||||
Accéder à mon espace
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
21
templates/partials/alerts.html.twig
Executable file
21
templates/partials/alerts.html.twig
Executable file
@ -0,0 +1,21 @@
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
{% if label == 'success' %}
|
||||
<div class="border border-green-300 dark:border-green-800 p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% elseif label == 'info' %}
|
||||
<div class="border border-blue-300 dark:border-blue-800 p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% elseif label == 'error' %}
|
||||
<div class="border border-red-300 dark:border-red-800 p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% elseif label == 'warning' %}
|
||||
<div class="border border-yellow-300 dark:border-yellow-800 p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
33
templates/partials/breadbrumb.html.twig
Executable file
33
templates/partials/breadbrumb.html.twig
Executable file
@ -0,0 +1,33 @@
|
||||
<nav class="flex" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-2 rtl:space-x-reverse {{ path == '' ? 'my-1' : '' }}">
|
||||
<li class="flex items-center">
|
||||
<a href="{{ path('app_files_index') }}"
|
||||
class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white">
|
||||
<twig:ux:icon class="w-3 h-3" name="fa6-solid:house"/>
|
||||
</a>
|
||||
</li>
|
||||
{% if path != '' %}
|
||||
{% set pathSplitted = path|split('/') %}
|
||||
{% for pa in pathSplitted %}
|
||||
{% if loop.last %}
|
||||
<li aria-current="page">
|
||||
<div class="flex items-center">
|
||||
<twig:ux:icon class="rtl:rotate-180 w-3 h-3 text-gray-400 mx-1"
|
||||
name="fa6-solid:chevron-right"/>
|
||||
<span class="ms-1 text-sm font-medium text-gray-500 md:ms-2 dark:text-gray-400">{{ pa }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<twig:ux:icon class="rtl:rotate-180 w-3 h-3 text-gray-400 mx-1"
|
||||
name="fa6-solid:chevron-right"/>
|
||||
<a href="#"
|
||||
class="ms-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ms-2 dark:text-gray-400 dark:hover:text-white">{{ pa }}</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
28
templates/partials/navbar-admin.html.twig
Executable file
28
templates/partials/navbar-admin.html.twig
Executable file
@ -0,0 +1,28 @@
|
||||
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a href="{{ path('app_home') }}" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="{{ asset('images/logo.png') }}" class="h-8" alt="Logo Camélia-Studio" />
|
||||
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Kumora</span>
|
||||
</a>
|
||||
<button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="hidden w-full md:block md:w-auto" id="navbar-default">
|
||||
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
|
||||
<li>
|
||||
<a href="{{ path('app_admin_index') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Administration</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('app_admin_user_index') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Gestion des utilisateurs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('app_home') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Retour à l'accueil</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
39
templates/partials/navbar.html.twig
Executable file
39
templates/partials/navbar.html.twig
Executable file
@ -0,0 +1,39 @@
|
||||
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a href="{{ path('app_home') }}" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="{{ asset('images/logo.png') }}" class="h-8" alt="Logo Camélia-Studio" />
|
||||
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Kumora</span>
|
||||
</a>
|
||||
<button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="hidden w-full md:block md:w-auto" id="navbar-default">
|
||||
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
|
||||
<li>
|
||||
<a href="{{ path('app_home') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Accueil</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('app_files_index') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Liste des fichiers</a>
|
||||
</li>
|
||||
{% if not is_granted('IS_AUTHENTICATED_FULLY') %}
|
||||
<li>
|
||||
<a href="{{ path('app_login') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Se connecter</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<li>
|
||||
<a href="{{ path('app_admin_index') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Administration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('app_logout') }}" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Se déconnecter</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
36
templates/security/login.html.twig
Executable file
36
templates/security/login.html.twig
Executable file
@ -0,0 +1,36 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Log in!{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="container mx-auto px-16 mt-4">
|
||||
<div class="block p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
|
||||
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Se connecter</h3>
|
||||
|
||||
|
||||
|
||||
<form method="post">
|
||||
{% if error %}
|
||||
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-800 dark:text-white dark:font-bold" role="alert">
|
||||
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-5">
|
||||
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Email</label>
|
||||
<input type="email" value="{{ last_username }}" name="_username" id="username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="kumora@camelia-studio.org" required />
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Mot de passe</label>
|
||||
<input autocomplete="current-password" type="password" name="_password" id="password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" data-controller="csrf-protection"
|
||||
value="{{ csrf_token('authenticate') }}"
|
||||
>
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Se connecter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
11
tests/bootstrap.php
Executable file
11
tests/bootstrap.php
Executable file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) {
|
||||
require dirname(__DIR__).'/config/bootstrap.php';
|
||||
} elseif (method_exists(Dotenv::class, 'bootEnv')) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
}
|
0
translations/.gitignore
vendored
Executable file
0
translations/.gitignore
vendored
Executable file
0
uploads/.gitkeep
Executable file
0
uploads/.gitkeep
Executable file
Loading…
x
Reference in New Issue
Block a user