✨ Init Symfony
This commit is contained in:
parent
a7ee569fff
commit
58d9b48cd3
41
.env
Normal file
41
.env
Normal 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://app:!ChangeMe!@127.0.0.1:3306/app?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
Normal file
6
.env.test
Normal 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
|
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
###> 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
|
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.
|
@ -1,3 +0,0 @@
|
||||
# Kumora
|
||||
|
||||
Le disque nuagique de l'association, afin que ses membres puissent échanger en toute simplicité.
|
10
assets/app.js
Normal file
10
assets/app.js
Normal 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
Normal file
5
assets/bootstrap.js
vendored
Normal 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
Normal file
15
assets/controllers.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
60
assets/controllers/csrf_protection_controller.js
Normal file
60
assets/controllers/csrf_protection_controller.js
Normal 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';
|
16
assets/controllers/hello_controller.js
Normal file
16
assets/controllers/hello_controller.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* This is an example Stimulus controller!
|
||||
*
|
||||
* Any element with a data-controller="hello" attribute will cause
|
||||
* this controller to be executed. The name "hello" comes from the filename:
|
||||
* hello_controller.js -> "hello"
|
||||
*
|
||||
* Delete this file or adapt it for your use!
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
||||
}
|
||||
}
|
3
assets/styles/app.css
Normal file
3
assets/styles/app.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: skyblue;
|
||||
}
|
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';
|
||||
}
|
18
compose.override.yaml
Normal file
18
compose.override.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
ports:
|
||||
- "5432"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
mailer:
|
||||
image: axllent/mailpit
|
||||
ports:
|
||||
- "1025"
|
||||
- "8025"
|
||||
environment:
|
||||
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||
###< symfony/mailer ###
|
25
compose.yaml
Normal file
25
compose.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
services:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
# You should definitely change the password in production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
volumes:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
109
composer.json
Normal file
109
composer.json
Normal file
@ -0,0 +1,109 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.13",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^3.3",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.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",
|
||||
"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.0",
|
||||
"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/serializer": "7.2.*",
|
||||
"symfony/stimulus-bundle": "^2.22",
|
||||
"symfony/string": "7.2.*",
|
||||
"symfony/translation": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/ux-turbo": "^2.22",
|
||||
"symfony/validator": "7.2.*",
|
||||
"symfony/web-link": "7.2.*",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
},
|
||||
"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.5",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/debug-bundle": "7.2.*",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "^7.2",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
}
|
||||
}
|
9853
composer.lock
generated
Normal file
9853
composer.lock
generated
Normal 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
|
||||
]
|
||||
]
|
||||
];
|
||||
?>
|
16
config/bundles.php
Normal file
16
config/bundles.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?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],
|
||||
];
|
11
config/packages/asset_mapper.yaml
Normal file
11
config/packages/asset_mapper.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
framework:
|
||||
asset_mapper:
|
||||
# The paths to make available to the asset mapper.
|
||||
paths:
|
||||
- assets/
|
||||
missing_import_mode: strict
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
asset_mapper:
|
||||
missing_import_mode: warn
|
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal 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
Normal file
11
config/packages/csrf.yaml
Normal 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
Normal file
5
config/packages/debug.yaml
Normal 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
Normal file
54
config/packages/doctrine.yaml
Normal 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
Normal file
6
config/packages/doctrine_migrations.yaml
Normal 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
|
15
config/packages/framework.yaml
Normal file
15
config/packages/framework.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
||||
# 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
Normal file
3
config/packages/mailer.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_DSN)%'
|
29
config/packages/messenger.yaml
Normal file
29
config/packages/messenger.yaml
Normal 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
Normal file
62
config/packages/monolog.yaml
Normal 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
Normal file
12
config/packages/notifier.yaml
Normal 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/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal 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
|
39
config/packages/security.yaml
Normal file
39
config/packages/security.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
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:
|
||||
users_in_memory: { memory: null }
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
|
||||
# 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
|
7
config/packages/translation.yaml
Normal file
7
config/packages/translation.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
providers:
|
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal 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
|
17
config/packages/web_profiler.yaml
Normal file
17
config/packages/web_profiler.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
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
Normal file
5
config/preload.php
Normal 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';
|
||||
}
|
5
config/routes.yaml
Normal file
5
config/routes.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
3
config/routes/security.yaml
Normal file
3
config/routes/security.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
24
config/services.yaml
Normal file
24
config/services.yaml
Normal 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
Normal file
28
importmap.php
Normal 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,
|
||||
],
|
||||
'@hotwired/stimulus' => [
|
||||
'version' => '3.2.2',
|
||||
],
|
||||
'@symfony/stimulus-bundle' => [
|
||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
];
|
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
Normal file
0
migrations/.gitignore
vendored
Normal file
38
phpunit.xml.dist
Normal file
38
phpunit.xml.dist
Normal 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>
|
9
public/index.php
Normal file
9
public/index.php
Normal 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']);
|
||||
};
|
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
11
src/Kernel.php
Normal file
11
src/Kernel.php
Normal 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
Normal file
0
src/Repository/.gitignore
vendored
Normal file
302
symfony.lock
Normal file
302
symfony.lock
Normal file
@ -0,0 +1,302 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"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/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/ux-turbo": {
|
||||
"version": "2.22",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.20",
|
||||
"ref": "c85ff94da66841d7ff087c19cbcd97a2df744ef9"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.17.0"
|
||||
}
|
||||
}
|
17
templates/base.html.twig
Normal file
17
templates/base.html.twig
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||
{% block stylesheets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
11
tests/bootstrap.php
Normal file
11
tests/bootstrap.php
Normal 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
Normal file
0
translations/.gitignore
vendored
Normal file
Loading…
x
Reference in New Issue
Block a user