feat : config + login
This commit is contained in:
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{compose.yaml,compose.*.yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
24
.env
Normal file
24
.env
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
###< symfony/routing ###
|
||||||
|
|
||||||
|
###> nelmio/cors-bundle ###
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=
|
||||||
|
JWT_COOKIE_SECURE=0
|
||||||
|
JWT_TOKEN_TTL=86400
|
||||||
|
JWT_COOKIE_TTL=86400
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env.local
|
||||||
|
/.env.local.php
|
||||||
|
/.env.*.local
|
||||||
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> friendsofphp/php-cs-fixer ###
|
||||||
|
/.php-cs-fixer.php
|
||||||
|
/.php-cs-fixer.cache
|
||||||
|
###< friendsofphp/php-cs-fixer ###
|
||||||
|
|
||||||
|
###> phpunit/phpunit ###
|
||||||
|
/phpunit.xml
|
||||||
|
/.phpunit.cache/
|
||||||
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
/config/jwt/*.pem
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
8
.idea/Lesstime.iml
generated
Normal file
8
.idea/Lesstime.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/db-forest-config.xml
generated
Normal file
6
.idea/db-forest-config.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="db-tree-configuration">
|
||||||
|
<option name="data" value="---------------------------------------- 1:0:9cad43df-2147-4989-b7a4-443067034884 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc 3:0:f407a514-c6b4-4b26-9555-445a85892502 4:0:09e221b8-067a-488b-9c1d-4e155a333079 " />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/material_theme_project_new.xml
generated
Normal file
10
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="userId" value="386cba74:19cc24e9181:-799b" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Lesstime.iml" filepath="$PROJECT_DIR$/.idea/Lesstime.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
20
.idea/php.xml
generated
Normal file
20
.idea/php.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
56
.php-cs-fixer.dist.php
Normal file
56
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PhpCsFixer\Config;
|
||||||
|
use PhpCsFixer\Finder;
|
||||||
|
|
||||||
|
$finder = Finder::create()
|
||||||
|
->in('src')
|
||||||
|
->notName('Kernel.php')
|
||||||
|
;
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'@Symfony' => true,
|
||||||
|
'@PSR12' => true,
|
||||||
|
'@PHP84Migration' => true,
|
||||||
|
'@PER-CS' => true,
|
||||||
|
'@PhpCsFixer' => true,
|
||||||
|
'strict_param' => true,
|
||||||
|
'strict_comparison' => true,
|
||||||
|
'no_useless_else' => true,
|
||||||
|
'no_useless_return' => true,
|
||||||
|
'binary_operator_spaces' => [
|
||||||
|
'operators' => [
|
||||||
|
'=' => 'align_single_space_minimal',
|
||||||
|
'||' => 'align_single_space_minimal',
|
||||||
|
'=>' => 'align_single_space_minimal',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'global_namespace_import' => [
|
||||||
|
'import_classes' => true,
|
||||||
|
'import_constants' => true,
|
||||||
|
'import_functions' => true,
|
||||||
|
],
|
||||||
|
'modernize_strpos' => true, // needs PHP 8+ or polyfill
|
||||||
|
'no_superfluous_phpdoc_tags' => true,
|
||||||
|
'echo_tag_syntax' => true,
|
||||||
|
'semicolon_after_instruction' => true,
|
||||||
|
'combine_consecutive_unsets' => true,
|
||||||
|
'ternary_to_null_coalescing' => true,
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'operator_linebreak' => [
|
||||||
|
'position' => 'beginning',
|
||||||
|
],
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'single_line_throw' => false,
|
||||||
|
'php_unit_test_class_requires_covers' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = new Config();
|
||||||
|
|
||||||
|
return $config
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules($rules)
|
||||||
|
->setFinder($finder)
|
||||||
|
;
|
||||||
21
bin/console
Executable file
21
bin/console
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
|
||||||
|
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||||
|
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||||
|
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
|
||||||
|
return new Application($kernel);
|
||||||
|
};
|
||||||
4
bin/phpunit
Executable file
4
bin/phpunit
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||||
31
commit-msg
Normal file
31
commit-msg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MSG_FILE="${1}"
|
||||||
|
FIRST_LINE="$(head -n 1 "$MSG_FILE" | tr -d '\r')"
|
||||||
|
|
||||||
|
# Autoriser commits auto-générés par git
|
||||||
|
if [[ "$FIRST_LINE" =~ ^Merge\ ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Types autorisés (MINUSCULES uniquement)
|
||||||
|
# Optionnel: scope => feat(auth) : ...
|
||||||
|
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||||
|
|
||||||
|
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
||||||
|
echo "❌ Message de commit invalide."
|
||||||
|
echo ""
|
||||||
|
echo "➡️ Format attendu : <type>(<scope optionnel>) : <message>"
|
||||||
|
echo "➡️ Types autorisés (minuscules uniquement) :"
|
||||||
|
echo " build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Exemples :"
|
||||||
|
echo " feat : add login page"
|
||||||
|
echo " fix(auth) : prevent null token crash"
|
||||||
|
echo " docs : update README"
|
||||||
|
echo ""
|
||||||
|
echo "❌ Exemple refusé :"
|
||||||
|
echo " Feat : add login page"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
93
composer.json
Normal file
93
composer.json
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"type": "project",
|
||||||
|
"license": "proprietary",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"api-platform/doctrine-orm": "^4.2",
|
||||||
|
"api-platform/symfony": "^4.2",
|
||||||
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
|
"doctrine/orm": "^3.6",
|
||||||
|
"lexik/jwt-authentication-bundle": "^3.2",
|
||||||
|
"nelmio/cors-bundle": "^2.6",
|
||||||
|
"phpdocumentor/reflection-docblock": "^6.0",
|
||||||
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
"symfony/asset": "8.0.*",
|
||||||
|
"symfony/console": "8.0.*",
|
||||||
|
"symfony/dotenv": "8.0.*",
|
||||||
|
"symfony/expression-language": "8.0.*",
|
||||||
|
"symfony/flex": "^2",
|
||||||
|
"symfony/framework-bundle": "8.0.*",
|
||||||
|
"symfony/property-access": "8.0.*",
|
||||||
|
"symfony/property-info": "8.0.*",
|
||||||
|
"symfony/runtime": "8.0.*",
|
||||||
|
"symfony/security-bundle": "8.0.*",
|
||||||
|
"symfony/serializer": "8.0.*",
|
||||||
|
"symfony/twig-bundle": "8.0.*",
|
||||||
|
"symfony/validator": "8.0.*",
|
||||||
|
"symfony/yaml": "8.0.*"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"php-http/discovery": true,
|
||||||
|
"symfony/flex": true,
|
||||||
|
"symfony/runtime": true
|
||||||
|
},
|
||||||
|
"bump-after-update": true,
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-php72": "*",
|
||||||
|
"symfony/polyfill-php73": "*",
|
||||||
|
"symfony/polyfill-php74": "*",
|
||||||
|
"symfony/polyfill-php80": "*",
|
||||||
|
"symfony/polyfill-php81": "*",
|
||||||
|
"symfony/polyfill-php82": "*",
|
||||||
|
"symfony/polyfill-php83": "*",
|
||||||
|
"symfony/polyfill-php84": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
},
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": false,
|
||||||
|
"require": "8.0.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/doctrine-fixtures-bundle": "^4.3",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.94",
|
||||||
|
"phpunit/phpunit": "^13.0",
|
||||||
|
"symfony/browser-kit": "8.0.*",
|
||||||
|
"symfony/css-selector": "8.0.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
10704
composer.lock
generated
Normal file
10704
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
config/bundles.php
Normal file
25
config/bundles.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
|
||||||
|
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||||
|
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
|
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||||
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
|
|
||||||
|
return [
|
||||||
|
FrameworkBundle::class => ['all' => true],
|
||||||
|
TwigBundle::class => ['all' => true],
|
||||||
|
SecurityBundle::class => ['all' => true],
|
||||||
|
DoctrineBundle::class => ['all' => true],
|
||||||
|
DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
|
NelmioCorsBundle::class => ['all' => true],
|
||||||
|
ApiPlatformBundle::class => ['all' => true],
|
||||||
|
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
|
];
|
||||||
7
config/packages/api_platform.yaml
Normal file
7
config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
api_platform:
|
||||||
|
title: Hello API Platform
|
||||||
|
version: 1.0.0
|
||||||
|
defaults:
|
||||||
|
stateless: true
|
||||||
|
cache_headers:
|
||||||
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
||||||
48
config/packages/doctrine.yaml
Normal file
48
config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version,
|
||||||
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
#server_version: '16'
|
||||||
|
|
||||||
|
profiling_collect_backtrace: '%kernel.debug%'
|
||||||
|
orm:
|
||||||
|
validate_xml_mapping: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
|
identity_generation_preferences:
|
||||||
|
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App:
|
||||||
|
type: attribute
|
||||||
|
is_bundle: false
|
||||||
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
|
prefix: 'App\Entity'
|
||||||
|
alias: App
|
||||||
|
controller_resolver:
|
||||||
|
auto_mapping: false
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
# "TEST_TOKEN" is typically set by ParaTest
|
||||||
|
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
query_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
result_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.result_cache_pool
|
||||||
|
|
||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
pools:
|
||||||
|
doctrine.result_cache_pool:
|
||||||
|
adapter: cache.app
|
||||||
|
doctrine.system_cache_pool:
|
||||||
|
adapter: cache.system
|
||||||
6
config/packages/doctrine_migrations.yaml
Normal file
6
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
# namespace is arbitrary but should be different from App\Migrations
|
||||||
|
# as migrations classes should NOT be autoloaded
|
||||||
|
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||||
|
enable_profiler: false
|
||||||
15
config/packages/framework.yaml
Normal file
15
config/packages/framework.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
|
session: true
|
||||||
|
|
||||||
|
#esi: true
|
||||||
|
#fragments: true
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
25
config/packages/lexik_jwt_authentication.yaml
Normal file
25
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
lexik_jwt_authentication:
|
||||||
|
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||||
|
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||||
|
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||||
|
token_ttl: '%env(int:JWT_TOKEN_TTL)%'
|
||||||
|
remove_token_from_body_when_cookies_used: true
|
||||||
|
token_extractors:
|
||||||
|
authorization_header:
|
||||||
|
enabled: false
|
||||||
|
cookie:
|
||||||
|
enabled: true
|
||||||
|
name: BEARER
|
||||||
|
query_parameter:
|
||||||
|
enabled: false
|
||||||
|
set_cookies:
|
||||||
|
BEARER:
|
||||||
|
lifetime: '%env(int:JWT_COOKIE_TTL)%'
|
||||||
|
samesite: lax
|
||||||
|
path: /
|
||||||
|
secure: '%env(bool:JWT_COOKIE_SECURE)%'
|
||||||
|
httpOnly: true
|
||||||
|
api_platform:
|
||||||
|
check_path: /api/login_check
|
||||||
|
username_path: username
|
||||||
|
password_path: password
|
||||||
11
config/packages/nelmio_cors.yaml
Normal file
11
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
nelmio_cors:
|
||||||
|
defaults:
|
||||||
|
origin_regex: true
|
||||||
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
|
allow_credentials: true
|
||||||
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
|
expose_headers: ['Link']
|
||||||
|
max_age: 3600
|
||||||
|
paths:
|
||||||
|
'^/': null
|
||||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
property_info:
|
||||||
|
with_constructor_extractor: true
|
||||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
default_uri: '%env(DEFAULT_URI)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
||||||
64
config/packages/security.yaml
Normal file
64
config/packages/security.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
security:
|
||||||
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
|
password_hashers:
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
|
providers:
|
||||||
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: username
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
dev:
|
||||||
|
# Ensure dev tools and static assets are always allowed
|
||||||
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
|
security: false
|
||||||
|
login:
|
||||||
|
pattern: ^/login_check
|
||||||
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
json_login:
|
||||||
|
check_path: /login_check
|
||||||
|
username_path: username
|
||||||
|
password_path: password
|
||||||
|
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||||
|
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||||
|
api:
|
||||||
|
pattern: ^/api
|
||||||
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
jwt: ~
|
||||||
|
logout:
|
||||||
|
path: /api/logout
|
||||||
|
target: /login
|
||||||
|
enable_csrf: false
|
||||||
|
delete_cookies:
|
||||||
|
BEARER:
|
||||||
|
path: /
|
||||||
|
|
||||||
|
# Activate different ways to authenticate:
|
||||||
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||||
|
# switch_user: true
|
||||||
|
|
||||||
|
# Note: Only the *first* matching rule is applied
|
||||||
|
access_control:
|
||||||
|
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
|
# Version de l'application en public
|
||||||
|
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [ GET ] }
|
||||||
|
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
security:
|
||||||
|
password_hashers:
|
||||||
|
# Password hashers are resource-intensive by design to ensure security.
|
||||||
|
# In tests, it's safe to reduce their cost to improve performance.
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
|
algorithm: auto
|
||||||
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
|
time_cost: 3 # Lowest possible value for argon
|
||||||
|
memory_cost: 10 # Lowest possible value for argon
|
||||||
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
twig:
|
||||||
|
file_name_pattern: '*.twig'
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
twig:
|
||||||
|
strict_variables: true
|
||||||
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
# Enables validator auto-mapping support.
|
||||||
|
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||||
|
#auto_mapping:
|
||||||
|
# App\Entity\: []
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
||||||
7
config/preload.php
Normal file
7
config/preload.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?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';
|
||||||
|
}
|
||||||
1761
config/reference.php
Normal file
1761
config/reference.php
Normal file
File diff suppressed because it is too large
Load Diff
11
config/routes.yaml
Normal file
11
config/routes.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||||
|
|
||||||
|
# This file is the entry point to configure the routes of your app.
|
||||||
|
# Methods with the #[Route] attribute are automatically imported.
|
||||||
|
# See also https://symfony.com/doc/current/routing.html
|
||||||
|
|
||||||
|
# To list all registered routes, run the following command:
|
||||||
|
# bin/console debug:router
|
||||||
|
|
||||||
|
controllers:
|
||||||
|
resource: routing.controllers
|
||||||
4
config/routes/api_platform.yaml
Normal file
4
config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
api_platform:
|
||||||
|
resource: .
|
||||||
|
type: api_platform
|
||||||
|
prefix: /api
|
||||||
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
when@dev:
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||||
|
prefix: /_error
|
||||||
7
config/routes/security.yaml
Normal file
7
config/routes/security.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
_security_logout:
|
||||||
|
resource: security.route_loader.logout
|
||||||
|
type: service
|
||||||
|
|
||||||
|
api_login:
|
||||||
|
path: /login_check
|
||||||
|
methods: [POST]
|
||||||
26
config/services.yaml
Normal file
26
config/services.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||||
|
|
||||||
|
# This file is the entry point to configure your own services.
|
||||||
|
# Files in the packages/ subdirectory configure your dependencies.
|
||||||
|
# See also https://symfony.com/doc/current/service_container/import.html
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
imports:
|
||||||
|
- { resource: version.yaml }
|
||||||
|
|
||||||
|
services:
|
||||||
|
# default configuration for services in *this* file
|
||||||
|
_defaults:
|
||||||
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
|
||||||
|
# makes classes in src/ available to be used as services
|
||||||
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
|
||||||
|
# add more service definitions when explicit configuration is needed
|
||||||
|
# please note that last definitions always *replace* previous ones
|
||||||
2
config/version.yaml
Normal file
2
config/version.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
parameters:
|
||||||
|
app.version: '0.1.0'
|
||||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
php:
|
||||||
|
container_name: php-${DOCKER_APP_NAME}-fpm
|
||||||
|
build:
|
||||||
|
context: ./docker/php
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
DOCKER_PHP_VERSION: ${DOCKER_PHP_VERSION}
|
||||||
|
DOCKER_NODE_VERSION: ${DOCKER_NODE_VERSION}
|
||||||
|
CURRENT_UID: ${CURRENT_UID}
|
||||||
|
CURRENT_GID: ${CURRENT_GID}
|
||||||
|
environment:
|
||||||
|
PHP_IDE_CONFIG: serverName=${DOCKER_APP_NAME}-docker
|
||||||
|
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
||||||
|
XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003
|
||||||
|
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
|
COMPOSER_HOME: /tmp/composer
|
||||||
|
COMPOSER_CACHE_DIR: /tmp/composer/cache
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www/html
|
||||||
|
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||||
|
- ~/.config:/var/www/.config # Pour la config de yarn
|
||||||
|
- ~/.composer:/var/www/.composer # Pour la config de composer
|
||||||
|
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
||||||
|
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
|
- ./LOG:/var/www/html/LOG
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- "3002:3002"
|
||||||
|
restart: unless-stopped
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: nginx-${DOCKER_APP_NAME}
|
||||||
|
depends_on:
|
||||||
|
- php
|
||||||
|
ports:
|
||||||
|
- "8082:80"
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www/html:ro
|
||||||
|
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
command: -p ${POSTGRES_PORT:-5435}
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5435}:${POSTGRES_PORT:-5435}"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
9
docker/.env.docker
Normal file
9
docker/.env.docker
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
DOCKER_APP_NAME=lesstime
|
||||||
|
DOCKER_PHP_VERSION=8.4.6
|
||||||
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
|
APP_USER=www-data
|
||||||
|
POSTGRES_DB=lesstime
|
||||||
|
POSTGRES_USER=root
|
||||||
|
POSTGRES_PASSWORD=root
|
||||||
|
POSTGRES_PORT=5435
|
||||||
|
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||||
9
docker/.env.docker.local
Normal file
9
docker/.env.docker.local
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
DOCKER_APP_NAME=lesstime
|
||||||
|
DOCKER_PHP_VERSION=8.4.6
|
||||||
|
DOCKER_NODE_VERSION=24.12.0
|
||||||
|
APP_USER=www-data
|
||||||
|
POSTGRES_DB=lesstime
|
||||||
|
POSTGRES_USER=root
|
||||||
|
POSTGRES_PASSWORD=root
|
||||||
|
POSTGRES_PORT=5435
|
||||||
|
XDEBUG_CLIENT_HOST=192.168.0.124
|
||||||
52
docker/nginx/conf.d/lesstime.conf
Normal file
52
docker/nginx/conf.d/lesstime.conf
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /var/www/html/frontend/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_wdt/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_profiler/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/login_check {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_param SCRIPT_NAME /index.php;
|
||||||
|
fastcgi_param PATH_INFO /login_check;
|
||||||
|
fastcgi_param REQUEST_URI /login_check;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
docker/php/Dockerfile
Normal file
129
docker/php/Dockerfile
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
ARG DOCKER_PHP_VERSION
|
||||||
|
|
||||||
|
FROM php:${DOCKER_PHP_VERSION}-fpm-bullseye
|
||||||
|
|
||||||
|
ARG DOCKER_NODE_VERSION
|
||||||
|
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
||||||
|
|
||||||
|
# Installer les dépendances et extensions PHP nécessaires
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libpng-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
libbz2-dev \
|
||||||
|
libgmp-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libonig-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
unixodbc-dev \
|
||||||
|
libsqlite3-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libc-client-dev \
|
||||||
|
libkrb5-dev \
|
||||||
|
freetds-dev \
|
||||||
|
vim \
|
||||||
|
tcpdump \
|
||||||
|
dnsutils \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
&& docker-php-ext-install -j$(nproc) \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
bcmath \
|
||||||
|
bz2 \
|
||||||
|
calendar \
|
||||||
|
exif \
|
||||||
|
gd \
|
||||||
|
gettext \
|
||||||
|
gmp \
|
||||||
|
ldap \
|
||||||
|
# mysqli \
|
||||||
|
pcntl \
|
||||||
|
pdo_pgsql \
|
||||||
|
# pdo_mysql \
|
||||||
|
# pdo_sqlite \
|
||||||
|
# pdo_sqlsrv \
|
||||||
|
soap \
|
||||||
|
sockets \
|
||||||
|
sysvsem \
|
||||||
|
xsl
|
||||||
|
|
||||||
|
|
||||||
|
# Installation de node
|
||||||
|
RUN wget -qO- "https://nodejs.org/dist/v${DOCKER_NODE_VERSION}/node-v${DOCKER_NODE_VERSION}-linux-x64.tar.xz" | tar xJC /tmp/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/bin /usr/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/include /usr/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/lib /usr/ && \
|
||||||
|
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/share /usr/ && \
|
||||||
|
npm install --global yarn
|
||||||
|
|
||||||
|
# installation/activation d'extensions php
|
||||||
|
RUN pecl install xdebug
|
||||||
|
RUN docker-php-ext-enable xdebug && \
|
||||||
|
docker-php-ext-install zip && \
|
||||||
|
docker-php-ext-install gd && \
|
||||||
|
docker-php-ext-install soap && \
|
||||||
|
docker-php-ext-configure intl && \
|
||||||
|
docker-php-ext-install intl
|
||||||
|
|
||||||
|
# Configuration spéciale pour quelques extensions
|
||||||
|
# RUN docker-php-ext-configure pdo_odbc --with-pdo-odbc=unixODBC,/usr && \
|
||||||
|
# docker-php-ext-install pdo_odbc \
|
||||||
|
RUN docker-php-ext-enable opcache
|
||||||
|
|
||||||
|
# Configurer Oracle OCI8 (nécessite le SDK Oracle, à installer manuellement ou à lier via les dépendances)
|
||||||
|
#RUN apt-get update && apt-get -y install wget unzip libaio1 && \
|
||||||
|
# wget https://download.oracle.com/otn_software/linux/instantclient/2340000/instantclient-basic-linux.x64-23.4.0.24.05.zip && \
|
||||||
|
# unzip -o instantclient-basic-linux.x64-23.4.0.24.05.zip -d /usr/local && \
|
||||||
|
# wget https://download.oracle.com/otn_software/linux/instantclient/2340000/instantclient-sdk-linux.x64-23.4.0.24.05.zip && \
|
||||||
|
# unzip -o instantclient-sdk-linux.x64-23.4.0.24.05.zip -d /usr/local
|
||||||
|
#
|
||||||
|
#RUN echo 'instantclient,/usr/local/instantclient_23_4' | pecl install oci8-3.4.0 \
|
||||||
|
# && docker-php-ext-enable oci8
|
||||||
|
#
|
||||||
|
#ENV ORACLE_BASE /usr/local/instantclient_23_4
|
||||||
|
#ENV LD_LIBRARY_PATH /usr/local/instantclient_23_4
|
||||||
|
#ENV TNS_ADMIN /usr/local/instantclient_23_4
|
||||||
|
#ENV ORACLE_HOME /usr/local/instantclient_23_4
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration pour utiliser Kerberos avec IMAP (si nécessaire)
|
||||||
|
# RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
|
||||||
|
# && docker-php-ext-install imap
|
||||||
|
|
||||||
|
# installation de composer
|
||||||
|
RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* && \
|
||||||
|
curl --insecure https://getcomposer.org/composer.phar -o /usr/bin/composer && chmod +x /usr/bin/composer
|
||||||
|
|
||||||
|
# cache Composer pour www-data
|
||||||
|
RUN mkdir -p /var/www/.composer/cache/vcs \
|
||||||
|
&& chown -R www-data:www-data /var/www/.composer
|
||||||
|
ENV COMPOSER_HOME=/var/www/.composer
|
||||||
|
|
||||||
|
# Création de la structure du projet
|
||||||
|
RUN mkdir /var/www/html/LOG
|
||||||
|
|
||||||
|
###> User ###
|
||||||
|
ARG CURRENT_UID
|
||||||
|
ARG CURRENT_GID
|
||||||
|
# mapping du user host avec www-data
|
||||||
|
RUN usermod -o -u ${CURRENT_UID} www-data && groupmod -o -g ${CURRENT_GID} www-data
|
||||||
|
RUN chown www-data:www-data -R /var/www/*
|
||||||
|
RUN chown www-data:www-data -R /var/www/.*
|
||||||
|
###< User ###
|
||||||
|
|
||||||
|
RUN rm -rf \
|
||||||
|
/var/lib/apt/lists/* \
|
||||||
|
/tmp/* \
|
||||||
|
/var/tmp/*
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
9
docker/php/config/docker-php-ext-xdebug.ini
Normal file
9
docker/php/config/docker-php-ext-xdebug.ini
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so
|
||||||
|
xdebug.mode=debug
|
||||||
|
xdebug.idekey=PHPSTORM
|
||||||
|
xdebug.start_with_request=yes
|
||||||
|
xdebug.discover_client_host=1
|
||||||
|
xdebug.client_port=9003
|
||||||
|
xdebug.log="/var/www/html/LOG/xdebug.log"
|
||||||
|
xdebug.log_level=0
|
||||||
|
xdebug.connect_timeout_ms=2
|
||||||
4
docker/php/config/php.ini
Normal file
4
docker/php/config/php.ini
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[Date]
|
||||||
|
; Defines the default timezone used by the date functions
|
||||||
|
; http://php.net/date.timezone
|
||||||
|
date.timezone = Europe/Paris
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
13
frontend/app.vue
Normal file
13
frontend/app.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { load } = useAppVersion()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
42
frontend/components/AppTopNav.vue
Normal file
42
frontend/components/AppTopNav.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<header class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
|
||||||
|
<div class="flex h-full items-center justify-end">
|
||||||
|
<div class="flex gap-12 text-xl text-white">
|
||||||
|
<div class="group relative flex gap-4">
|
||||||
|
<Icon name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" />
|
||||||
|
<p class="self-center cursor-pointer">{{ user?.username }}</p>
|
||||||
|
<div class="invisible absolute right-0 top-full z-20 mt-2 w-44 rounded-md border border-neutral-200 bg-white py-1 text-sm text-neutral-800 opacity-0 shadow-lg transition-all group-hover:visible group-hover:opacity-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
|
||||||
|
>
|
||||||
|
Mon profil
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
user?: UserData
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await auth.logout()
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
213
frontend/composables/useApi.ts
Normal file
213
frontend/composables/useApi.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import type { FetchOptions } from 'ofetch'
|
||||||
|
import { $fetch, FetchError } from 'ofetch'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
export type AnyObject = Record<string, unknown>
|
||||||
|
|
||||||
|
export type BlobResponse = {
|
||||||
|
data: Blob
|
||||||
|
headers: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiClient = {
|
||||||
|
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<BlobResponse>
|
||||||
|
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
|
||||||
|
FetchOptions<ResponseType> & {
|
||||||
|
toast?: boolean
|
||||||
|
toastOn401?: boolean
|
||||||
|
toastTitle?: string
|
||||||
|
toastErrorMessage?: string
|
||||||
|
toastSuccessMessage?: string
|
||||||
|
toastErrorKey?: string
|
||||||
|
toastSuccessKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useApi = (): ApiClient => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const baseURL = config.public.apiBase ?? '/api'
|
||||||
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
let isHandlingUnauthorized = false
|
||||||
|
const i18n = nuxtApp.$i18n as
|
||||||
|
| {
|
||||||
|
t: (key: string) => string
|
||||||
|
te?: (key: string) => boolean
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
|
||||||
|
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
|
||||||
|
|
||||||
|
const extractErrorMessage = (error: unknown, responseData?: unknown): string => {
|
||||||
|
const data = responseData ?? (error as FetchError)?.data
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
const record = data as Record<string, unknown>
|
||||||
|
return (
|
||||||
|
(record['hydra:description'] as string) ||
|
||||||
|
(record.detail as string) ||
|
||||||
|
(record.message as string) ||
|
||||||
|
(record.error as string) ||
|
||||||
|
(record.title as string) ||
|
||||||
|
(record['hydra:title'] as string) ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (error as FetchError)?.message ?? 'Erreur inconnue.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodErrorKeys: Record<string, string> = {
|
||||||
|
GET: 'errors.http.get',
|
||||||
|
POST: 'errors.http.post',
|
||||||
|
PUT: 'errors.http.put',
|
||||||
|
PATCH: 'errors.http.patch',
|
||||||
|
DELETE: 'errors.http.delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = $fetch.create({
|
||||||
|
baseURL,
|
||||||
|
retry: 0,
|
||||||
|
credentials: 'include',
|
||||||
|
onResponse({ options, response }) {
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response?.status && response.status >= 400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const successKey = apiOptions?.toastSuccessKey
|
||||||
|
const successMessage =
|
||||||
|
apiOptions?.toastSuccessMessage ||
|
||||||
|
(successKey ? (te(successKey) ? t(successKey) : successKey) : '')
|
||||||
|
|
||||||
|
if (successMessage) {
|
||||||
|
toast.success({
|
||||||
|
title: 'Succès',
|
||||||
|
message: successMessage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onResponseError({ response, error, options }) {
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (response?.status === 401) {
|
||||||
|
const requestUrl = typeof options?.url === 'string' ? options.url : ''
|
||||||
|
const isLoginCheck = requestUrl.includes('/login_check')
|
||||||
|
const isLogout = requestUrl.includes('/logout')
|
||||||
|
const shouldToast401 = apiOptions?.toastOn401 === true && apiOptions?.toast !== false
|
||||||
|
|
||||||
|
if (shouldToast401) {
|
||||||
|
const errorKey = apiOptions?.toastErrorKey
|
||||||
|
const errorMessage =
|
||||||
|
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
|
||||||
|
const extractedMessage = extractErrorMessage(error, response?._data)
|
||||||
|
const message =
|
||||||
|
apiOptions?.toastErrorMessage ||
|
||||||
|
errorMessage ||
|
||||||
|
extractedMessage ||
|
||||||
|
'Une erreur est survenue.'
|
||||||
|
|
||||||
|
toast.error({
|
||||||
|
title: apiOptions?.toastTitle ?? 'Erreur',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoginCheck && !isLogout) {
|
||||||
|
if (!isHandlingUnauthorized) {
|
||||||
|
isHandlingUnauthorized = true
|
||||||
|
auth.clearSession()
|
||||||
|
const route = useRoute()
|
||||||
|
if (route.path !== '/login') {
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
isHandlingUnauthorized = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const method =
|
||||||
|
typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
|
||||||
|
const defaultKey = methodErrorKeys[method]
|
||||||
|
const defaultMessage =
|
||||||
|
defaultKey && te(defaultKey) ? t(defaultKey) : ''
|
||||||
|
const errorKey = apiOptions?.toastErrorKey
|
||||||
|
const errorMessage =
|
||||||
|
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
|
||||||
|
const extractedMessage = extractErrorMessage(error, response?._data)
|
||||||
|
const message =
|
||||||
|
apiOptions?.toastErrorMessage ||
|
||||||
|
errorMessage ||
|
||||||
|
defaultMessage ||
|
||||||
|
extractedMessage ||
|
||||||
|
'Une erreur est survenue.'
|
||||||
|
|
||||||
|
toast.error({
|
||||||
|
title: apiOptions?.toastTitle ?? 'Erreur',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const request = <T>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
||||||
|
url: string,
|
||||||
|
options: ApiFetchOptions<'json'> = {}
|
||||||
|
) => {
|
||||||
|
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||||
|
const needsMergePatch = method === 'PATCH'
|
||||||
|
|
||||||
|
const headers = new Headers(options.headers as HeadersInit | undefined)
|
||||||
|
|
||||||
|
if (needsMergePatch && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/merge-patch+json')
|
||||||
|
} else if (needsJsonBody && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
|
|
||||||
|
return client<T>(url, { ...options, method, headers })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('GET', url, { ...options, query })
|
||||||
|
},
|
||||||
|
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
|
||||||
|
return client
|
||||||
|
.raw(url, { ...options, method: 'GET', query, responseType: 'blob' })
|
||||||
|
.then((res) => ({ data: res._data as Blob, headers: res.headers }))
|
||||||
|
},
|
||||||
|
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('POST', url, { ...options, body })
|
||||||
|
},
|
||||||
|
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('PUT', url, { ...options, body })
|
||||||
|
},
|
||||||
|
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('PATCH', url, { ...options, body })
|
||||||
|
},
|
||||||
|
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('DELETE', url, { ...options, query })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/composables/useAppVersion.ts
Normal file
17
frontend/composables/useAppVersion.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const useAppVersion = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const version = useState<string | null>('app-version', () => null)
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
if (version.value) {
|
||||||
|
return version.value
|
||||||
|
}
|
||||||
|
const response = await api.get<{ version: string }>('version', {}, {
|
||||||
|
toast: false
|
||||||
|
})
|
||||||
|
version.value = response.version
|
||||||
|
return version.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { version, load }
|
||||||
|
}
|
||||||
4
frontend/i18n.config.ts
Normal file
4
frontend/i18n.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default defineI18nConfig(() => ({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'fr'
|
||||||
|
}))
|
||||||
22
frontend/i18n/locales/fr.json
Normal file
22
frontend/i18n/locales/fr.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"errors": {
|
||||||
|
"http": {
|
||||||
|
"get": "Impossible de récupérer les données.",
|
||||||
|
"post": "Impossible de créer la ressource.",
|
||||||
|
"put": "Impossible de mettre à jour la ressource.",
|
||||||
|
"patch": "Impossible de mettre à jour la ressource.",
|
||||||
|
"delete": "Impossible de supprimer la ressource."
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Identifiants invalides.",
|
||||||
|
"logout": "Impossible de se déconnecter.",
|
||||||
|
"session": "Session expirée"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"auth": {
|
||||||
|
"login": "Connexion réussie.",
|
||||||
|
"logout": "Déconnexion réussie."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/layouts/auth.vue
Normal file
11
frontend/layouts/auth.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-tertiary-500 from-tertiary-500 via-white to-neutral-100 text-neutral-900">
|
||||||
|
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { version } = useAppVersion()
|
||||||
|
</script>
|
||||||
52
frontend/layouts/default.vue
Normal file
52
frontend/layouts/default.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-screen overflow-hidden">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<aside class="flex h-full w-64 flex-shrink-0 flex-col border-r border-neutral-200 bg-tertiary-500">
|
||||||
|
<div>
|
||||||
|
<img src="/malio.png" alt="Logo" class="w-auto"/>
|
||||||
|
</div>
|
||||||
|
<nav class="flex-1 px-4 pb-6">
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="flex items-center gap-3 px-4 pb-3 pt-6 text-md font-semibold text-black hover:bg-tertiary-500 hover:text-primary-500 border-t border-secondary-500"
|
||||||
|
active-class="bg-tertiary-500 text-primary-500"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:question-mark" size="24"/>
|
||||||
|
<span class="self-baseline text-md">Tableau de bord</span>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/project-list"
|
||||||
|
class="flex gap-3 px-4 py-3 text-md font-semibold text-black hover:bg-tertiary-500 hover:text-primary-500"
|
||||||
|
active-class="bg-tertiary-500 text-primary-500"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:folder-outline" size="24"/>
|
||||||
|
<span class="self-baseline text-md">Projets</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 items-center p-4">
|
||||||
|
<p class="font-bold">v 0.0.0</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="h-full flex-1 overflow-hidden flex flex-col">
|
||||||
|
<AppTopNav :user="auth.user" />
|
||||||
|
<main class="flex-1 overflow-y-auto px-8 py-12">
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useAppVersion} from "~/composables/useAppVersion";
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const {version} = useAppVersion()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await auth.logout()
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
16
frontend/middleware/auth.global.ts
Normal file
16
frontend/middleware/auth.global.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const isLogin = to.path === '/login'
|
||||||
|
|
||||||
|
if (!auth.checked) {
|
||||||
|
await auth.ensureSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLogin && !auth.isAuthenticated) {
|
||||||
|
return navigateTo('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogin && auth.isAuthenticated) {
|
||||||
|
return navigateTo('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
41
frontend/nuxt.config.ts
Normal file
41
frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-07-15',
|
||||||
|
devtools: {enabled: false},
|
||||||
|
ssr: false,
|
||||||
|
app: {
|
||||||
|
baseURL: process.env.NODE_ENV === 'production'
|
||||||
|
? (process.env.NUXT_PUBLIC_APP_BASE || '/')
|
||||||
|
: '/'
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'nuxt-toast',
|
||||||
|
'@nuxtjs/i18n',
|
||||||
|
'@nuxt/icon'
|
||||||
|
],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devServer: {port: 3002},
|
||||||
|
toast: {
|
||||||
|
settings: {
|
||||||
|
timeout: 2000,
|
||||||
|
closeOnClick: true,
|
||||||
|
progressBar: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
strategy: 'no_prefix',
|
||||||
|
defaultLocale: 'fr',
|
||||||
|
langDir: 'locales',
|
||||||
|
locales: [
|
||||||
|
{code: 'fr', file: 'fr.json', name: 'Français'}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
strict: true
|
||||||
|
}
|
||||||
|
})
|
||||||
13987
frontend/package-lock.json
generated
Normal file
13987
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "nuxt-app",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/icon": "^2.2.1",
|
||||||
|
"@nuxtjs/i18n": "^10.2.3",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"nuxt": "^4.3.1",
|
||||||
|
"nuxt-toast": "^1.4.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.29",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/pages/index.vue
Normal file
7
frontend/pages/index.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<h1 class="text-primary-500">Tableau de bord</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
76
frontend/pages/login.vue
Normal file
76
frontend/pages/login.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto w-full max-w-lg">
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
||||||
|
>
|
||||||
|
<img src="/malio.png" alt="Logo" class="w-[150px]"/>
|
||||||
|
</span>
|
||||||
|
<form
|
||||||
|
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-semibold text-neutral-700" for="username">
|
||||||
|
Nom d'utilisateur
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-semibold text-neutral-700" for="password">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
<p class="font-bold">v{{ version }}</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({layout: 'auth'})
|
||||||
|
useHead({
|
||||||
|
title: 'Connexion'
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const { version } = useAppVersion()
|
||||||
|
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await auth.login(username.value, password.value)
|
||||||
|
|
||||||
|
await router.push('/')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/malio.png
Normal file
BIN
frontend/public/malio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
2
frontend/public/robots.txt
Normal file
2
frontend/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
22
frontend/services/auth.ts
Normal file
22
frontend/services/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { UserData } from './dto/user-data'
|
||||||
|
|
||||||
|
export const getCurrentUser = () => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<UserData>('/me', {}, { toastErrorKey: 'errors.auth.session' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = (username: string, password: string) => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post('/login_check', { username, password }, {
|
||||||
|
toastOn401: true,
|
||||||
|
toastErrorKey: 'errors.auth.login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post('/logout', {}, {
|
||||||
|
toastErrorKey: 'errors.auth.logout',
|
||||||
|
toastSuccessKey: 'success.auth.logout'
|
||||||
|
})
|
||||||
|
}
|
||||||
5
frontend/services/dto/user-data.ts
Normal file
5
frontend/services/dto/user-data.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type UserData = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
roles: string[]
|
||||||
|
}
|
||||||
63
frontend/stores/auth.ts
Normal file
63
frontend/stores/auth.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import { getCurrentUser, login, logout } from '~/services/auth'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
user: null as UserData | null,
|
||||||
|
isLoading: false,
|
||||||
|
checked: false
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isAuthenticated: (state) => Boolean(state.user)
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
clearSession() {
|
||||||
|
this.user = null
|
||||||
|
this.checked = true
|
||||||
|
this.isLoading = false
|
||||||
|
},
|
||||||
|
async ensureSession() {
|
||||||
|
if (this.checked) {
|
||||||
|
return this.user
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checked = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const me = await getCurrentUser()
|
||||||
|
this.user = me
|
||||||
|
return me
|
||||||
|
} catch {
|
||||||
|
this.user = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(username, password)
|
||||||
|
const me = await getCurrentUser()
|
||||||
|
this.user = me
|
||||||
|
this.checked = true
|
||||||
|
return me
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await logout()
|
||||||
|
} catch {
|
||||||
|
// Ignore logout errors so we can still clear local auth state.
|
||||||
|
} finally {
|
||||||
|
this.user = null
|
||||||
|
this.checked = true
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
25
frontend/tailwind.config.ts
Normal file
25
frontend/tailwind.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type {Config} from 'tailwindcss'
|
||||||
|
|
||||||
|
export default <Partial<Config>>{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['"Helvetica Neue"', 'Helvetica', 'Arial', 'sans-serif']
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
500: '#222783',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
500: '#304998'
|
||||||
|
},
|
||||||
|
tertiary: {
|
||||||
|
500: '#F3F4F8'
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
500: '#056CF2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
frontend/tsconfig.json
Normal file
18
frontend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
128
makefile
Normal file
128
makefile
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Permet d'utiliser un .env.docker.local pour override
|
||||||
|
ENV_DEFAULT = docker/.env.docker
|
||||||
|
ENV_LOCAL = docker/.env.docker.local
|
||||||
|
ENV_FILE := $(if $(wildcard $(ENV_LOCAL)),$(ENV_LOCAL),$(ENV_DEFAULT))
|
||||||
|
|
||||||
|
# Permet d'avoir les variables du fichier .env.docker.local
|
||||||
|
include $(ENV_DEFAULT)
|
||||||
|
-include $(ENV_LOCAL)
|
||||||
|
|
||||||
|
PHP_CONTAINER = php-$(DOCKER_APP_NAME)-fpm
|
||||||
|
SYMFONY_CONSOLE = $(EXEC_PHP) php bin/console
|
||||||
|
|
||||||
|
DOCKER_COMPOSE = docker compose --env-file $(ENV_FILE)
|
||||||
|
DOCKER = docker
|
||||||
|
|
||||||
|
EXEC_PHP = $(DOCKER) exec -t -u $(APP_USER) $(PHP_CONTAINER)
|
||||||
|
EXEC_PHP_CS_FIXER = $(EXEC_PHP) php vendor/bin/php-cs-fixer
|
||||||
|
EXEC_PHP_ROOT = $(DOCKER) exec -t -u root $(PHP_CONTAINER)
|
||||||
|
EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER)
|
||||||
|
EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER)
|
||||||
|
FILES =
|
||||||
|
|
||||||
|
#========================================================================================
|
||||||
|
|
||||||
|
env-init:
|
||||||
|
@mkdir -p docker
|
||||||
|
@cp --update=none $(ENV_DEFAULT) $(ENV_LOCAL)
|
||||||
|
|
||||||
|
# Lance le container
|
||||||
|
start: env-init
|
||||||
|
@echo "**** START CONTAINERS ****"
|
||||||
|
@cp --update=none docker/.env.docker docker/.env.docker.local
|
||||||
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
|
# Éteint le container
|
||||||
|
stop:
|
||||||
|
$(DOCKER_COMPOSE) stop
|
||||||
|
|
||||||
|
restart: env-init
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
|
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS migration-migrate
|
||||||
|
|
||||||
|
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
||||||
|
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
||||||
|
|
||||||
|
composer-install:
|
||||||
|
$(EXEC_PHP) composer install
|
||||||
|
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
||||||
|
|
||||||
|
build-nuxtJS:
|
||||||
|
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
||||||
|
$(EXEC_PHP) sh -lc "cd frontend && npm install && npm run build:dist"
|
||||||
|
|
||||||
|
dev-nuxt:
|
||||||
|
$(EXEC_PHP) sh -c "cd frontend && npm run dev"
|
||||||
|
|
||||||
|
delete_built_dir:
|
||||||
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
||||||
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf frontend/node_modules
|
||||||
|
|
||||||
|
remove_orphans:
|
||||||
|
$(DOCKER_COMPOSE) kill
|
||||||
|
$(DOCKER_COMPOSE) down --volumes --remove-orphans
|
||||||
|
|
||||||
|
build-without-cache:
|
||||||
|
$(DOCKER_COMPOSE) build \
|
||||||
|
--build-arg="DOCKER_PHP_VERSION=$(DOCKER_PHP_VERSION)" \
|
||||||
|
--build-arg="DOCKER_NODE_VERSION=$(DOCKER_NODE_VERSION)" \
|
||||||
|
--build-arg="CURRENT_UID=$(shell id -u)" \
|
||||||
|
--build-arg="CURRENT_GID=$(shell id -g)" \
|
||||||
|
--no-cache
|
||||||
|
|
||||||
|
migration-migrate:
|
||||||
|
$(SYMFONY_CONSOLE) doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
fixtures:
|
||||||
|
$(SYMFONY_CONSOLE) --no-interaction doctrine:fixtures:load
|
||||||
|
|
||||||
|
# Attention, supprime votre bdd local
|
||||||
|
db-reset:
|
||||||
|
$(DOCKER_COMPOSE) down -v
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
$(MAKE) wait
|
||||||
|
$(SYMFONY_CONSOLE) doctrine:database:create --if-not-exists
|
||||||
|
$(MAKE) migration-migrate
|
||||||
|
$(MAKE) fixtures
|
||||||
|
|
||||||
|
# Restart la bdd
|
||||||
|
db-restart:
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
|
cache-clear:
|
||||||
|
$(SYMFONY_CONSOLE) cache:clear
|
||||||
|
|
||||||
|
copy-git-hook:
|
||||||
|
$(EXEC_PHP) cp pre-commit .git/hooks/
|
||||||
|
$(EXEC_PHP) cp commit-msg .git/hooks/
|
||||||
|
$(EXEC_PHP) chmod a+x .git/hooks/pre-commit
|
||||||
|
$(EXEC_PHP) chmod a+x .git/hooks/commit-msg
|
||||||
|
|
||||||
|
shell:
|
||||||
|
$(EXEC_PHP_INTERACTIVE) bash
|
||||||
|
|
||||||
|
shell-root:
|
||||||
|
$(EXEC_PHP_INTERACTIVE_ROOT) bash
|
||||||
|
|
||||||
|
# Suivi temps réel des logs dev
|
||||||
|
logs-dev:
|
||||||
|
$(EXEC_PHP_INTERACTIVE) sh -lc "tail -f var/log/dev.log"
|
||||||
|
|
||||||
|
# Force la version node
|
||||||
|
node-use:
|
||||||
|
bash -lc 'source "$$HOME/.nvm/nvm.sh" && nvm install && nvm use'
|
||||||
|
|
||||||
|
# Utilisé par le pre-commit pour fix les fichiers modifiés
|
||||||
|
php-cs-fixer-allow-risky:
|
||||||
|
@echo "Fixing files: $(FILES)"
|
||||||
|
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes $(FILES)
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)
|
||||||
|
|
||||||
|
wait:
|
||||||
|
sleep 10
|
||||||
0
migrations/.gitignore
vendored
Normal file
0
migrations/.gitignore
vendored
Normal file
32
migrations/Version20260308180201.php
Normal file
32
migrations/Version20260308180201.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260308180201 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 "user" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON "user" (username)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP TABLE "user"');
|
||||||
|
}
|
||||||
|
}
|
||||||
44
phpunit.dist.xml
Normal file
44
phpunit.dist.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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"
|
||||||
|
colors="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="display_errors" value="1" />
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
<server name="APP_ENV" value="test" force="true" />
|
||||||
|
<server name="SHELL_VERBOSITY" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Project Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source ignoreSuppressionOfDeprecations="true"
|
||||||
|
ignoreIndirectDeprecations="true"
|
||||||
|
restrictNotices="true"
|
||||||
|
restrictWarnings="true"
|
||||||
|
>
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
|
||||||
|
<deprecationTrigger>
|
||||||
|
<method>Doctrine\Deprecations\Deprecation::trigger</method>
|
||||||
|
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
|
||||||
|
<function>trigger_deprecation</function>
|
||||||
|
</deprecationTrigger>
|
||||||
|
</source>
|
||||||
|
|
||||||
|
<extensions>
|
||||||
|
</extensions>
|
||||||
|
</phpunit>
|
||||||
38
pre-commit
Normal file
38
pre-commit
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "######### Pre-commit hook start #############"
|
||||||
|
echo "--- php-cs-fixer pre commit hook start ---"
|
||||||
|
|
||||||
|
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.php$')
|
||||||
|
# Vérifier s'il y a des fichiers PHP modifiés
|
||||||
|
if [ -n "$FILES" ]; then
|
||||||
|
echo "Running PHP CS Fixer on staged PHP files..."
|
||||||
|
|
||||||
|
# Convertir la liste des fichiers en une chaîne séparée par des espaces
|
||||||
|
FILES_LIST=""
|
||||||
|
for FILE in $FILES; do
|
||||||
|
FILES_LIST="$FILES_LIST $FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Exécuter la cible make pour PHP CS Fixer
|
||||||
|
make php-cs-fixer-allow-risky FILES="$FILES_LIST"
|
||||||
|
|
||||||
|
# Ajouter les fichiers corrigés au commit
|
||||||
|
git add $FILES
|
||||||
|
else
|
||||||
|
echo "No PHP files to fix."
|
||||||
|
fi
|
||||||
|
echo "--- php-cs-fixer pre commit hook finish---"
|
||||||
|
|
||||||
|
echo "--- phpunit pre commit hook start ---"
|
||||||
|
make test
|
||||||
|
PHPUNIT_RESULT=$?
|
||||||
|
|
||||||
|
if [ $PHPUNIT_RESULT -ne 0 ]; then
|
||||||
|
echo "PHPUnit tests failed. Aborting commit."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "--- phpunit pre commit hook finished ---"
|
||||||
|
|
||||||
|
echo "All checks passed. Proceeding with commit."
|
||||||
|
exit 0
|
||||||
11
public/index.php
Normal file
11
public/index.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
};
|
||||||
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/ApiResource/.gitignore
vendored
Normal file
25
src/ApiResource/AppVersion.php
Normal file
25
src/ApiResource/AppVersion.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\State\AppVersionProvider;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/version',
|
||||||
|
normalizationContext: ['groups' => ['version:read']],
|
||||||
|
provider: AppVersionProvider::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
final class AppVersion
|
||||||
|
{
|
||||||
|
#[Groups(['version:read'])]
|
||||||
|
public string $version = '';
|
||||||
|
}
|
||||||
28
src/DataFixtures/AppFixtures.php
Normal file
28
src/DataFixtures/AppFixtures.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\DataFixtures;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
class AppFixtures extends Fixture
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
$admin = new User();
|
||||||
|
$admin->setUsername('admin');
|
||||||
|
$admin->setRoles(['ROLE_ADMIN']);
|
||||||
|
$admin->setPassword($this->passwordHasher->hashPassword($admin, 'admin'));
|
||||||
|
$manager->persist($admin);
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
121
src/Entity/User.php
Normal file
121
src/Entity/User.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\State\MeProvider;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/me',
|
||||||
|
provider: MeProvider::class,
|
||||||
|
normalizationContext: ['groups' => ['me:read']],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||||
|
#[ORM\Table(name: '`user`')]
|
||||||
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['me:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 180, unique: true)]
|
||||||
|
#[Groups(['me:read'])]
|
||||||
|
private ?string $username = null;
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['me:read'])]
|
||||||
|
private array $roles = [];
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?string $password = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
private ?DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsername(): ?string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUsername(string $username): static
|
||||||
|
{
|
||||||
|
$this->username = $username;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return (string) $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
$roles = $this->roles;
|
||||||
|
$roles[] = 'ROLE_USER';
|
||||||
|
|
||||||
|
return array_values(array_unique($roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $roles */
|
||||||
|
public function setRoles(array $roles): static
|
||||||
|
{
|
||||||
|
$this->roles = $roles;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(string $password): static
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(DateTimeImmutable $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eraseCredentials(): void {}
|
||||||
|
}
|
||||||
13
src/Kernel.php
Normal file
13
src/Kernel.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
|
|
||||||
|
class Kernel extends BaseKernel
|
||||||
|
{
|
||||||
|
use MicroKernelTrait;
|
||||||
|
}
|
||||||
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
20
src/Repository/UserRepository.php
Normal file
20
src/Repository/UserRepository.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<User>
|
||||||
|
*/
|
||||||
|
class UserRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/State/AppVersionProvider.php
Normal file
26
src/State/AppVersionProvider.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\ApiResource\AppVersion;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
final readonly class AppVersionProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%app.version%')]
|
||||||
|
private string $version,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): AppVersion
|
||||||
|
{
|
||||||
|
$dto = new AppVersion();
|
||||||
|
$dto->version = $this->version;
|
||||||
|
|
||||||
|
return $dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/State/MeProvider.php
Normal file
26
src/State/MeProvider.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements ProviderInterface<User>
|
||||||
|
*/
|
||||||
|
final readonly class MeProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): User
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
return $this->security->getUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
232
symfony.lock
Normal file
232
symfony.lock
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
{
|
||||||
|
"api-platform/symfony": {
|
||||||
|
"version": "4.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "4.0",
|
||||||
|
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/api_platform.yaml",
|
||||||
|
"config/routes/api_platform.yaml",
|
||||||
|
"src/ApiResource/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/deprecations": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "18ee08e513ba0303fd09a01fc1c934870af06ffa"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine.yaml",
|
||||||
|
"src/Entity/.gitignore",
|
||||||
|
"src/Repository/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-fixtures-bundle": {
|
||||||
|
"version": "4.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/DataFixtures/AppFixtures.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-migrations-bundle": {
|
||||||
|
"version": "4.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.1",
|
||||||
|
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine_migrations.yaml",
|
||||||
|
"migrations/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"friendsofphp/php-cs-fixer": {
|
||||||
|
"version": "3.94",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.39",
|
||||||
|
"ref": "97aaf9026490db73b86c23d49e5774bc89d2b232"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".php-cs-fixer.dist.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lexik/jwt-authentication-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.5",
|
||||||
|
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/lexik_jwt_authentication.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nelmio/cors-bundle": {
|
||||||
|
"version": "2.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.5",
|
||||||
|
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/nelmio_cors.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"phpunit/phpunit": {
|
||||||
|
"version": "13.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "11.1",
|
||||||
|
"ref": "1117deb12541f35793eec9fff7494d7aa12283fc"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env.test",
|
||||||
|
"phpunit.dist.xml",
|
||||||
|
"tests/bootstrap.php",
|
||||||
|
"bin/phpunit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/console": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.3",
|
||||||
|
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/console"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/flex": {
|
||||||
|
"version": "2.10",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env",
|
||||||
|
".env.dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/framework-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "09f6e081c763a206802674ce0cb34a022f0ffc6d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/cache.yaml",
|
||||||
|
"config/packages/framework.yaml",
|
||||||
|
"config/preload.php",
|
||||||
|
"config/routes/framework.yaml",
|
||||||
|
"config/services.yaml",
|
||||||
|
"public/index.php",
|
||||||
|
"src/Controller/.gitignore",
|
||||||
|
"src/Kernel.php",
|
||||||
|
".editorconfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/property_info.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/routing": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/routing.yaml",
|
||||||
|
"config/routes.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/security-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "c42fee7802181cdd50f61b8622715829f5d2335c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/security.yaml",
|
||||||
|
"config/routes/security.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/twig-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.4",
|
||||||
|
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/twig.yaml",
|
||||||
|
"templates/base.html.twig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/uid": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symfony/validator": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/validator.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
templates/base.html.twig
Normal file
16
templates/base.html.twig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||||
|
{% block stylesheets %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
tests/bootstrap.php
Normal file
15
tests/bootstrap.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Symfony\Component\Dotenv\Dotenv;
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (method_exists(Dotenv::class, 'bootEnv')) {
|
||||||
|
new Dotenv()->bootEnv(dirname(__DIR__).'/.env');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['APP_DEBUG']) {
|
||||||
|
umask(0o000);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user