Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
332a1a4fa9 | |||
9e7b35fdd9 | |||
13ba510d55 | |||
0fa3b88e08 | |||
51ddf6510e | |||
08cffee1c1 | |||
2bd4a99da1 | |||
93830df031 | |||
9073737e36 | |||
a4ffaca6a6 | |||
dd6105e8e9 | |||
0aecfd62eb | |||
7141c8d1f3 | |||
5651251da7 | |||
6e30de336f | |||
8c3c1e3216 | |||
a545af7edb | |||
cb09b879f9 | |||
1c9a9cab3e | |||
6ad4845bca | |||
4173fd53f1 | |||
e42d09b788 | |||
f762a5a5a1 | |||
fd0f74bdad | |||
f3dcd7b37c | |||
7b87aef501 | |||
e5f7d5a721 | |||
b7a73e42ee | |||
896c6e12e0 | |||
f071fb8b9a | |||
b968006b4d | |||
62fe712f0b | |||
2568db1736 | |||
913dcae23a | |||
d79fe72155 | |||
c268f8873d | |||
c3df560fc7 | |||
1289186463 | |||
e8554bcecc | |||
f6572696f4 | |||
087e26f417 | |||
7330349a5a | |||
3eda535868 | |||
a71184f9f1 | |||
9ecc72628c | |||
2b52d18da5 | |||
1ded9cf762 | |||
3a8fc71eb2 | |||
72ee30cc01 | |||
9d2aa6a2e4 | |||
7d0b43aa4d | |||
d52dedd707 | |||
6b56a34c6d | |||
062c5f5895 | |||
38f7355d42 | |||
14c6ce39ab | |||
c925421381 | |||
333d00b2a0 | |||
4e39f6cb12 | |||
db0eb5a3fb | |||
e548bc1820 | |||
38283bcf73 | |||
9064bba152 | |||
65d7685e72 | |||
ad642ce6ac | |||
ffd94e1ae6 | |||
d6e1679aba | |||
6e140f84f7 | |||
4cab96f9b6 | |||
f858359a51 | |||
3c4be32c6e | |||
15b33a11d7 | |||
43c8f9af85 | |||
99166674e4 | |||
6763ed4d1e | |||
800dbc2686 | |||
327c354ac4 | |||
7c574fc9a5 | |||
5f14e5f079 | |||
aad06bc2c5 | |||
9d8982c3f8 | |||
df1b0268c6 | |||
e4c7b7004d |
@ -5,3 +5,4 @@ vendor/
|
||||
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
node_modules/
|
13
.env
13
.env
@ -15,8 +15,9 @@
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_ENV=prod
|
||||
APP_SECRET=
|
||||
APP_DEBUG=false
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
@ -37,5 +38,13 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
MAILER_DSN=sendmail://default
|
||||
###< symfony/mailer ###
|
||||
|
||||
BASE_PREFIX=kumora
|
||||
DEFAULT_IMAGE=https://camelia-studio.org/v5/images/camelia_studio.png
|
||||
APP_VERSION=1.0.0
|
||||
GIT_URL=https://git.crystalyx.net/camelia-studio/Kumora
|
||||
CAMELIA_URL=https://camelia-studio.org/
|
||||
TSUBAKIMONO_URL=https://tsubakimono.camelia-studio.org/
|
||||
DISCORD_URL=https://discord.gg/nBuZ9vJ
|
||||
|
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push with Kaniko
|
||||
uses: aevea/action-kaniko@master
|
||||
|
17
.gitea/workflows/php-cs-fixer.yml
Normal file
17
.gitea/workflows/php-cs-fixer.yml
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
name: Apply PHP CS Fixer
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
php-cs-fixer:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Run PHP CS Fixer
|
||||
uses: docker://oskarstark/php-cs-fixer-ga@sha256:5cfb9c812528bbafd614ca16a7d48e7ab35421d930cab611e815d6bfe8f07bfa
|
||||
with:
|
||||
args: --diff --dry-run
|
||||
|
21
.gitea/workflows/phpstan.yml
Normal file
21
.gitea/workflows/phpstan.yml
Normal file
@ -0,0 +1,21 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup PHP with pre-release PECL extension
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
- name: Download Composer
|
||||
run: php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php composer-setup.php && php -r "unlink('composer-setup.php');"
|
||||
- name: Install Dependencies
|
||||
run: php composer.phar install
|
||||
- name: Run PHPStan
|
||||
run: php vendor/bin/phpstan
|
||||
|
18
.gitea/workflows/rector.yml
Normal file
18
.gitea/workflows/rector.yml
Normal file
@ -0,0 +1,18 @@
|
||||
on: [push, pull_request]
|
||||
name: rector
|
||||
jobs:
|
||||
rector:
|
||||
name: Rector
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup PHP with pre-release PECL extension
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
- name: Download Composer
|
||||
run: php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php composer-setup.php && php -r "unlink('composer-setup.php');"
|
||||
- name: Install Dependencies
|
||||
run: php composer.phar install
|
||||
- name: Run Rector
|
||||
run: vendor/bin/rector --dry-run
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -34,3 +34,12 @@
|
||||
!uploads/.gitkeep
|
||||
|
||||
/public/kumora/
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> phpstan/phpstan ###
|
||||
phpstan.neon
|
||||
###< phpstan/phpstan ###
|
||||
node_modules/
|
103
.php-cs-fixer.dist.php
Normal file
103
.php-cs-fixer.dist.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of PHP CS Fixer.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->ignoreVCSIgnored(true)
|
||||
->in(__DIR__)
|
||||
->append([
|
||||
__DIR__ . '/dev-tools/doc.php',
|
||||
// __DIR__.'/php-cs-fixer', disabled, as we want to be able to run bootstrap file even on lower PHP version, to show nice message
|
||||
__FILE__,
|
||||
])
|
||||
;
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
$config
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PHP82Migration' => true,
|
||||
'@PHP80Migration:risky' => true,
|
||||
'@PhpCsFixer:risky' => true,
|
||||
'@PSR1' => true,
|
||||
'@PSR2' => true,
|
||||
'@PSR12' => true,
|
||||
'align_multiline_comment' => [
|
||||
'comment_type' => 'phpdocs_only',
|
||||
],
|
||||
'array_indentation' => true,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
// Anciennement 'braces'
|
||||
'single_space_around_construct' => true,
|
||||
'control_structure_braces' => true,
|
||||
'control_structure_continuation_position' => true,
|
||||
'declare_parentheses' => true,
|
||||
'no_multiple_statements_per_line' => true,
|
||||
'braces_position' => true,
|
||||
'statement_indentation' => true,
|
||||
|
||||
'compact_nullable_type_declaration' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'doctrine_annotation_array_assignment' => [
|
||||
'operator' => '=',
|
||||
],
|
||||
'doctrine_annotation_spaces' => [
|
||||
'after_array_assignments_equals' => false,
|
||||
'before_array_assignments_equals' => false,
|
||||
],
|
||||
'php_unit_internal_class' => false,
|
||||
'php_unit_test_class_requires_covers' => false,
|
||||
'no_extra_blank_lines' => [
|
||||
'tokens' => [
|
||||
'break',
|
||||
'continue',
|
||||
'curly_brace_block',
|
||||
'extra',
|
||||
'parenthesis_brace_block',
|
||||
'return',
|
||||
'square_brace_block',
|
||||
'throw',
|
||||
'use',
|
||||
],
|
||||
],
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'ordered_imports' => [
|
||||
'imports_order' => [
|
||||
'class',
|
||||
'function',
|
||||
'const',
|
||||
],
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'php_unit_method_casing' => [
|
||||
'case' => 'camel_case',
|
||||
],
|
||||
'phpdoc_order' => true,
|
||||
'phpdoc_separation' => true,
|
||||
'phpdoc_trim_consecutive_blank_line_separation' => true,
|
||||
'phpdoc_types_order' => ['null_adjustment' => 'always_last'],
|
||||
'strict_comparison' => true,
|
||||
'strict_param' => true,
|
||||
'yoda_style' => true,
|
||||
'modernize_strpos' => true, // needs PHP 8+ or polyfill
|
||||
'native_constant_invocation' => false,
|
||||
'php_unit_strict' => false,
|
||||
'native_function_invocation' => false,
|
||||
])
|
||||
->setFinder($finder)
|
||||
;
|
||||
|
||||
return $config;
|
@ -1,4 +1,4 @@
|
||||
FROM dunglas/frankenphp
|
||||
FROM dunglas/frankenphp@sha256:bc16b2c6900748ffd951b751a0798dba6a13ffa22ed4c793cf460ca0be4bc446
|
||||
|
||||
ENV SERVER_NAME=":80"
|
||||
|
||||
|
@ -6,5 +6,4 @@ import './bootstrap.js';
|
||||
* 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! 🎉');
|
||||
import 'flowbite';
|
||||
|
@ -1,5 +1,14 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-dropzone": {
|
||||
"dropzone": {
|
||||
"enabled": true,
|
||||
"fetch": "eager",
|
||||
"autoimport": {
|
||||
"@symfony/ux-dropzone/dist/style.min.css": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
|
BIN
assets/images/favicon.ico
Executable file → Normal file
BIN
assets/images/favicon.ico
Executable file → Normal file
Binary file not shown.
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 15 KiB |
BIN
assets/images/logo-kumora.png
Normal file
BIN
assets/images/logo-kumora.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 KiB |
@ -1,3 +1,28 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import "flowbite/src/themes/default";
|
||||
@plugin "flowbite/plugin";
|
||||
|
||||
@source '../../vendor/tales-from-a-dev/flowbite-bundle/templates/**/*.html.twig';
|
||||
@source "../node_modules/flowbite";
|
||||
|
||||
@custom-variant dark (@media (prefers-color-scheme: dark));
|
||||
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
@ -7,15 +7,15 @@
|
||||
"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",
|
||||
"doctrine/dbal": "^4.2.2",
|
||||
"doctrine/doctrine-bundle": "^2.13.2",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4.1",
|
||||
"doctrine/orm": "^3.3.2",
|
||||
"league/flysystem": "^3.29.1",
|
||||
"oneup/flysystem-bundle": "^4.12.3",
|
||||
"oneup/flysystem-bundle": "^4.12.4",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.1",
|
||||
"phpstan/phpdoc-parser": "^2.0",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"phpstan/phpdoc-parser": "^2.0.2",
|
||||
"symfony/apache-pack": "^1.0.1",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/asset-mapper": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
@ -38,25 +38,28 @@
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/security-csrf": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/stimulus-bundle": "^2.22.1",
|
||||
"symfony/stimulus-bundle": "^2.23.0",
|
||||
"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/ux-dropzone": "^2.23.0",
|
||||
"symfony/ux-icons": "^2.23",
|
||||
"symfony/ux-turbo": "^2.23.0",
|
||||
"symfony/ux-twig-component": "^2.23.0",
|
||||
"symfony/validator": "7.2.*",
|
||||
"symfony/web-link": "7.2.*",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"symfonycasts/tailwind-bundle": "^0.6.1",
|
||||
"symfonycasts/reset-password-bundle": "^1.23.1",
|
||||
"symfonycasts/tailwind-bundle": "^0.7.1",
|
||||
"tales-from-a-dev/flowbite-bundle": "^0.7.1",
|
||||
"twig/extra-bundle": "^2.12|^3.18",
|
||||
"twig/twig": "^2.12|^3.18"
|
||||
"twig/extra-bundle": "^2.12|^3.20",
|
||||
"twig/twig": "^2.12|^3.20"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"phpstan/extension-installer": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
@ -106,11 +109,16 @@
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6.22",
|
||||
"friendsofphp/php-cs-fixer": "^3.69.1",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.3",
|
||||
"phpstan/phpstan-symfony": "^2.0.2",
|
||||
"phpunit/phpunit": "^11.5.8",
|
||||
"rector/rector": "^2.0.9",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/debug-bundle": "7.2.*",
|
||||
"symfony/maker-bundle": "^1.61",
|
||||
"symfony/maker-bundle": "^1.62.1",
|
||||
"symfony/phpunit-bridge": "^7.2",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
|
2579
composer.lock
generated
Executable file → Normal file
2579
composer.lock
generated
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
@ -19,4 +21,6 @@ return [
|
||||
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||
TalesFromADev\Twig\Extra\Tailwind\Bridge\Symfony\Bundle\TalesFromADevTwigExtraTailwindBundle::class => ['all' => true],
|
||||
TalesFromADev\FlowbiteBundle\TalesFromADevFlowbiteBundle::class => ['all' => true],
|
||||
Symfony\UX\Dropzone\DropzoneBundle::class => ['all' => true],
|
||||
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
||||
];
|
||||
|
@ -4,7 +4,7 @@ framework:
|
||||
paths:
|
||||
- assets/
|
||||
missing_import_mode: strict
|
||||
public_prefix: /kumora/assets
|
||||
public_prefix: "%env(BASE_PREFIX)%/assets"
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
|
@ -1,6 +1,10 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
mapping_types:
|
||||
uuid: string
|
||||
types:
|
||||
uuid: Symfony\Bridge\Doctrine\Types\UuidType
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
|
@ -13,7 +13,7 @@ framework:
|
||||
max_retries: 3
|
||||
multiplier: 2
|
||||
failed: 'doctrine://default?queue_name=failed'
|
||||
# sync: 'sync://'
|
||||
sync: 'sync://'
|
||||
|
||||
default_bus: messenger.bus.default
|
||||
|
||||
@ -21,9 +21,9 @@ framework:
|
||||
messenger.bus.default: []
|
||||
|
||||
routing:
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
||||
Symfony\Component\Notifier\Message\ChatMessage: async
|
||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: sync
|
||||
Symfony\Component\Notifier\Message\ChatMessage: sync
|
||||
Symfony\Component\Notifier\Message\SmsMessage: sync
|
||||
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
|
2
config/packages/reset_password.yaml
Normal file
2
config/packages/reset_password.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
symfonycasts_reset_password:
|
||||
request_password_repository: App\Repository\ResetPasswordRequestRepository
|
2
config/packages/symfonycasts_tailwind.yaml
Normal file
2
config/packages/symfonycasts_tailwind.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
symfonycasts_tailwind:
|
||||
binary_version: 'v4.0.7'
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
declare(strict_types=1);
|
||||
|
||||
if (file_exists(dirname(__DIR__) . '/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__) . '/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ controllers:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
prefix: /kumora
|
||||
prefix: '%base.prefix%'
|
||||
|
@ -4,13 +4,16 @@
|
||||
# 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:
|
||||
base.prefix: '%env(BASE_PREFIX)%'
|
||||
default.image: '%env(DEFAULT_IMAGE)%'
|
||||
|
||||
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.
|
||||
|
||||
bind:
|
||||
string $defaultImage: '%default.image%'
|
||||
# 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\:
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
@ -25,4 +27,17 @@ return [
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '8.0.12',
|
||||
],
|
||||
'flowbite' => [
|
||||
'version' => '2.5.2',
|
||||
],
|
||||
'@popperjs/core' => [
|
||||
'version' => '2.11.8',
|
||||
],
|
||||
'flowbite-datepicker' => [
|
||||
'version' => '1.3.0',
|
||||
],
|
||||
'flowbite/dist/flowbite.min.css' => [
|
||||
'version' => '2.5.2',
|
||||
'type' => 'css',
|
||||
],
|
||||
];
|
||||
|
@ -1,37 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
40
migrations/Version20250122182101.php
Normal file
40
migrations/Version20250122182101.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250122182101 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE reset_password_request (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at DATETIME NOT NULL, expires_at DATETIME NOT NULL, user_id INTEGER NOT NULL, CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('CREATE INDEX IDX_7CE748AA76ED395 ON reset_password_request (user_id)');
|
||||
$this->addSql('CREATE TABLE "user" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(255) NOT NULL, roles CLOB NOT NULL, password VARCHAR(255) NOT NULL)');
|
||||
$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, available_at DATETIME NOT NULL, delivered_at DATETIME DEFAULT NULL)');
|
||||
$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 down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE reset_password_request');
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
$this->addSql('DROP TABLE messenger_messages');
|
||||
}
|
||||
}
|
31
migrations/Version20250122182130.php
Normal file
31
migrations/Version20250122182130.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250122182130 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE parent_directory (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, owner_role VARCHAR(255) NOT NULL)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE parent_directory');
|
||||
}
|
||||
}
|
35
migrations/Version20250122183447.php
Normal file
35
migrations/Version20250122183447.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250122183447 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE user ADD COLUMN folder_role VARCHAR(255) NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, email, roles, password FROM "user"');
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
$this->addSql('CREATE TABLE "user" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(255) NOT NULL, roles CLOB NOT NULL, password VARCHAR(255) NOT NULL)');
|
||||
$this->addSql('INSERT INTO "user" (id, email, roles, password) SELECT id, email, roles, password FROM __temp__user');
|
||||
$this->addSql('DROP TABLE __temp__user');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
|
||||
}
|
||||
}
|
36
migrations/Version20250122192521.php
Normal file
36
migrations/Version20250122192521.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250122192521 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE user ADD COLUMN fullname VARCHAR(255) NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, email, roles, password, folder_role FROM "user"');
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
$this->addSql('CREATE TABLE "user" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(255) NOT NULL, roles CLOB NOT NULL, password VARCHAR(255) NOT NULL, folder_role VARCHAR(255) NOT NULL)');
|
||||
$this->addSql('INSERT INTO "user" (id, email, roles, password, folder_role) SELECT id, email, roles, password, folder_role FROM __temp__user');
|
||||
$this->addSql('DROP TABLE __temp__user');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
|
||||
}
|
||||
}
|
40
migrations/Version20250123225212.php
Normal file
40
migrations/Version20250123225212.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250123225212 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__parent_directory AS SELECT id, name, owner_role FROM parent_directory');
|
||||
$this->addSql('DROP TABLE parent_directory');
|
||||
$this->addSql('CREATE TABLE parent_directory (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, owner_role VARCHAR(255) NOT NULL, user_created_id INTEGER NOT NULL, CONSTRAINT FK_B7336B34F987D8A8 FOREIGN KEY (user_created_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO parent_directory (id, name, owner_role) SELECT id, name, owner_role FROM __temp__parent_directory');
|
||||
$this->addSql('DROP TABLE __temp__parent_directory');
|
||||
$this->addSql('CREATE INDEX IDX_B7336B34F987D8A8 ON parent_directory (user_created_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__parent_directory AS SELECT id, name, owner_role FROM parent_directory');
|
||||
$this->addSql('DROP TABLE parent_directory');
|
||||
$this->addSql('CREATE TABLE parent_directory (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, owner_role VARCHAR(255) NOT NULL)');
|
||||
$this->addSql('INSERT INTO parent_directory (id, name, owner_role) SELECT id, name, owner_role FROM __temp__parent_directory');
|
||||
$this->addSql('DROP TABLE __temp__parent_directory');
|
||||
}
|
||||
}
|
36
migrations/Version20250123230226.php
Normal file
36
migrations/Version20250123230226.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250123230226 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE parent_directory ADD COLUMN is_public BOOLEAN NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__parent_directory AS SELECT id, name, owner_role, user_created_id FROM parent_directory');
|
||||
$this->addSql('DROP TABLE parent_directory');
|
||||
$this->addSql('CREATE TABLE parent_directory (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, owner_role VARCHAR(255) NOT NULL, user_created_id INTEGER NOT NULL, CONSTRAINT FK_B7336B34F987D8A8 FOREIGN KEY (user_created_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO parent_directory (id, name, owner_role, user_created_id) SELECT id, name, owner_role, user_created_id FROM __temp__parent_directory');
|
||||
$this->addSql('DROP TABLE __temp__parent_directory');
|
||||
$this->addSql('CREATE INDEX IDX_B7336B34F987D8A8 ON parent_directory (user_created_id)');
|
||||
}
|
||||
}
|
32
migrations/Version20250126120344.php
Normal file
32
migrations/Version20250126120344.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250126120344 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE parent_directory_permission (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, role VARCHAR(255) NOT NULL, read BOOLEAN NOT NULL, write BOOLEAN NOT NULL, parent_directory_id INTEGER NOT NULL, CONSTRAINT FK_F93986627CFA5BB1 FOREIGN KEY (parent_directory_id) REFERENCES parent_directory (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('CREATE INDEX IDX_F93986627CFA5BB1 ON parent_directory_permission (parent_directory_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE parent_directory_permission');
|
||||
}
|
||||
}
|
297
package-lock.json
generated
Normal file
297
package-lock.json
generated
Normal file
@ -0,0 +1,297 @@
|
||||
{
|
||||
"name": "Kumora",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"flowbite": "^3.1.2",
|
||||
"tailwindcss": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flowbite": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-3.1.2.tgz",
|
||||
"integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"flowbite-datepicker": "^1.3.1",
|
||||
"mini-svg-data-uri": "^1.4.3",
|
||||
"postcss": "^8.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/flowbite-datepicker": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.2.tgz",
|
||||
"integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"flowbite": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/flowbite-datepicker/node_modules/flowbite": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.5.2.tgz",
|
||||
"integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"flowbite-datepicker": "^1.3.0",
|
||||
"mini-svg-data-uri": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz",
|
||||
"integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"flowbite": "^3.1.2",
|
||||
"tailwindcss": "^4.0.8"
|
||||
}
|
||||
}
|
7
phpstan-baseline.neon
Normal file
7
phpstan-baseline.neon
Normal file
@ -0,0 +1,7 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#^Property App\\Entity\\ResetPasswordRequest\:\:\$id \(int\|null\) is never assigned int so it can be removed from the property type\.$#'
|
||||
identifier: property.unusedType
|
||||
count: 1
|
||||
path: src/Entity/ResetPasswordRequest.php
|
21
phpstan.dist.neon
Normal file
21
phpstan.dist.neon
Normal file
@ -0,0 +1,21 @@
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 5
|
||||
paths:
|
||||
- src
|
||||
excludePaths:
|
||||
- tests
|
||||
symfony:
|
||||
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
|
||||
strictRules:
|
||||
noVariableVariables: false
|
||||
ignoreErrors:
|
||||
-
|
||||
identifier: property.unusedType
|
||||
|
||||
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
treatPhpDocTypesAsCertain: false
|
||||
|
@ -1,12 +1,9 @@
|
||||
<?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" />
|
||||
@ -22,17 +19,4 @@
|
||||
<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>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
return static fn (array $context) => new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
32
rector.php
Normal file
32
rector.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Doctrine\Set\DoctrineSetList;
|
||||
use Rector\Set\ValueObject\LevelSetList;
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
use Rector\Symfony\Set\SymfonySetList;
|
||||
|
||||
return static function (RectorConfig $rectorConfig): void {
|
||||
$rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml');
|
||||
|
||||
$rectorConfig->sets([
|
||||
LevelSetList::UP_TO_PHP_82,
|
||||
SymfonySetList::SYMFONY_71,
|
||||
SymfonySetList::SYMFONY_CODE_QUALITY,
|
||||
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
|
||||
SetList::DEAD_CODE,
|
||||
SetList::CODE_QUALITY,
|
||||
SetList::PHP_82,
|
||||
DoctrineSetList::DOCTRINE_CODE_QUALITY,
|
||||
]);
|
||||
$rectorConfig->paths([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
__DIR__ . '/migrations',
|
||||
]);
|
||||
|
||||
$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
|
||||
};
|
@ -1,8 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Enum\RoleEnum;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
@ -24,40 +27,40 @@ class CreateUserCommand extends Command
|
||||
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);
|
||||
$fullname = $io->ask('Nom de l\'utilisateur');
|
||||
$email = $io->ask('Email de l\'utilisateur');
|
||||
$password = $io->askHidden('Mot de passe de l\'utilisateur');
|
||||
$isAdmin = $io->confirm('Est-ce un administrateur ?');
|
||||
$folderRole = $io->choice('Groupe', array_map(static fn ($role) => $role->value, RoleEnum::cases()), RoleEnum::VISITEUR->value);
|
||||
|
||||
try {
|
||||
$user = $this->userRepository->findOneBy(['email' => $email]);
|
||||
if ($user) {
|
||||
if (null !== $user) {
|
||||
$io->error('Un utilisateur existe déjà avec cet email');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setFullname($fullname);
|
||||
$user->setEmail($email);
|
||||
$user->setPassword($this->passwordHasher->hashPassword($user, $password));
|
||||
$user->setRoles($isAdmin ? ['ROLE_ADMIN'] : ['ROLE_USER']);
|
||||
$user->initId();
|
||||
$user->setFolderRole(RoleEnum::from($folderRole));
|
||||
|
||||
$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');
|
||||
$io->error('Une erreur est survenue lors de la création de l\'utilisateur : ' . $e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
@ -40,13 +42,12 @@ class AdminController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
#[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) {
|
||||
if (!$user instanceof \App\Entity\User) {
|
||||
$user = new User();
|
||||
$isNew = true;
|
||||
}
|
||||
@ -58,7 +59,6 @@ class AdminController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$role = $form->get('role')->getData();
|
||||
$user->setRoles([$role]);
|
||||
$user->initId();
|
||||
|
||||
if ($form->has('plainPassword')) {
|
||||
$plainPassword = $form->get('plainPassword')->getData();
|
||||
|
@ -1,54 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\ParentDirectory;
|
||||
use App\Entity\User;
|
||||
use App\Enum\RoleEnum;
|
||||
use App\Form\CreateDirectoryType;
|
||||
use App\Form\FilePermissionType;
|
||||
use App\Form\RenameType;
|
||||
use App\Form\UploadType;
|
||||
use App\Repository\ParentDirectoryRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemReader;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
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
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ParentDirectoryRepository $parentDirectoryRepository,
|
||||
private readonly Filesystem $defaultAdapter,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/', name: 'index')]
|
||||
public function index(Filesystem $defaultAdapter, UrlGeneratorInterface $urlGenerator, #[MapQueryParameter('path')] string $path = ''): Response
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index(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);
|
||||
$path = $this->normalizePath($path);
|
||||
$this->getUser();
|
||||
if ('' !== $path) {
|
||||
$pathExploded = explode('/', $path);
|
||||
|
||||
if ($path !== '' && !$defaultAdapter->directoryExists($path)) {
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $pathExploded[0]]);
|
||||
|
||||
if (null === $parentDir || !$this->defaultAdapter->directoryExists($path)) {
|
||||
throw $this->createNotFoundException("Ce dossier n'existe pas !");
|
||||
}
|
||||
|
||||
if (!$this->isGranted('file_read', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit d'accéder à ce dossier !");
|
||||
}
|
||||
}
|
||||
|
||||
$files = $defaultAdapter->listContents('/' . $path);
|
||||
$files = $this->defaultAdapter->listContents('/' . $path);
|
||||
|
||||
$realFiles = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!str_starts_with($file['path'], '.')) {
|
||||
$filename = basename((string) $file['path']);
|
||||
if (!str_starts_with($filename, '.')) {
|
||||
// On vérifie si l'utilisateur a le droit d'accéder au fichier (vérifier que owner_role du parentDirectory correspondant est bien le folderRole de l'utilisateur)
|
||||
$pathFile = explode('/', (string) $file['path']);
|
||||
if ('' !== $path) {
|
||||
$parentDirectory = $this->parentDirectoryRepository->findOneBy(['name' => $pathFile[0]]);
|
||||
|
||||
if (null === $parentDirectory || !$this->isGranted('file_read', $parentDirectory)) {
|
||||
continue;
|
||||
}
|
||||
} elseif ('file' !== $file['type']) {
|
||||
$parentDirectory = $this->parentDirectoryRepository->findOneBy(['name' => $filename]);
|
||||
|
||||
if (null === $parentDirectory || !$this->isGranted('file_read', $parentDirectory)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$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']]),
|
||||
'size' => $file['fileSize'] ?? $this->calculateSize($file),
|
||||
'url' => 'file' === $file['type']
|
||||
? $this->generateUrl('app_files_app_file_proxy', ['filename' => $file['path'], 'preview' => false], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
: $this->generateUrl('app_files_index', ['path' => $file['path']]),
|
||||
'previewUrl' => 'file' === $file['type']
|
||||
? $this->generateUrl('app_files_app_file_proxy', ['filename' => $file['path'], 'preview' => true], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
: $this->generateUrl('app_files_index', ['path' => $file['path']]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -57,39 +103,445 @@ class FilesController extends AbstractController
|
||||
usort($realFiles, static function ($a, $b) {
|
||||
if ($a['type'] === $b['type']) {
|
||||
return $a['path'] <=> $b['path'];
|
||||
} else {
|
||||
return $a['type'] <=> $b['type'];
|
||||
}
|
||||
return $a['type'] <=> $b['type'];
|
||||
});
|
||||
|
||||
return $this->render('files/index.html.twig', [
|
||||
'files' => $realFiles,
|
||||
'path' => $path,
|
||||
'parentDir' => $parentDir ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/file-proxy', name: 'app_file_proxy')]
|
||||
public function fileProxy(Filesystem $defaultAdapter, #[MapQueryParameter('filename')]string $filename)
|
||||
public function fileProxy(Filesystem $defaultAdapter, #[MapQueryParameter('filename')] string $filename, #[MapQueryParameter('preview')] bool $preview)
|
||||
{
|
||||
$mimetype = $defaultAdapter->mimeType($filename);
|
||||
if ($mimetype === '') {
|
||||
$file = $this->normalizePath($filename);
|
||||
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => explode('/', $file)[0]]);
|
||||
|
||||
if (null === $parentDir) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit d'accéder à ce fichier !");
|
||||
}
|
||||
|
||||
// Si l'owner role sur le parent est visiteur, on peut accéder au fichier sans être connecté
|
||||
if (!$this->isGranted('file_read', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit d'accéder à ce fichier !");
|
||||
}
|
||||
|
||||
$mimetype = $defaultAdapter->mimeType($file);
|
||||
if ('' === $mimetype) {
|
||||
$mimetype = 'application/octet-stream';
|
||||
}
|
||||
|
||||
$response = new StreamedResponse(static function () use ($filename, $defaultAdapter): void {
|
||||
|
||||
$response = new StreamedResponse(static function () use ($file, $defaultAdapter): void {
|
||||
$outputStream = fopen('php://output', 'w');
|
||||
$fileStream = $defaultAdapter->readStream($filename);
|
||||
$fileStream = $defaultAdapter->readStream($file);
|
||||
stream_copy_to_stream($fileStream, $outputStream);
|
||||
});
|
||||
|
||||
$response->headers->set('Content-Type', $mimetype);
|
||||
|
||||
if (!$preview) {
|
||||
$disposition = HeaderUtils::makeDisposition(
|
||||
HeaderUtils::DISPOSITION_ATTACHMENT,
|
||||
basename($filename)
|
||||
basename($file)
|
||||
);
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/file-delete', name: 'delete')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function fileDelete(Filesystem $defaultAdapter, #[MapQueryParameter('filename')] string $filename): RedirectResponse
|
||||
{
|
||||
$this->getUser();
|
||||
$file = $this->normalizePath($filename);
|
||||
|
||||
$realPath = explode('/', $file);
|
||||
$parentDir = null;
|
||||
|
||||
if (count($realPath) > 1) {
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
|
||||
|
||||
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit de supprimer ce fichier !");
|
||||
}
|
||||
}
|
||||
|
||||
if ('' !== $file && !str_starts_with($file, '.') && $defaultAdapter->fileExists($file)) {
|
||||
$defaultAdapter->delete($file);
|
||||
|
||||
$this->addFlash('success', 'Le fichier a bien été supprimé.');
|
||||
} else {
|
||||
$this->addFlash('error', 'Le fichier n\'existe pas.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => dirname($file),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/directory-delete', name: 'delete_directory')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function directoryDelete(Filesystem $defaultAdapter, #[MapQueryParameter('path')] string $path): RedirectResponse
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$this->getUser();
|
||||
|
||||
$realPath = explode('/', $path);
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
|
||||
|
||||
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit de supprimer ce dossier !");
|
||||
}
|
||||
|
||||
if ('' !== $path && !str_starts_with($path, '.') && $defaultAdapter->directoryExists($path)) {
|
||||
$defaultAdapter->deleteDirectory($path);
|
||||
if ($parentDir->getName() === $path) {
|
||||
$this->entityManager->remove($parentDir);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'Le dossier a bien été supprimé.');
|
||||
} else {
|
||||
$this->addFlash('error', 'Le dossier n\'existe pas.');
|
||||
}
|
||||
|
||||
$newPath = dirname($path);
|
||||
|
||||
if ('.' === $newPath) {
|
||||
$newPath = '';
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => $newPath,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/rename', name: 'rename')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function rename(#[MapQueryParameter('path')] string $filepath, Request $request, Filesystem $defaultAdapter): Response
|
||||
{
|
||||
$filepath = $this->normalizePath($filepath);
|
||||
$this->getUser();
|
||||
|
||||
if ('' === $filepath || str_starts_with($filepath, '.') || !$defaultAdapter->fileExists($filepath)) {
|
||||
throw $this->createNotFoundException("Ce fichier n'existe pas !");
|
||||
}
|
||||
|
||||
$realPath = explode('/', $filepath);
|
||||
|
||||
if (count($realPath) > 1) {
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
|
||||
|
||||
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit de renommer ce fichier !");
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'newName' => pathinfo($filepath, PATHINFO_BASENAME),
|
||||
];
|
||||
$form = $this->createForm(RenameType::class, $data);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$data = $form->getData();
|
||||
|
||||
$newName = $data['newName'];
|
||||
|
||||
$newPath = dirname($filepath) . '/' . $newName;
|
||||
|
||||
$defaultAdapter->move($filepath, $newPath);
|
||||
|
||||
$this->addFlash('success', 'Le fichier a bien été renommé.');
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => dirname($filepath),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('files/rename.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'filepath' => $filepath,
|
||||
'type' => 'fichier',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/create-directory', name: 'create_directory')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function createDirectory(Request $request, Filesystem $defaultAdapter, #[MapQueryParameter('base')] string $basePath): Response
|
||||
{
|
||||
$basePath = $this->normalizePath($basePath);
|
||||
$realPath = explode('/', $basePath);
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->getUser();
|
||||
|
||||
if (count($realPath) > 1) {
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
|
||||
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit de créer de sous-dossier dans ce dossier !");
|
||||
}
|
||||
}
|
||||
$form = $this->createForm(CreateDirectoryType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$data = $form->getData();
|
||||
|
||||
$name = $data['name'];
|
||||
|
||||
if (count(explode('/', (string) $name)) > 1) {
|
||||
$name = explode('/', (string) $name)[0];
|
||||
}
|
||||
|
||||
if ($defaultAdapter->directoryExists($basePath . '/' . $name)) {
|
||||
$this->addFlash('error', 'Le dossier existe déjà.');
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => $basePath,
|
||||
]);
|
||||
}
|
||||
|
||||
$defaultAdapter->createDirectory($basePath . '/' . $name);
|
||||
|
||||
$defaultAdapter->write($basePath . '/' . $name . '/.gitkeep', '');
|
||||
|
||||
// si basePath est vide, on crée un parentDirectory
|
||||
if ('' === $basePath) {
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->getUser();
|
||||
$parentDirectory = new ParentDirectory();
|
||||
$parentDirectory->setName($name);
|
||||
$parentDirectory->setOwnerRole($user->getFolderRole());
|
||||
$parentDirectory->setIsPublic(false);
|
||||
$parentDirectory->setUserCreated($user);
|
||||
|
||||
$this->entityManager->persist($parentDirectory);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'Le dossier a bien été créé.');
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => $basePath,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('files/create_directory.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'basePath' => $basePath,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/rename-directory', name: 'rename-directory')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function renameDirectory(#[MapQueryParameter('path')] string $filepath, Request $request, Filesystem $defaultAdapter): Response
|
||||
{
|
||||
$filepath = $this->normalizePath($filepath);
|
||||
|
||||
$realPath = explode('/', $filepath);
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
|
||||
|
||||
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit de renommer ce dossier !");
|
||||
}
|
||||
|
||||
if ('' === $filepath || str_starts_with($filepath, '.') || !$defaultAdapter->directoryExists($filepath)) {
|
||||
throw $this->createNotFoundException("Ce dossier n'existe pas !");
|
||||
}
|
||||
|
||||
$data = [
|
||||
'newName' => pathinfo($filepath, PATHINFO_BASENAME),
|
||||
];
|
||||
$form = $this->createForm(RenameType::class, $data);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$data = $form->getData();
|
||||
|
||||
$newName = $data['newName'];
|
||||
|
||||
$newPath = dirname($filepath) . '/' . $newName;
|
||||
|
||||
// Si c'est un parent directory, on renomme le parent directory dans la base de données
|
||||
if ($parentDir->getName() === $filepath) {
|
||||
$parentDir->setName($newName);
|
||||
$this->entityManager->persist($parentDir);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$defaultAdapter->move($filepath, $newPath);
|
||||
|
||||
$this->addFlash('success', 'Le dossier a bien été renommé.');
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => dirname($filepath),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('files/rename.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'filepath' => $filepath,
|
||||
'type' => 'dossier',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
#[Route('/upload', name: 'upload')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function upload(#[MapQueryParameter('path')] string $path, Request $request, Filesystem $defaultAdapter): Response
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
$this->getUser();
|
||||
|
||||
if ('' === $path) {
|
||||
throw $this->createNotFoundException("Vous ne pouvez pas téléverser de fichier à la racine !");
|
||||
}
|
||||
|
||||
$realPath = explode('/', $path);
|
||||
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $realPath[0]]);
|
||||
|
||||
if (null === $parentDir || !$this->isGranted('file_write', $parentDir)) {
|
||||
throw $this->createNotFoundException("Vous n'avez pas le droit de téléverser des fichiers dans ce dossier !");
|
||||
}
|
||||
|
||||
$form = $this->createForm(UploadType::class);
|
||||
|
||||
if (!$defaultAdapter->directoryExists($path)) {
|
||||
throw $this->createNotFoundException("Ce dossier n'existe pas !");
|
||||
}
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$data = $form->getData();
|
||||
$files = $data['files'];
|
||||
|
||||
/**
|
||||
* @var UploadedFile $file
|
||||
*/
|
||||
foreach ($files as $file) {
|
||||
$filename = $file->getClientOriginalName();
|
||||
$defaultAdapter->write($path . '/' . $filename, $file->getContent());
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'Les ' . count($files) . ' fichiers ont bien été envoyés.');
|
||||
|
||||
return $this->redirectToRoute('app_files_index', [
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('files/upload.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
// On retire les slashs en début et fin de chaîne
|
||||
$path = trim($path, '/');
|
||||
// On retire les chemins relatifs
|
||||
$path = str_replace('..', '', $path);
|
||||
// On retire les . qui sont seul dans la chaîne, en vérifiant qu'il n'y a pas de lettre avant ou après
|
||||
$path = preg_replace('/(?<!\w)\.(?!\w)/', '', $path);
|
||||
|
||||
return str_replace('//', '/', $path);
|
||||
}
|
||||
|
||||
#[Route('/file-edit-permission/{parentDir}', name: 'file_edit_permission')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function fileRead(#[MapEntity(mapping: ['parentDir' => 'name'])] ParentDirectory $parentDir, Request $request): Response
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->getUser();
|
||||
|
||||
// 2 possibilités : soit l'utilisateur est le créateur du dossier, soit le dossier est public et l'utilisateur a le role Conseil d'administration
|
||||
// Si ce n'est pas le cas, on redirige vers la page d'accueil
|
||||
if ($parentDir->getUserCreated() !== $user) {
|
||||
if ($parentDir->isPublic() && RoleEnum::CONSEIL_ADMINISTRATION !== $user->getFolderRole()) {
|
||||
$this->addFlash('error', 'Vous n\'avez pas le droit de modifier les permissions de ce dossier.');
|
||||
return $this->redirectToRoute('app_files_index');
|
||||
} elseif (!$parentDir->isPublic()) {
|
||||
$this->addFlash('error', 'Vous n\'avez pas le droit de modifier les permissions de ce dossier.');
|
||||
return $this->redirectToRoute('app_files_index');
|
||||
}
|
||||
}
|
||||
|
||||
$form = $this->createForm(FilePermissionType::class, $parentDir);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$datas = $form->getData();
|
||||
|
||||
foreach ($datas->getParentDirectoryPermissions() as $parentPerm) {
|
||||
$this->entityManager->persist($parentPerm);
|
||||
}
|
||||
$this->entityManager->persist($datas);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Les permissions ont bien été modifiées.');
|
||||
|
||||
return $this->redirectToRoute('app_files_index');
|
||||
}
|
||||
|
||||
return $this->render('files/file_edit.html.twig', [
|
||||
'parentDir' => $parentDir,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function calculateSize($file): int
|
||||
{
|
||||
$folderPath = $file['path'];
|
||||
// On récupère tout les fichiers dans le dossier
|
||||
$files = $this->defaultAdapter->listContents($folderPath, true);
|
||||
|
||||
$size = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$size += $file['fileSize'] ?? 0;
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
108
src/Controller/ProfileController.php
Normal file
108
src/Controller/ProfileController.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DTO\EmailDTO;
|
||||
use App\DTO\PasswordDTO;
|
||||
use App\Entity\User;
|
||||
use App\Form\EmailFormType;
|
||||
use App\Form\PasswordFormType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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;
|
||||
|
||||
class ProfileController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/profile', name: 'app_profile')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index()
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->getUser();
|
||||
|
||||
return $this->render('profile/index.html.twig', [
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/profile/edit/email', name: 'app_profile_email_edit')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function editEmail(Request $request): Response
|
||||
{
|
||||
$emailDTO = new EmailDTO();
|
||||
$form = $this->createForm(EmailFormType::class, $emailDTO);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->getUser();
|
||||
|
||||
if ($this->passwordHasher->isPasswordValid($user, $emailDTO->password)) {
|
||||
$user->setEmail($emailDTO->email);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre adresse email a bien été modifiée.');
|
||||
|
||||
return $this->redirectToRoute('app_profile');
|
||||
}
|
||||
|
||||
$this->addFlash('error', 'Le mot de passe est incorrect.');
|
||||
}
|
||||
|
||||
return $this->render('profile/edit_email.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[Route('/profile/edit/password', name: 'app_profile_password_edit')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function editPassword(Request $request): Response
|
||||
{
|
||||
$passwordDTO = new PasswordDTO();
|
||||
$form = $this->createForm(PasswordFormType::class, $passwordDTO);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->getUser();
|
||||
|
||||
if ($this->passwordHasher->isPasswordValid($user, $passwordDTO->password)) {
|
||||
$user->setPassword($this->passwordHasher->hashPassword($user, $passwordDTO->newPassword));
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre mot de passe a bien été modifiée.');
|
||||
|
||||
return $this->redirectToRoute('app_profile');
|
||||
}
|
||||
|
||||
$this->addFlash('error', 'Le mot de passe est incorrect.');
|
||||
}
|
||||
|
||||
return $this->render('profile/edit_password.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
181
src/Controller/ResetPasswordController.php
Normal file
181
src/Controller/ResetPasswordController.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\ChangePasswordFormType;
|
||||
use App\Form\ResetPasswordRequestFormType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
||||
|
||||
#[Route('/reset-password')]
|
||||
class ResetPasswordController extends AbstractController
|
||||
{
|
||||
use ResetPasswordControllerTrait;
|
||||
|
||||
public function __construct(
|
||||
private ResetPasswordHelperInterface $resetPasswordHelper,
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display & process form to request a password reset.
|
||||
*/
|
||||
#[Route('', name: 'app_forgot_password_request')]
|
||||
public function request(Request $request, MailerInterface $mailer, TranslatorInterface $translator): Response
|
||||
{
|
||||
$form = $this->createForm(ResetPasswordRequestFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var string $email */
|
||||
$email = $form->get('email')->getData();
|
||||
|
||||
return $this->processSendingPasswordResetEmail(
|
||||
$email,
|
||||
$mailer
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('reset_password/request.html.twig', [
|
||||
'requestForm' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation page after a user has requested a password reset.
|
||||
*/
|
||||
#[Route('/check-email', name: 'app_check_email')]
|
||||
public function checkEmail(): Response
|
||||
{
|
||||
// Generate a fake token if the user does not exist or someone hit this page directly.
|
||||
// This prevents exposing whether or not a user was found with the given email address or not
|
||||
if (!$resetToken = $this->getTokenObjectFromSession() instanceof \SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken) {
|
||||
$resetToken = $this->resetPasswordHelper->generateFakeResetToken();
|
||||
}
|
||||
|
||||
return $this->render('reset_password/check_email.html.twig', [
|
||||
'resetToken' => $resetToken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and process the reset URL that the user clicked in their email.
|
||||
*/
|
||||
#[Route('/reset/{token}', name: 'app_reset_password')]
|
||||
public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, string $token = null): Response
|
||||
{
|
||||
if (null !== $token) {
|
||||
// We store the token in session and remove it from the URL, to avoid the URL being
|
||||
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
|
||||
$this->storeTokenInSession($token);
|
||||
|
||||
return $this->redirectToRoute('app_reset_password');
|
||||
}
|
||||
|
||||
$token = $this->getTokenFromSession();
|
||||
if (null === $token) {
|
||||
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var User $user */
|
||||
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
|
||||
} catch (ResetPasswordExceptionInterface $e) {
|
||||
$this->addFlash('reset_password_error', sprintf(
|
||||
'%s - %s',
|
||||
$translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
|
||||
$translator->trans($e->getReason(), [], 'ResetPasswordBundle')
|
||||
));
|
||||
|
||||
return $this->redirectToRoute('app_forgot_password_request');
|
||||
}
|
||||
|
||||
// The token is valid; allow the user to change their password.
|
||||
$form = $this->createForm(ChangePasswordFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// A password reset token should be used only once, remove it.
|
||||
$this->resetPasswordHelper->removeResetRequest($token);
|
||||
|
||||
/** @var string $plainPassword */
|
||||
$plainPassword = $form->get('plainPassword')->getData();
|
||||
|
||||
// Encode(hash) the plain password, and set it.
|
||||
$user->setPassword($passwordHasher->hashPassword($user, $plainPassword));
|
||||
$this->entityManager->flush();
|
||||
|
||||
// The session is cleaned up after the password has been changed.
|
||||
$this->cleanSessionAfterReset();
|
||||
|
||||
$this->addFlash('reset_password_success', 'Votre mot de passe a été réinitialisé avec succès.');
|
||||
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
return $this->render('reset_password/reset.html.twig', [
|
||||
'resetForm' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse
|
||||
{
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy([
|
||||
'email' => $emailFormData,
|
||||
]);
|
||||
|
||||
// Do not reveal whether a user account was found or not.
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('app_check_email');
|
||||
}
|
||||
|
||||
try {
|
||||
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
|
||||
} catch (ResetPasswordExceptionInterface) {
|
||||
// If you want to tell the user why a reset email was not sent, uncomment
|
||||
// the lines below and change the redirect to 'app_forgot_password_request'.
|
||||
// Caution: This may reveal if a user is registered or not.
|
||||
//
|
||||
// $this->addFlash('reset_password_error', sprintf(
|
||||
// '%s - %s',
|
||||
// $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, [], 'ResetPasswordBundle'),
|
||||
// $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
|
||||
// ));
|
||||
|
||||
return $this->redirectToRoute('app_check_email');
|
||||
}
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->from(new Address('contact@camelia-studio.org', 'Camélia Studio'))
|
||||
->to((string) $user->getEmail())
|
||||
->subject('Kumora - Votre demande de réinitialisation de mot de passe')
|
||||
->htmlTemplate('reset_password/email.html.twig')
|
||||
->context([
|
||||
'resetToken' => $resetToken,
|
||||
])
|
||||
;
|
||||
|
||||
$mailer->send($email);
|
||||
|
||||
// Store the token object in session for retrieval in check-email route.
|
||||
$this->setTokenObjectInSession($resetToken);
|
||||
|
||||
return $this->redirectToRoute('app_check_email');
|
||||
}
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
15
src/DTO/EmailDTO.php
Normal file
15
src/DTO/EmailDTO.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
class EmailDTO
|
||||
{
|
||||
|
||||
public string $email;
|
||||
public string $password;
|
||||
public function __construct(
|
||||
) {
|
||||
}
|
||||
}
|
12
src/DTO/PasswordDTO.php
Normal file
12
src/DTO/PasswordDTO.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
class PasswordDTO
|
||||
{
|
||||
public string $password;
|
||||
public string $newPassword;
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
125
src/Entity/ParentDirectory.php
Normal file
125
src/Entity/ParentDirectory.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enum\RoleEnum;
|
||||
use App\Repository\ParentDirectoryRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ParentDirectoryRepository::class)]
|
||||
class ParentDirectory
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(enumType: RoleEnum::class)]
|
||||
private ?RoleEnum $ownerRole = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'parentDirectories')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $userCreated = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $isPublic = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ParentDirectoryPermission>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ParentDirectoryPermission::class, mappedBy: 'parentDirectory', orphanRemoval: true)]
|
||||
private Collection $parentDirectoryPermissions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->parentDirectoryPermissions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwnerRole(): ?RoleEnum
|
||||
{
|
||||
return $this->ownerRole;
|
||||
}
|
||||
|
||||
public function setOwnerRole(RoleEnum $ownerRole): static
|
||||
{
|
||||
$this->ownerRole = $ownerRole;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserCreated(): ?User
|
||||
{
|
||||
return $this->userCreated;
|
||||
}
|
||||
|
||||
public function setUserCreated(?User $userCreated): static
|
||||
{
|
||||
$this->userCreated = $userCreated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPublic(): ?bool
|
||||
{
|
||||
return $this->isPublic;
|
||||
}
|
||||
|
||||
public function setIsPublic(bool $isPublic): static
|
||||
{
|
||||
$this->isPublic = $isPublic;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ParentDirectoryPermission>
|
||||
*/
|
||||
public function getParentDirectoryPermissions(): Collection
|
||||
{
|
||||
return $this->parentDirectoryPermissions;
|
||||
}
|
||||
|
||||
public function addParentDirectoryPermission(ParentDirectoryPermission $parentDirectoryPermission): static
|
||||
{
|
||||
if (!$this->parentDirectoryPermissions->contains($parentDirectoryPermission)) {
|
||||
$this->parentDirectoryPermissions->add($parentDirectoryPermission);
|
||||
$parentDirectoryPermission->setParentDirectory($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeParentDirectoryPermission(ParentDirectoryPermission $parentDirectoryPermission): static
|
||||
{
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($this->parentDirectoryPermissions->removeElement($parentDirectoryPermission) && $parentDirectoryPermission->getParentDirectory() === $this) {
|
||||
$parentDirectoryPermission->setParentDirectory(null);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
84
src/Entity/ParentDirectoryPermission.php
Normal file
84
src/Entity/ParentDirectoryPermission.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enum\RoleEnum;
|
||||
use App\Repository\ParentDirectoryPermissionRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ParentDirectoryPermissionRepository::class)]
|
||||
class ParentDirectoryPermission
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(enumType: RoleEnum::class)]
|
||||
private ?RoleEnum $role = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $read = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?bool $write = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'parentDirectoryPermissions')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?ParentDirectory $parentDirectory = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRole(): ?RoleEnum
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function setRole(RoleEnum $role): static
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRead(): ?bool
|
||||
{
|
||||
return $this->read;
|
||||
}
|
||||
|
||||
public function setRead(bool $read): static
|
||||
{
|
||||
$this->read = $read;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isWrite(): ?bool
|
||||
{
|
||||
return $this->write;
|
||||
}
|
||||
|
||||
public function setWrite(bool $write): static
|
||||
{
|
||||
$this->write = $write;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentDirectory(): ?ParentDirectory
|
||||
{
|
||||
return $this->parentDirectory;
|
||||
}
|
||||
|
||||
public function setParentDirectory(?ParentDirectory $parentDirectory): static
|
||||
{
|
||||
$this->parentDirectory = $parentDirectory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
38
src/Entity/ResetPasswordRequest.php
Normal file
38
src/Entity/ResetPasswordRequest.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ResetPasswordRequestRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
|
||||
class ResetPasswordRequest implements ResetPasswordRequestInterface
|
||||
{
|
||||
use ResetPasswordRequestTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
|
||||
{
|
||||
$this->initialize($expiresAt, $selector, $hashedToken);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enum\RoleEnum;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
@ -16,8 +21,9 @@ use Symfony\Component\Uid\Uuid;
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid', length: 180)]
|
||||
private ?Uuid $id = null;
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $email = null;
|
||||
@ -34,17 +40,24 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column]
|
||||
private ?string $password = null;
|
||||
|
||||
#[ORM\Column(enumType: RoleEnum::class)]
|
||||
private ?RoleEnum $folder_role = null;
|
||||
|
||||
public function initId(): void
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $fullname = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ParentDirectory>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ParentDirectory::class, mappedBy: 'userCreated')]
|
||||
private Collection $parentDirectories;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if ($this->id !== null) {
|
||||
return;
|
||||
$this->parentDirectories = new ArrayCollection();
|
||||
}
|
||||
|
||||
$this->id = Uuid::v4();
|
||||
}
|
||||
|
||||
public function getId(): ?Uuid
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@ -118,4 +131,56 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFolderRole(): ?RoleEnum
|
||||
{
|
||||
return $this->folder_role;
|
||||
}
|
||||
|
||||
public function setFolderRole(RoleEnum $folder_role): static
|
||||
{
|
||||
$this->folder_role = $folder_role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFullname(): ?string
|
||||
{
|
||||
return $this->fullname;
|
||||
}
|
||||
|
||||
public function setFullname(string $fullname): static
|
||||
{
|
||||
$this->fullname = $fullname;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ParentDirectory>
|
||||
*/
|
||||
public function getParentDirectories(): Collection
|
||||
{
|
||||
return $this->parentDirectories;
|
||||
}
|
||||
|
||||
public function addParentDirectory(ParentDirectory $parentDirectory): static
|
||||
{
|
||||
if (!$this->parentDirectories->contains($parentDirectory)) {
|
||||
$this->parentDirectories->add($parentDirectory);
|
||||
$parentDirectory->setUserCreated($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeParentDirectory(ParentDirectory $parentDirectory): static
|
||||
{
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($this->parentDirectories->removeElement($parentDirectory) && $parentDirectory->getUserCreated() === $this) {
|
||||
$parentDirectory->setUserCreated(null);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
53
src/Enum/RoleEnum.php
Normal file
53
src/Enum/RoleEnum.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum RoleEnum: string
|
||||
{
|
||||
case CONSEIL_ADMINISTRATION = 'Conseil d\'administration';
|
||||
case ADMINISTRATEUR = 'Administrateur';
|
||||
case MEMBRE = 'Membre';
|
||||
case MEMBRE_HONORAIRE = 'Membre honoraire';
|
||||
case PARTENAIRE = 'Partenaire';
|
||||
case VISITEUR = 'Visiteur';
|
||||
|
||||
public function getHigherRoles(): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
$isFound = false;
|
||||
foreach (RoleEnum::cases() as $role) {
|
||||
if ($role === $this) {
|
||||
$isFound = true;
|
||||
}
|
||||
|
||||
if ($isFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
$roles[] = $role;
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
public function getInferiorRoles(): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
$isFound = false;
|
||||
foreach (RoleEnum::cases() as $role) {
|
||||
if ($role === $this) {
|
||||
$isFound = true;
|
||||
}
|
||||
|
||||
if ($isFound) {
|
||||
$roles[] = $role;
|
||||
}
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
}
|
67
src/Form/ChangePasswordFormType.php
Normal file
67
src/Form/ChangePasswordFormType.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
|
||||
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
||||
|
||||
class ChangePasswordFormType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('plainPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'options' => [
|
||||
'attr' => [
|
||||
'autocomplete' => 'new-password',
|
||||
],
|
||||
],
|
||||
'first_options' => [
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Merci de renseigner un mot de passe',
|
||||
]),
|
||||
new Length([
|
||||
'min' => 12,
|
||||
'minMessage' => 'Votre mot de passe doit contenir au moins {{ limit }} caractères',
|
||||
// max length allowed by Symfony for security reasons
|
||||
'max' => 4096,
|
||||
]),
|
||||
new NotCompromisedPassword([
|
||||
'message' => 'Ce mot de passe a été exposé lors d\'une fuite de données, il ne peut pas être utilisé.',
|
||||
]),
|
||||
],
|
||||
'label' => 'Nouveau mot de passe',
|
||||
],
|
||||
'second_options' => [
|
||||
'label' => 'Répéter le mot de passe',
|
||||
],
|
||||
'invalid_message' => 'Les mots de passe ne correspondent pas.',
|
||||
// Instead of being set onto the object directly,
|
||||
// this is read and encoded in the controller
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Mettre à jour',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
24
src/Form/CreateDirectoryType.php
Normal file
24
src/Form/CreateDirectoryType.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class CreateDirectoryType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name', null, [
|
||||
'label' => 'Nom du dossier',
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Créer',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
49
src/Form/EmailFormType.php
Normal file
49
src/Form/EmailFormType.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\DTO\EmailDTO;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
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 EmailFormType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Nouvelle adresse email',
|
||||
'attr' => [
|
||||
'placeholder' => 'Nouvelle adresse email',
|
||||
'autocomplete' => 'new-email',
|
||||
],
|
||||
])
|
||||
->add('password', PasswordType::class, [
|
||||
'label' => 'Mot de passe actuel',
|
||||
'attr' => [
|
||||
'placeholder' => 'Mot de passe actuel',
|
||||
'autocomplete' => 'new-password',
|
||||
]
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Enregistrer',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => EmailDTO::class,
|
||||
'attr' => [
|
||||
'autocomplete' => 'off',
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
51
src/Form/FilePermissionType.php
Normal file
51
src/Form/FilePermissionType.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ParentDirectory;
|
||||
use App\Entity\User;
|
||||
use App\Enum\RoleEnum;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class FilePermissionType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('ownerRole', EnumType::class, [
|
||||
'class' => RoleEnum::class,
|
||||
'label' => 'Rôle minimum',
|
||||
])
|
||||
->add('isPublic', null, [
|
||||
'label' => 'Dossier public',
|
||||
])
|
||||
->add('parentDirectoryPermissions', CollectionType::class, [
|
||||
'entry_type' => ParentDirectoryPermissionType::class,
|
||||
'entry_options' => [
|
||||
'label' => false,
|
||||
],
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Enregistrer',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ParentDirectory::class,
|
||||
]);
|
||||
}
|
||||
}
|
42
src/Form/ParentDirectoryPermissionType.php
Normal file
42
src/Form/ParentDirectoryPermissionType.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ParentDirectory;
|
||||
use App\Entity\ParentDirectoryPermission;
|
||||
use App\Entity\User;
|
||||
use App\Enum\RoleEnum;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ParentDirectoryPermissionType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('role', EnumType::class, [
|
||||
'class' => RoleEnum::class,
|
||||
'label' => 'Rôle',
|
||||
])
|
||||
->add('read', null, [
|
||||
'label' => 'Lecture',
|
||||
])
|
||||
->add('write', null, [
|
||||
'label' => 'Écriture',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ParentDirectoryPermission::class,
|
||||
]);
|
||||
}
|
||||
}
|
51
src/Form/PasswordFormType.php
Normal file
51
src/Form/PasswordFormType.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\DTO\PasswordDTO;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PasswordFormType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('password', PasswordType::class, [
|
||||
'label' => 'Mot de passe actuel',
|
||||
'attr' => [
|
||||
'placeholder' => 'Mot de passe actuel',
|
||||
'autocomplete' => 'old-password',
|
||||
],
|
||||
'required' => true,
|
||||
])
|
||||
->add('newPassword', RepeatedType::class, [
|
||||
'type' => PasswordType::class,
|
||||
'first_options' => ['label' => 'Nouveau mot de passe'],
|
||||
'attr' => [
|
||||
'autocomplete' => 'new-password',
|
||||
],
|
||||
'second_options' => ['label' => 'Confirmer le mot de passe'],
|
||||
'required' => true,
|
||||
'invalid_message' => 'Les mots de passe ne correspondent pas.',
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Enregistrer',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => PasswordDTO::class,
|
||||
]);
|
||||
}
|
||||
}
|
31
src/Form/RenameType.php
Normal file
31
src/Form/RenameType.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class RenameType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('newName', null, [
|
||||
'label' => 'Nouveau nom',
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Renommer',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
]);
|
||||
}
|
||||
}
|
39
src/Form/ResetPasswordRequestFormType.php
Normal file
39
src/Form/ResetPasswordRequestFormType.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class ResetPasswordRequestFormType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
'attr' => ['autocomplete' => 'email'],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Merci de renseigner votre adresse email',
|
||||
]),
|
||||
],
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Envoyer',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
37
src/Form/UploadType.php
Normal file
37
src/Form/UploadType.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\UX\Dropzone\Form\DropzoneType;
|
||||
|
||||
class UploadType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('files', FileType::class, [
|
||||
'label' => 'Fichiers à envoyer',
|
||||
'attr' => [
|
||||
'placeholder' => 'Déposez vos fichiers ici',
|
||||
],
|
||||
'multiple' => true,
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'label' => 'Téléverser',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Enum\RoleEnum;
|
||||
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\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@ -16,9 +20,16 @@ class UserAdminType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('fullname', null, [
|
||||
'label' => 'Nom complet',
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Adresse email',
|
||||
])
|
||||
->add('folderRole', EnumType::class, [
|
||||
'label' => 'Groupe',
|
||||
'class' => RoleEnum::class,
|
||||
])
|
||||
->add('role', ChoiceType::class, [
|
||||
'label' => 'Rôle',
|
||||
'choices' => [
|
||||
@ -44,7 +55,6 @@ class UserAdminType extends AbstractType
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
'label' => 'Enregistrer',
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
|
45
src/Repository/ParentDirectoryPermissionRepository.php
Normal file
45
src/Repository/ParentDirectoryPermissionRepository.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ParentDirectoryPermission;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ParentDirectoryPermission>
|
||||
*/
|
||||
class ParentDirectoryPermissionRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ParentDirectoryPermission::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return ParentDirectoryPermission[] Returns an array of ParentDirectoryPermission objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('p.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?ParentDirectoryPermission
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
45
src/Repository/ParentDirectoryRepository.php
Normal file
45
src/Repository/ParentDirectoryRepository.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ParentDirectory;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ParentDirectory>
|
||||
*/
|
||||
class ParentDirectoryRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ParentDirectory::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return ParentDirectory[] Returns an array of ParentDirectory objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('p.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?ParentDirectory
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
34
src/Repository/ResetPasswordRequestRepository.php
Normal file
34
src/Repository/ResetPasswordRequestRepository.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ResetPasswordRequest;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
|
||||
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ResetPasswordRequest>
|
||||
*/
|
||||
class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface
|
||||
{
|
||||
use ResetPasswordRequestRepositoryTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ResetPasswordRequest::class);
|
||||
}
|
||||
|
||||
public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new \InvalidArgumentException('User must be an instance of ' . User::class);
|
||||
}
|
||||
return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
58
src/Security/Voter/FileVoter.php
Normal file
58
src/Security/Voter/FileVoter.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\ParentDirectory;
|
||||
use App\Entity\ParentDirectoryPermission;
|
||||
use App\Entity\User;
|
||||
use App\Enum\RoleEnum;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class FileVoter extends Voter
|
||||
{
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return $subject instanceof ParentDirectory;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
/**
|
||||
* @var ParentDirectory $realSubject
|
||||
*/
|
||||
$realSubject = $subject;
|
||||
$user = $token->getUser();
|
||||
|
||||
$parentDirectoryPermissionsVisiteurRead = array_filter($realSubject->getParentDirectoryPermissions()->toArray(), static fn (ParentDirectoryPermission $parentDirectoryPermission) => RoleEnum::VISITEUR === $parentDirectoryPermission->getRole());
|
||||
|
||||
if ([] !== $parentDirectoryPermissionsVisiteurRead && !($user instanceof User)) {
|
||||
return 'file_read' === $attribute && array_values($parentDirectoryPermissionsVisiteurRead)[0]->isRead();
|
||||
}
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
$parentDirectoryPermissions = array_filter($realSubject->getParentDirectoryPermissions()->toArray(), static fn (ParentDirectoryPermission $parentDirectoryPermission) => $parentDirectoryPermission->getRole() === $user->getFolderRole());
|
||||
|
||||
$parentDirectoryPermission = null;
|
||||
|
||||
if ([] !== $parentDirectoryPermissions) {
|
||||
$parentDirectoryPermission = array_values($parentDirectoryPermissions)[0];
|
||||
}
|
||||
|
||||
$checkNeeded = false;
|
||||
|
||||
if (null !== $parentDirectoryPermission) {
|
||||
$checkNeeded = 'file_read' === $attribute ? $parentDirectoryPermission->isRead() : $parentDirectoryPermission->isWrite();
|
||||
}
|
||||
|
||||
if ($realSubject->getUserCreated() === $user) {
|
||||
return true;
|
||||
}
|
||||
return $realSubject->isPublic() && ($checkNeeded || $realSubject->getOwnerRole() === $user->getFolderRole() || in_array($user->getFolderRole(), $realSubject->getOwnerRole()->getHigherRoles(), true));
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\BasenameExtensionRuntime;
|
||||
|
20
src/Twig/Extension/EntityExtension.php
Normal file
20
src/Twig/Extension/EntityExtension.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\EntityExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class EntityExtension extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('get_parent_dir', [EntityExtensionRuntime::class, 'getParentDir']),
|
||||
];
|
||||
}
|
||||
}
|
20
src/Twig/Extension/EnvironmentExtension.php
Normal file
20
src/Twig/Extension/EnvironmentExtension.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\EnvironmentExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class EnvironmentExtension extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('get_env', [EnvironmentExtensionRuntime::class, 'getEnv']),
|
||||
];
|
||||
}
|
||||
}
|
20
src/Twig/Extension/GravatarExtension.php
Normal file
20
src/Twig/Extension/GravatarExtension.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\GravatarExtensionRuntime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class GravatarExtension extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('get_gravatar', [GravatarExtensionRuntime::class, 'getGravatar']),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\SizeExtensionRuntime;
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Extension;
|
||||
|
||||
use App\Twig\Runtime\TimeExtensionRuntime;
|
||||
|
@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class BasenameExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
|
||||
public function basename($value)
|
||||
{
|
||||
return \basename($value);
|
||||
return \basename((string) $value);
|
||||
}
|
||||
}
|
||||
|
22
src/Twig/Runtime/EntityExtensionRuntime.php
Normal file
22
src/Twig/Runtime/EntityExtensionRuntime.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use App\Entity\ParentDirectory;
|
||||
use App\Repository\ParentDirectoryRepository;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class EntityExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ParentDirectoryRepository $parentDirectoryRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function getParentDir(string $value): ParentDirectory
|
||||
{
|
||||
return $this->parentDirectoryRepository->findOneBy(['name' => $value]);
|
||||
}
|
||||
}
|
15
src/Twig/Runtime/EnvironmentExtensionRuntime.php
Normal file
15
src/Twig/Runtime/EnvironmentExtensionRuntime.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class EnvironmentExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function getEnv(string $value, string $default = ''): string
|
||||
{
|
||||
return $_ENV[$value] ?? $default;
|
||||
}
|
||||
}
|
21
src/Twig/Runtime/GravatarExtensionRuntime.php
Normal file
21
src/Twig/Runtime/GravatarExtensionRuntime.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class GravatarExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(private readonly string $defaultImage)
|
||||
{
|
||||
}
|
||||
|
||||
public function getGravatar(string $email): string
|
||||
{
|
||||
$hash = hash('sha256', strtolower(trim($email)));
|
||||
|
||||
return 'https://gravatar.com/avatar/' . $hash . '?s=2048&d=' . urlencode($this->defaultImage);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
@ -15,8 +17,8 @@ class SizeExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
$bytes = $value;
|
||||
$size = ['B', 'KB', 'MB', 'GB','TB'];
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
$factor = floor((strlen((string) $bytes) - 1) / 3);
|
||||
|
||||
return sprintf('%.1f', $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
|
||||
return sprintf('%.1f', $bytes / 1024 ** $factor) . ' ' . @$size[$factor];
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
39
symfony.lock
39
symfony.lock
@ -26,6 +26,18 @@
|
||||
"migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"friendsofphp/php-cs-fixer": {
|
||||
"version": "3.68",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||
},
|
||||
"files": [
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"oneup/flysystem-bundle": {
|
||||
"version": "4.12",
|
||||
"recipe": {
|
||||
@ -38,6 +50,18 @@
|
||||
"config/packages/oneup_flysystem.yaml"
|
||||
]
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "2.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
|
||||
},
|
||||
"files": [
|
||||
"phpstan.dist.neon"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "9.6",
|
||||
"recipe": {
|
||||
@ -295,6 +319,9 @@
|
||||
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||
}
|
||||
},
|
||||
"symfony/ux-dropzone": {
|
||||
"version": "v2.22.1"
|
||||
},
|
||||
"symfony/ux-icons": {
|
||||
"version": "2.22",
|
||||
"recipe": {
|
||||
@ -353,6 +380,18 @@
|
||||
"config/routes/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"symfonycasts/reset-password-bundle": {
|
||||
"version": "1.23",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "97c1627c0384534997ae1047b93be517ca16de43"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/reset_password.yaml"
|
||||
]
|
||||
},
|
||||
"symfonycasts/tailwind-bundle": {
|
||||
"version": "v0.6.1"
|
||||
},
|
||||
|
5
tailwind.config.js
Executable file → Normal file
5
tailwind.config.js
Executable file → Normal file
@ -8,5 +8,8 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('flowbite/plugin')
|
||||
],
|
||||
darkMode: 'media',
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
{% extends 'base-admin.html.twig' %}
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Le cloud de Camélia-Studio{% endblock %}
|
||||
{% block title %}Le disque nuagique 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">
|
||||
<a href="{{ path('app_admin_user_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>
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends 'base-admin.html.twig' %}
|
||||
{% extends 'base.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">
|
||||
<div class="block p-6 bg-white border border-gray-200 rounded-lg shadow-sm 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 }}
|
||||
|
@ -1,11 +1,11 @@
|
||||
{% extends 'base-admin.html.twig' %}
|
||||
{% extends 'base.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">
|
||||
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow-sm 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>
|
||||
<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-hidden dark:focus:ring-blue-800">Créer un utilisateur</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{{ include('partials/alerts.html.twig') }}
|
||||
@ -24,13 +24,13 @@
|
||||
Rôle
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Action
|
||||
Actions
|
||||
</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">
|
||||
<tr class="odd:bg-white dark:odd:bg-gray-900 even:bg-gray-50 dark:even: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>
|
||||
|
@ -1,18 +0,0 @@
|
||||
<!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>
|
@ -11,8 +11,12 @@
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-turbo="false">
|
||||
<body data-turbo="false" class="flex flex-col min-h-screen">
|
||||
{% include "partials/navbar.html.twig" %}
|
||||
<div class="flex-1">
|
||||
{% include "partials/alerts.html.twig" %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{% include "partials/footer.html.twig" %}
|
||||
</body>
|
||||
</html>
|
||||
|
15
templates/files/create_directory.html.twig
Normal file
15
templates/files/create_directory.html.twig
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.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-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Créer un dossier dans /{{ basePath }}</h3>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
Créer un dossier
|
||||
{% endblock %}
|
||||
|
37
templates/files/file_edit.html.twig
Normal file
37
templates/files/file_edit.html.twig
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends 'base.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-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Gérer les permissions du dossier {{ parentDir.name }}</h3>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.ownerRole) }}
|
||||
{{ form_row(form.isPublic) }}
|
||||
{{ form_label(form.parentDirectoryPermissions) }}
|
||||
{{ form_widget(form.parentDirectoryPermissions) }}
|
||||
<button type="button" id="add-parentDirectoryPermissions" class="py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-hidden bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">Ajouter</button>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
Gérer les permissions du dossier {{ parentDir.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script>
|
||||
document.addEventListener('readystatechange', () => {
|
||||
if (document.readyState === "complete") {
|
||||
document.querySelector('#add-parentDirectoryPermissions').addEventListener('click', function() {
|
||||
const container = document.querySelector('#file_permission_parentDirectoryPermissions');
|
||||
const prototype = container.dataset.prototype;
|
||||
const index = container.children.length;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', prototype.replace(/__name__/g, index));
|
||||
});
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
@ -8,6 +8,18 @@
|
||||
<div class="mt-4">
|
||||
{{ include('partials/alerts.html.twig') }}
|
||||
</div>
|
||||
{% if parentDir == null or (parentDir != null and is_granted('file_write', parentDir)) %}
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_files_create_directory', {
|
||||
base: path
|
||||
}) }}" 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-hidden dark:focus:ring-blue-800">Créer un dossier</a>
|
||||
{% if path != '' %}
|
||||
<a href="{{ path('app_files_upload', {
|
||||
path: path
|
||||
}) }}" 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-hidden dark:focus:ring-blue-800">Ajouter des fichiers</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-4">
|
||||
{% include 'partials/breadbrumb.html.twig' %}
|
||||
</div>
|
||||
@ -26,7 +38,10 @@
|
||||
Modifié le
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Action
|
||||
Groupes d'accès
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -39,25 +54,65 @@
|
||||
{% 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>
|
||||
<a class="text-blue-700 dark:text-blue-500 hover:text-blue-900 dark:hover:text-blue-700" href="{{ file.previewUrl }}" {{ file.type == 'file' ? 'target="_blank"' : '' }}>{{ 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">
|
||||
<div class="flex gap-1">
|
||||
{% if parentDir == null %}
|
||||
{% set parentDirectory = get_parent_dir(file.path) %}
|
||||
{% else %}
|
||||
{% set parentDirectory = parentDir %}
|
||||
{% endif %}
|
||||
{% for parentRole in parentDirectory.ownerRole.getHigherRoles() %}
|
||||
<img class="w-6 h-6" title="{{ parentRole.value }}" src="https://ui-avatars.com/api/?name={{ parentRole.value }}" alt="">
|
||||
{% endfor %}
|
||||
<img class="w-6 h-6" title="{{ parentDirectory.ownerRole.value }}" src="https://ui-avatars.com/api/?name={{ parentDirectory.ownerRole.value }}" alt="">
|
||||
{% for permission in parentDirectory.parentDirectoryPermissions %}
|
||||
<img class="w-6 h-6" title="{{ permission.role.value }}" src="https://ui-avatars.com/api/?name={{ permission.role.value }}" alt="">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</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>
|
||||
<a title="Permet de voir le fichier" href="{{ file.previewUrl }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:eye" /></a>
|
||||
<button title="Permet de copier le lien de prévisualisation du fichier" onclick="navigator.clipboard.writeText('{{ file.previewUrl }}'); alert('Lien copié avec succès !')" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6 cursor-pointer" name="fa6-solid:clipboard" /></button>
|
||||
<a title="Permet de télécharger le fichier" href="{{ file.url }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:download" /></a>
|
||||
{% if is_granted('file_write', parentDir) %}
|
||||
<a title="Permet de renommer le fichier" href="{{ path('app_files_rename', {
|
||||
path: file.path
|
||||
}) }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
|
||||
<a title="Permet de supprimer le fichier" href="{{
|
||||
path('app_files_delete', {
|
||||
filename: file.path
|
||||
})
|
||||
}}" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% if parentDir == null %}
|
||||
{% set parentDirectory = get_parent_dir(file.path) %}
|
||||
{% else %}
|
||||
{% set parentDirectory = parentDir %}
|
||||
{% endif %}
|
||||
|
||||
{% if parentDir == null and (parentDirectory.userCreated == app.user or (parentDirectory.isPublic and app.user.folderRole == enum('App\\Enum\\RoleEnum').CONSEIL_ADMINISTRATION)) %}
|
||||
<a href="{{ path('app_files_file_edit_permission', {
|
||||
parentDir: parentDirectory.name,
|
||||
}) }}" class="hover:text-blue-700 duration-300" title="Permet de modifier les permissions du dossier"><twig:ux:icon class="w-6 h-6" name="fa6-solid:shield" /></a>
|
||||
{% endif %}
|
||||
{% if is_granted('file_write', parentDirectory) %}
|
||||
<a href="{{ path('app_files_rename-directory', {
|
||||
path: file.path
|
||||
}) }}" class="hover:text-blue-700 duration-300" title="Permet de renommer le dossier"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
|
||||
<a href="{{ path('app_files_delete_directory', {
|
||||
path: file.path
|
||||
}) }}" class="hover:text-red-700 duration-300" title="Permet de supprimer le dossier"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
15
templates/files/rename.html.twig
Normal file
15
templates/files/rename.html.twig
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.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-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Renommer le {{ type }} {{ filepath }}</h3>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
Renommer un {{ type }}
|
||||
{% endblock %}
|
||||
|
15
templates/files/upload.html.twig
Normal file
15
templates/files/upload.html.twig
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.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-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Ajouter des fichiers dans /{{ path }}</h3>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
Ajouter des fichiers
|
||||
{% endblock %}
|
||||
|
@ -1,9 +1,10 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Le cloud de Camélia-Studio{% endblock %}
|
||||
{% block title %}Le disque nuagique de Camélia-Studio{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-96 flex items-center flex-col justify-center">
|
||||
<img src="{{ get_env('DEFAULT_IMAGE') }}" alt="Mascotte Camélia Studio" class="max-w-96">
|
||||
<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">
|
||||
@ -11,5 +12,10 @@
|
||||
Accéder à mon espace
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb-16 flex items-center justify-center gap-8">
|
||||
<a target="_blank" href="{{ get_env('CAMELIA_URL') }}" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">Camélia Studio</a>
|
||||
<a target="_blank" href="{{ get_env('TSUBAKIMONO_URL') }}" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">Tsubakimono</a>
|
||||
<a target="_blank" href="{{ get_env('DISCORD_URL') }}" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">Discord</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,4 +1,5 @@
|
||||
{% for label, messages in app.flashes %}
|
||||
<div class="container mx-auto px-16 mt-4">
|
||||
{% 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">
|
||||
@ -18,4 +19,5 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
@ -8,7 +8,9 @@
|
||||
</li>
|
||||
{% if path != '' %}
|
||||
{% set pathSplitted = path|split('/') %}
|
||||
{% set base = '' %}
|
||||
{% for pa in pathSplitted %}
|
||||
{% set base = base ~ '/' ~ pa %}
|
||||
{% if loop.last %}
|
||||
<li aria-current="page">
|
||||
<div class="flex items-center">
|
||||
@ -22,7 +24,7 @@
|
||||
<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="#"
|
||||
<a href="{{ path('app_files_index', {path: base}) }}"
|
||||
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>
|
||||
|
9
templates/partials/footer.html.twig
Normal file
9
templates/partials/footer.html.twig
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
<footer class="bg-white border-gray-200 dark:bg-gray-900 p-4">
|
||||
<div class="w-full flex justify-center gap-4">
|
||||
<span class="text-gray-700 sm:text-center dark:text-gray-200">Site propulsé grâce à <a href="{{ get_env('GIT_URL') }}" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">Kumora</a></span>
|
||||
<span class="text-gray-500 sm:text-center dark:text-gray-400">
|
||||
Version {{ get_env('APP_VERSION', '0.0.1') }}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user