Compare commits

..

No commits in common. "main" and "0.0.1" have entirely different histories.
main ... 0.0.1

112 changed files with 846 additions and 5224 deletions

View File

@ -5,4 +5,3 @@ vendor/
uploads/*
!uploads/.gitkeep
node_modules/

13
.env
View File

@ -15,9 +15,8 @@
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=prod
APP_ENV=dev
APP_SECRET=
APP_DEBUG=false
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
@ -38,13 +37,5 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> symfony/mailer ###
MAILER_DSN=sendmail://default
MAILER_DSN=null://null
###< 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

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Build and Push with Kaniko
uses: aevea/action-kaniko@master

View File

@ -1,17 +0,0 @@
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

View File

@ -1,21 +0,0 @@
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

View File

@ -1,18 +0,0 @@
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
View File

@ -34,12 +34,3 @@
!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/

View File

@ -1,103 +0,0 @@
<?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;

View File

@ -1,4 +1,4 @@
FROM dunglas/frankenphp@sha256:bc16b2c6900748ffd951b751a0798dba6a13ffa22ed4c793cf460ca0be4bc446
FROM dunglas/frankenphp
ENV SERVER_NAME=":80"

View File

@ -6,4 +6,5 @@ import './bootstrap.js';
* which should already be in your base.html.twig.
*/
import './styles/app.css';
import 'flowbite';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

View File

@ -1,14 +1,5 @@
{
"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 Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

@ -1,28 +1,3 @@
@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);
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -7,15 +7,15 @@
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^4.2.2",
"doctrine/doctrine-bundle": "^2.13.2",
"doctrine/doctrine-migrations-bundle": "^3.4.1",
"doctrine/orm": "^3.3.2",
"doctrine/dbal": "^3.9.3",
"doctrine/doctrine-bundle": "^2.13.1",
"doctrine/doctrine-migrations-bundle": "^3.3.1",
"doctrine/orm": "^3.3.1",
"league/flysystem": "^3.29.1",
"oneup/flysystem-bundle": "^4.12.4",
"oneup/flysystem-bundle": "^4.12.3",
"phpdocumentor/reflection-docblock": "^5.6.1",
"phpstan/phpdoc-parser": "^2.0.2",
"symfony/apache-pack": "^1.0.1",
"phpstan/phpdoc-parser": "^2.0",
"symfony/apache-pack": "^1.0",
"symfony/asset": "7.2.*",
"symfony/asset-mapper": "7.2.*",
"symfony/console": "7.2.*",
@ -38,28 +38,25 @@
"symfony/security-bundle": "7.2.*",
"symfony/security-csrf": "7.2.*",
"symfony/serializer": "7.2.*",
"symfony/stimulus-bundle": "^2.23.0",
"symfony/stimulus-bundle": "^2.22.1",
"symfony/string": "7.2.*",
"symfony/translation": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/uid": "7.2.*",
"symfony/ux-dropzone": "^2.23.0",
"symfony/ux-icons": "^2.23",
"symfony/ux-turbo": "^2.23.0",
"symfony/ux-twig-component": "^2.23.0",
"symfony/ux-icons": "^2.22.1",
"symfony/ux-turbo": "^2.22.1",
"symfony/ux-twig-component": "^2.22.1",
"symfony/validator": "7.2.*",
"symfony/web-link": "7.2.*",
"symfony/yaml": "7.2.*",
"symfonycasts/reset-password-bundle": "^1.23.1",
"symfonycasts/tailwind-bundle": "^0.7.1",
"symfonycasts/tailwind-bundle": "^0.6.1",
"tales-from-a-dev/flowbite-bundle": "^0.7.1",
"twig/extra-bundle": "^2.12|^3.20",
"twig/twig": "^2.12|^3.20"
"twig/extra-bundle": "^2.12|^3.18",
"twig/twig": "^2.12|^3.18"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"phpstan/extension-installer": true,
"symfony/flex": true,
"symfony/runtime": true
},
@ -109,16 +106,11 @@
}
},
"require-dev": {
"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",
"phpunit/phpunit": "^9.6.22",
"symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*",
"symfony/debug-bundle": "7.2.*",
"symfony/maker-bundle": "^1.62.1",
"symfony/maker-bundle": "^1.61",
"symfony/phpunit-bridge": "^7.2",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*"

2565
composer.lock generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
@ -21,6 +19,4 @@ 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],
];

View File

@ -4,7 +4,7 @@ framework:
paths:
- assets/
missing_import_mode: strict
public_prefix: "%env(BASE_PREFIX)%/assets"
public_prefix: /kumora/assets
when@prod:
framework:

View File

@ -1,10 +1,6 @@
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)

View 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: sync
Symfony\Component\Notifier\Message\ChatMessage: sync
Symfony\Component\Notifier\Message\SmsMessage: sync
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@ -1,2 +0,0 @@
symfonycasts_reset_password:
request_password_repository: App\Repository\ResetPasswordRequestRepository

View File

@ -1,2 +0,0 @@
symfonycasts_tailwind:
binary_version: 'v4.0.7'

View File

@ -1,7 +1,5 @@
<?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';
}

View File

@ -3,4 +3,4 @@ controllers:
path: ../src/Controller/
namespace: App\Controller
type: attribute
prefix: '%base.prefix%'
prefix: /kumora

View File

@ -4,16 +4,13 @@
# 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\:

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/**
* Returns the importmap for this application.
*
@ -27,17 +25,4 @@ 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',
],
];

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20241229133017 extends AbstractMigration
{
public function getDescription(): string
{
return 'Ajout de la table user et messenger_messages';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE "user" (id BLOB NOT NULL --(DC2Type:uuid)
, email VARCHAR(255) NOT NULL, roles CLOB NOT NULL --(DC2Type:json)
, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
)');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@ -1,40 +0,0 @@
<?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');
}
}

View File

@ -1,31 +0,0 @@
<?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');
}
}

View File

@ -1,35 +0,0 @@
<?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)');
}
}

View File

@ -1,36 +0,0 @@
<?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)');
}
}

View File

@ -1,40 +0,0 @@
<?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');
}
}

View File

@ -1,36 +0,0 @@
<?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)');
}
}

View File

@ -1,32 +0,0 @@
<?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
View File

@ -1,297 +0,0 @@
{
"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"
}
}
}

View File

@ -1,6 +0,0 @@
{
"dependencies": {
"flowbite": "^3.1.2",
"tailwindcss": "^4.0.8"
}
}

View File

@ -1,7 +0,0 @@
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

View File

@ -1,21 +0,0 @@
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

View File

@ -1,9 +1,12 @@
<?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" />
@ -19,4 +22,17 @@
<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>

View File

@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static fn (array $context) => new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

View File

@ -1,32 +0,0 @@
<?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);
};

View File

@ -1,11 +1,8 @@
<?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;
@ -27,40 +24,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 (null !== $user) {
if ($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->setFolderRole(RoleEnum::from($folderRole));
$user->initId();
$this->entityManager->persist($user);
$this->entityManager->flush();
$io->success('Utilisateur créé avec succès');
} catch (\Exception $e) {
$io->error('Une erreur est survenue lors de la création de l\'utilisateur : ' . $e->getMessage());
$io->error('Une erreur est survenue lors de la création de l\'utilisateur');
}
return Command::SUCCESS;
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
@ -42,12 +40,13 @@ 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 instanceof \App\Entity\User) {
if (!$user) {
$user = new User();
$isNew = true;
}
@ -59,6 +58,7 @@ 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();

View File

@ -1,100 +1,54 @@
<?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 Symfony\Bridge\Doctrine\Attribute\MapEntity;
use League\Flysystem\FilesystemReader;
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')]
#[IsGranted('ROLE_USER')]
public function index(UrlGeneratorInterface $urlGenerator, #[MapQueryParameter('path')] string $path = ''): Response
public function index(Filesystem $defaultAdapter, UrlGeneratorInterface $urlGenerator, #[MapQueryParameter('path')] string $path = ''): Response
{
$path = $this->normalizePath($path);
$this->getUser();
if ('' !== $path) {
$pathExploded = explode('/', $path);
// 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);
$parentDir = $this->parentDirectoryRepository->findOneBy(['name' => $pathExploded[0]]);
if (null === $parentDir || !$this->defaultAdapter->directoryExists($path)) {
if ($path !== '' && !$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 = $this->defaultAdapter->listContents('/' . $path);
$files = $defaultAdapter->listContents('/' . $path);
$realFiles = [];
foreach ($files as $file) {
$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;
}
}
if (!str_starts_with($file['path'], '.')) {
$realFiles[] = [
'type' => $file['type'],
'path' => $file['path'],
'last_modified' => $file['lastModified'],
'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']]),
'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']]),
];
}
}
@ -103,445 +57,39 @@ 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 $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, #[MapQueryParameter('preview')] bool $preview)
public function fileProxy(Filesystem $defaultAdapter, #[MapQueryParameter('filename')]string $filename)
{
$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 = $defaultAdapter->mimeType($filename);
if ($mimetype === '') {
$mimetype = 'application/octet-stream';
}
$response = new StreamedResponse(static function () use ($file, $defaultAdapter): void {
$response = new StreamedResponse(static function () use ($filename, $defaultAdapter): void {
$outputStream = fopen('php://output', 'w');
$fileStream = $defaultAdapter->readStream($file);
$fileStream = $defaultAdapter->readStream($filename);
stream_copy_to_stream($fileStream, $outputStream);
});
$response->headers->set('Content-Type', $mimetype);
if (!$preview) {
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
basename($file)
basename($filename)
);
$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;
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

View File

@ -1,108 +0,0 @@
<?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(),
]);
}
}

View File

@ -1,181 +0,0 @@
<?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');
}
}

View File

@ -1,18 +1,16 @@
<?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\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SecurityController extends AbstractController
{
public function __construct()
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
{
}

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace App\DTO;
class EmailDTO
{
public string $email;
public string $password;
public function __construct(
) {
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace App\DTO;
class PasswordDTO
{
public string $password;
public string $newPassword;
public function __construct()
{
}
}

View File

@ -1,125 +0,0 @@
<?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;
}
}

View File

@ -1,84 +0,0 @@
<?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;
}
}

View File

@ -1,38 +0,0 @@
<?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;
}
}

View File

@ -1,13 +1,8 @@
<?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;
@ -21,9 +16,8 @@ use Symfony\Component\Uid\Uuid;
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: 'uuid', length: 180)]
private ?Uuid $id = null;
#[ORM\Column(length: 255)]
private ?string $email = null;
@ -40,24 +34,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column]
private ?string $password = null;
#[ORM\Column(enumType: RoleEnum::class)]
private ?RoleEnum $folder_role = null;
#[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()
public function initId(): void
{
$this->parentDirectories = new ArrayCollection();
if ($this->id !== null) {
return;
}
public function getId(): ?int
$this->id = Uuid::v4();
}
public function getId(): ?Uuid
{
return $this->id;
}
@ -131,56 +118,4 @@ 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;
}
}

View File

@ -1,53 +0,0 @@
<?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;
}
}

View File

@ -1,67 +0,0 @@
<?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,
]);
}
}

View File

@ -1,24 +0,0 @@
<?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',
])
;
}
}

View File

@ -1,49 +0,0 @@
<?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',
]
]);
}
}

View File

@ -1,51 +0,0 @@
<?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,
]);
}
}

View File

@ -1,42 +0,0 @@
<?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,
]);
}
}

View File

@ -1,51 +0,0 @@
<?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,
]);
}
}

View File

@ -1,31 +0,0 @@
<?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([
]);
}
}

View File

@ -1,39 +0,0 @@
<?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,
]);
}
}

View File

@ -1,37 +0,0 @@
<?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([
]);
}
}

View File

@ -1,15 +1,11 @@
<?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;
@ -20,16 +16,9 @@ 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' => [
@ -55,6 +44,7 @@ class UserAdminType extends AbstractType
$builder->add('submit', SubmitType::class, [
'label' => 'Enregistrer',
]);
}
public function configureOptions(OptionsResolver $resolver): void

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;

View File

@ -1,45 +0,0 @@
<?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()
// ;
// }
}

View File

@ -1,45 +0,0 @@
<?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()
// ;
// }
}

View File

@ -1,34 +0,0 @@
<?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);
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Security\Authentication;
use Symfony\Component\HttpFoundation\RedirectResponse;

View File

@ -1,58 +0,0 @@
<?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));
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Twig\Extension;
use App\Twig\Runtime\BasenameExtensionRuntime;

View File

@ -1,20 +0,0 @@
<?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']),
];
}
}

View File

@ -1,20 +0,0 @@
<?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']),
];
}
}

View File

@ -1,20 +0,0 @@
<?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']),
];
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Twig\Extension;
use App\Twig\Runtime\SizeExtensionRuntime;

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Twig\Extension;
use App\Twig\Runtime\TimeExtensionRuntime;

View File

@ -1,15 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Twig\Runtime;
use Twig\Extension\RuntimeExtensionInterface;
class BasenameExtensionRuntime implements RuntimeExtensionInterface
{
public function basename($value)
{
return \basename((string) $value);
return \basename($value);
}
}

View File

@ -1,22 +0,0 @@
<?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]);
}
}

View File

@ -1,15 +0,0 @@
<?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;
}
}

View File

@ -1,21 +0,0 @@
<?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);
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Twig\Runtime;
use Twig\Extension\RuntimeExtensionInterface;
@ -17,8 +15,8 @@ class SizeExtensionRuntime implements RuntimeExtensionInterface
{
$bytes = $value;
$size = ['B', 'KB', 'MB', 'GB','TB'];
$factor = floor((strlen((string) $bytes) - 1) / 3);
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf('%.1f', $bytes / 1024 ** $factor) . ' ' . @$size[$factor];
return sprintf('%.1f', $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
}
}

View File

@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Twig\Runtime;
use Twig\Extension\RuntimeExtensionInterface;

View File

@ -26,18 +26,6 @@
"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": {
@ -50,18 +38,6 @@
"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": {
@ -319,9 +295,6 @@
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
}
},
"symfony/ux-dropzone": {
"version": "v2.22.1"
},
"symfony/ux-icons": {
"version": "2.22",
"recipe": {
@ -380,18 +353,6 @@
"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 Normal file → Executable file
View File

@ -8,8 +8,5 @@ module.exports = {
theme: {
extend: {},
},
plugins: [
require('flowbite/plugin')
],
darkMode: 'media',
plugins: [],
}

View File

@ -1,13 +1,13 @@
{% extends 'base.html.twig' %}
{% extends 'base-admin.html.twig' %}
{% block title %}Le disque nuagique de Camélia-Studio{% endblock %}
{% block title %}Le cloud de Camélia-Studio{% endblock %}
{% block body %}
<div class="min-h-96 flex items-center flex-col justify-center">
<h1 class="text-4xl font-bold mb-2 text-gray-900 dark:text-white">Bienvenue sur l'administration</h1>
<p class="text-xl text-gray-600 dark:text-gray-300 mb-8">Gérez facilement les accès des membres de Camélia-Studio à l'espace de stockage partagé Kumora. Ajoutez, modifiez ou retirez les utilisateurs en quelques clics.</p>
<div class="mb-16">
<a href="{{ path('app_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">
<a href="{{ path('app_files_index') }}" class="text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 dark:bg-gray-800 dark:text-white dark:border-gray-600 font-medium rounded-lg px-8 py-3">
Gérer les utilisateurs
</a>
</div>

View File

@ -1,8 +1,8 @@
{% extends 'base.html.twig' %}
{% extends 'base-admin.html.twig' %}
{% block body %}
<div class="container mx-auto px-16 mt-4">
<div class="block p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
<div class="block p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
<h3 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{% if not isNew %}
Edition de l'utilisateur {{ user.email }}

View File

@ -1,11 +1,11 @@
{% extends 'base.html.twig' %}
{% extends 'base-admin.html.twig' %}
{% block body %}
<div class="container mx-auto px-16 mt-4">
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Liste des utilisateurs</h5>
<div class="flex justify-end">
<a href="{{ path('app_admin_user_create') }}" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-hidden 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-none 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">
Actions
Action
</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<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">
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ user.id }}
</th>

18
templates/base-admin.html.twig Executable file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="fr" class="bg-white dark:bg-gray-800 dark:text-white">
<head>
<meta charset="UTF-8">
<title>Kumora - {% block title %}Accueil{% endblock %}</title>
<link rel="icon" href="{{ asset('images/favicon.ico') }}">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>
<body data-turbo="false">
{% include "partials/navbar-admin.html.twig" %}
{% block body %}{% endblock %}
</body>
</html>

View File

@ -11,12 +11,8 @@
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>
<body data-turbo="false" class="flex flex-col min-h-screen">
<body data-turbo="false">
{% 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>

View File

@ -1,15 +0,0 @@
{% 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 %}

View File

@ -1,37 +0,0 @@
{% 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 %}

View File

@ -8,18 +8,6 @@
<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>
@ -38,10 +26,7 @@
Modifié le
</th>
<th scope="col" class="px-6 py-3">
Groupes d'accès
</th>
<th scope="col" class="px-6 py-3">
Actions
Action
</th>
</tr>
</thead>
@ -54,65 +39,25 @@
{% 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.previewUrl }}" {{ file.type == 'file' ? 'target="_blank"' : '' }}>{{ file.path|basename }}</a>
<a class="text-blue-700 dark:text-blue-500 hover:text-blue-900 dark:hover:text-blue-700" href="{{ file.url }}">{{ file.path|basename }}</a>
</th>
<td class="px-6 py-4">
{% if file.type == 'file' %}
{{ file.size|show_size }}
{% endif %}
</td>
<td class="px-6 py-4">
{{ file.last_modified|time_diff }}
</td>
<td class="px-6 py-4">
<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 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 %}
<a href="{{ file.url }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:download" /></a>
<a href="#" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
<a href="#" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
{% else %}
{% 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 %}
<a href="{{ file.url }}" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:link" /></a>
<a href="#" class="hover:text-blue-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:pencil" /></a>
<a href="#" class="hover:text-red-700 duration-300"><twig:ux:icon class="w-6 h-6" name="fa6-solid:trash-can" /></a>
{% endif %}
</td>
</tr>

View File

@ -1,15 +0,0 @@
{% 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 %}

View File

@ -1,15 +0,0 @@
{% 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 %}

View File

@ -1,10 +1,9 @@
{% extends 'base.html.twig' %}
{% block title %}Le disque nuagique de Camélia-Studio{% endblock %}
{% block title %}Le cloud 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">
@ -12,10 +11,5 @@
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 %}

View File

@ -1,4 +1,3 @@
<div class="container mx-auto px-16 mt-4">
{% for label, messages in app.flashes %}
{% for message in messages %}
{% if label == 'success' %}
@ -20,4 +19,3 @@
{% endif %}
{% endfor %}
{% endfor %}
</div>

View File

@ -8,9 +8,7 @@
</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">
@ -24,7 +22,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="{{ path('app_files_index', {path: base}) }}"
<a href="#"
class="ms-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ms-2 dark:text-gray-400 dark:hover:text-white">{{ pa }}</a>
</div>
</li>

View File

@ -1,9 +0,0 @@
<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