Compare commits

..

5 Commits

Author SHA1 Message Date
jens 5063ba80af kleinere Fixes 2026-06-14 18:20:12 +02:00
jens cd0d554fef Infra 2026-06-14 18:00:43 +02:00
jens 4be49200f0 Datei Umbenannt 2026-06-13 19:27:14 +02:00
jens dbc8a0c1a7 Task 1 2026-06-13 19:21:36 +02:00
jens 41a12f556a Projekt und anforderungsprofil ergänzt 2026-06-13 13:42:45 +02:00
39 changed files with 7770 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# ==========================================
# GardenPlan Entwicklungsumgebung (Active)
# ==========================================
# --- Application ---
APP_ENV=dev
APP_SECRET=gardenplan_dev_secret_change_in_production_2024
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_HOSTS=^.*$
# --- Database (PostgreSQL) ---
POSTGRES_DB=gardenplan
POSTGRES_USER=symfony
POSTGRES_PASSWORD=changeme
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
# --- Keycloak (OIDC / JWT) ---
KEYCLOAK_SERVER_URL=http://keycloak:8080
KEYCLOAK_REALM=gardenplan
KEYCLOAK_CLIENT_ID=symfony-app
KEYCLOAK_CLIENT_SECRET=changeme_client_secret
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=adminsecret
# --- Mailer (lokales Dev-Tool) ---
MAILER_DSN=null://null
# --- Messenger (Queue) ---
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=messenger_messages
+29
View File
@@ -0,0 +1,29 @@
# ==========================================
# GardenPlan Entwicklungsumgebung
# ==========================================
# --- Application ---
APP_ENV=dev
APP_SECRET=change_me_to_a_random_string_in_production
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_HOSTS=^.*$
# --- Database (PostgreSQL) ---
POSTGRES_DB=gardenplan
POSTGRES_USER=symfony
POSTGRES_PASSWORD=changeme
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
# --- Keycloak (OIDC / JWT) ---
KEYCLOAK_SERVER_URL=http://keycloak:8080
KEYCLOAK_REALM=gardenplan
KEYCLOAK_CLIENT_ID=symfony-app
KEYCLOAK_CLIENT_SECRET=changeme_client_secret
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=adminsecret
# --- Mailer (lokales Dev-Tool) ---
MAILER_DSN=null://null
# --- Messenger (Queue) ---
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=messenger_messages
+17
View 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
+50
View File
@@ -0,0 +1,50 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
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 ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> 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=8f813531f1ad5443c687f70247b99c825771c9791460384f22cdb0353248f8f9
###< lexik/jwt-authentication-bundle ###
+26
View File
@@ -0,0 +1,26 @@
# ==========================================
# GardenPlan Entwicklung (lokal ohne Docker)
# ==========================================
# --- Application ---
APP_ENV=dev
APP_SECRET=gardenplan_dev_secret_change_in_production_2024
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_HOSTS=^.*$
# --- Database (PostgreSQL lokal) ---
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
# --- Keycloak (OIDC / JWT) ---
KEYCLOAK_SERVER_URL=http://keycloak:8080
KEYCLOAK_REALM=gardenplan
KEYCLOAK_CLIENT_ID=symfony-app
KEYCLOAK_CLIENT_SECRET=changeme_client_secret
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=adminsecret
# --- Mailer (lokales Dev-Tool) ---
MAILER_DSN=null://null
# --- Symfony Cache/Log ---
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=messenger_messages
+14
View File
@@ -0,0 +1,14 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> lexik/jwt-authentication-bundle ###
/config/jwt/*.pem
###< lexik/jwt-authentication-bundle ###
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+21
View 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);
};
+7
View File
@@ -0,0 +1,7 @@
services:
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###
+25
View File
@@ -0,0 +1,25 @@
services:
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-16}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###
+77
View File
@@ -0,0 +1,77 @@
{
"name": "symfony/skeleton",
"type": "project",
"license": "MIT",
"description": "A minimal Symfony project recommended to create bare bones applications",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "*",
"doctrine/orm": "^3.6",
"lexik/jwt-authentication-bundle": "^3.2",
"symfony/console": "8.1.*",
"symfony/dotenv": "8.1.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "8.1.*",
"symfony/messenger": "*",
"symfony/runtime": "8.1.*",
"symfony/security-bundle": "8.1.*",
"symfony/validator": "*",
"symfony/yaml": "8.1.*"
},
"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.1.*"
}
}
}
+4881
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
];
+19
View 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
+46
View File
@@ -0,0 +1,46 @@
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
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
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
+15
View 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
@@ -0,0 +1,4 @@
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
+22
View File
@@ -0,0 +1,22 @@
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
# failed: 'doctrine://default?queue_name=failed'
sync: 'sync://'
routing:
# Route your messages to the transports
# 'App\Message\YourMessage': async
# when@test:
# framework:
# messenger:
# transports:
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
# async: 'in-memory://'
@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true
+10
View 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
+39
View File
@@ -0,0 +1,39 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
# Ensure dev tools and static assets are always allowed
pattern: ^/(_profiler|_wdt|assets|build)/
security: false
main:
lazy: true
provider: users_in_memory
# Activate different ways to authenticate:
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Note: Only the *first* matching rule is applied
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
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
+11
View 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
+5
View File
@@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}
File diff suppressed because it is too large Load Diff
+11
View 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
View File
@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error
+3
View File
@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service
+23
View File
@@ -0,0 +1,23 @@
# 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:
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
+9
View File
@@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
/**
* @return list<string> An array of allowed values for APP_ENV
*/
private function getAllowedEnvs(): array
{
return ['prod', 'dev', 'test'];
}
}
+144
View File
@@ -0,0 +1,144 @@
{
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fdd756167454623e21f1d769c5b814b243782a67"
}
},
"doctrine/doctrine-bundle": {
"version": "3.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.0",
"ref": "d39a3bd844edfe90c20ae520b804a3bf4f82b4ad"
},
"files": [
"config/packages/doctrine.yaml",
"src/Entity/.gitignore",
"src/Repository/.gitignore"
]
},
"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"
]
},
"symfony/console": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
},
"files": [
"bin/console"
]
},
"symfony/flex": {
"version": "2.11",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env",
".env.dev"
]
},
"symfony/framework-bundle": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "8.1",
"ref": "312027aea160796a50bf2d185503afdb5d71f570"
},
"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/messenger": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.0",
"ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
},
"files": [
"config/packages/messenger.yaml"
]
},
"symfony/property-info": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"config/packages/property_info.yaml"
]
},
"symfony/routing": {
"version": "8.1",
"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.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.4",
"ref": "c42fee7802181cdd50f61b8622715829f5d2335c"
},
"files": [
"config/packages/security.yaml",
"config/routes/security.yaml"
]
},
"symfony/validator": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
},
"files": [
"config/packages/validator.yaml"
]
}
}
+18
View File
@@ -0,0 +1,18 @@
Ziel: Bereitstellung einer stabilen Umgebung für die Entwicklung und den Betrieb des MVP.
Technische Vorgaben:
Backend: Symfony Framework (PHP 8.2+).
Datenbank: PostgreSQL.
Identity Provider: Keycloak (Zentrale Authentifizierung via OIDC/JWT).
Frontend: React oder Vue.js mit Tailwind CSS.
Kommunikation: REST-API (JSON) über HTTPS.
Anforderungen an die Umgebung:
Einrichtung eines Docker-Compose-Setups (Container für PHP-FPM, Nginx, PostgreSQL und Keycloak).
Konfiguration der .env-Dateien zur Trennung von Entwicklung und Produktion.
Akzeptanzkriterien:
Symfony-Projekt startet erfolgreich und ist erreichbar.
Datenbankverbindung zu PostgreSQL ist hergestellt.
Keycloak-Instanz ist aufgesetzt; ein Test-Nutzer kann sich authentifizieren und ein JWT erhalten.
+25
View File
@@ -0,0 +1,25 @@
### Finales Anforderungsprofil "GardenPlan"
**1. Kern-Struktur & Zugriff**
* **Hierarchie:** Nutzer $\rightarrow$ Gärten (Multi-Garten) $\rightarrow$ Beete $\rightarrow$ Saisons (flexible Zeiträume).
* **Berechtigungen:** RBAC (Role Based Access Control) auf **Garten-Ebene** (`Owner`, `Editor`, `Viewer`).
* **Sicherheit:** Keycloak für Auth, strikte Datenisolierung via `Garden-ID`.
**2. Pflanzen & Wissen**
* **Datenbasis:** Globaler Read-Only Katalog + individuelle Nutzer-Sorten.
* **Wissen:** Passives Nachschlagewerk für Pflanznachbarschaften (keine automatische Warnung).
**3. Planung & Dynamik**
* **Zeitstrahl:** Informative Ansicht der Anbauphasen.
* **Dynamik:** Die tatsächliche Aussaat ist der "Trigger". Alle abhängigen Termine (Pikieren, Erntevorhersage) verschieben sich automatisch basierend auf dem Differenzdatum zum Plan.
* **Saison-Übernahme:** Kopieren des Plans in eine neue Saison inklusive eines einstellbaren Zeit-Offsets.
**4. Aufgabenmanagement**
* **Automatische Aufgaben:** 1:1 Verknüpfung mit einer Pflanzung (individuelle Termine pro Charge).
* **Manuelle Aufgaben:** Many-to-Many Verknüpfung (eine Aufgabe kann mehrere Pflanzungen betreffen).
* **Tracking:** Präzise Erfassung von Abschlussdatum und Uhrzeit.
**5. Ernte & Analyse**
* **Erfassung:** Einfache Zuordnung zu Sorte + Beet (nicht zwingend an spezifische Pflanzung gebunden).
* **Metriken:** Gleichzeitige Erfassung von Stückzahl und Gewicht möglich.
* **Analyse:** Vergleich der Erträge über verschiedene flexible Saisons hinweg.
+9
View File
@@ -0,0 +1,9 @@
-- ==========================================
-- GardenPlan PostgreSQL Init Script
-- Erstellt Keycloak-Datenbank & -Benutzer
-- ==========================================
CREATE DATABASE keycloak_db;
CREATE USER keycloak WITH PASSWORD 'keycloak_secret';
GRANT ALL PRIVILEGES ON DATABASE keycloak_db TO keycloak;
ALTER USER keycloak CREATEDB;
+377
View File
@@ -0,0 +1,377 @@
{
"id": "gardenplan-realm",
"realm": "gardenplan",
"displayName": "GardenPlan",
"displayNameHtml": "<div class=\"kc-logo-text\"><span>GardenPlan</span></div>",
"notBefore": 0,
"defaultSignatureAlgorithm": "RS256",
"revokeRefreshToken": false,
"refreshTokenMaxReuse": 0,
"accessTokenLifespan": 3600,
"accessTokenLifespanForImplicitFlow": 900,
"ssoSessionIdleTimeout": 3600,
"ssoSessionMaxLifespan": 86400,
"ssoSessionIdleTimeoutRememberMe": 0,
"ssoSessionMaxLifespanRememberMe": 0,
"offlineSessionIdleTimeout": 2592000,
"offlineSessionMaxLifespanEnabled": false,
"offlineSessionMaxLifespan": 5184000,
"clientSessionIdleTimeout": 0,
"clientSessionMaxLifespan": 0,
"clientOfflineSessionIdleTimeout": 0,
"clientOfflineSessionMaxLifespan": 0,
"accessCodeLifespan": 60,
"accessCodeLifespanUserAction": 300,
"accessCodeLifespanLogin": 1800,
"actionTokenGeneratedByAdminLifespan": 43200,
"actionTokenGeneratedByUserLifespan": 3600,
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "external",
"registrationAllowed": true,
"registrationEmailAsUsername": false,
"rememberMe": false,
"verifyEmail": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": false,
"editUsernameAllowed": false,
"bruteForceProtected": false,
"permanentLockout": false,
"maxTemporaryLockouts": 0,
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60,
"quickLoginCheckMilliSlots": 144,
"quickLoginCheckMilliDetails": 200,
"bruteForceStrategy": "MULTI_USER",
"resetPasswordTimeout": 0,
"defaultRole": {
"id": "gardenplan-default-role",
"name": "default-roles-gardenplan",
"description": "${role_default-roles}",
"type": "DEFAULT",
"scope": "GLOBAL"
},
"requiredCredentials": [
"password"
],
"otpPolicyType": "totp",
"otpPolicyAlgorithm": "HmacSHA1",
"otpPolicyInitialCounter": 0,
"otpPolicyDigits": 6,
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpSupportedApplications": [
"FreeOTP",
"Google Authenticator"
],
"localizationTexts": {},
"webAuthnChallengeUsage": "per-session",
"identityProviderAliases": {},
"users": [
{
"id": "test-user-001",
"username": "testuser",
"enabled": true,
"emailVerified": true,
"email": "testuser@gardenplan.local",
"credentials": [
{
"type": "password",
"value": "testpassword123!",
"temporary": false
}
],
"roles": {
"client": {
"symfony-app": [
"uma_authorization"
]
},
"realm": [
"default-roles-gardenplan",
"offline_access",
"uma_authorization"
]
}
}
],
"clients": [
{
"id": "symfony-app-client",
"clientId": "symfony-app",
"name": "Symfony GardenPlan API",
"description": "Backend REST-API Client für GardenPlan",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "changeme_client_secret",
"baseUrl": "http://localhost/",
"redirectUris": [
"http://localhost/*",
"http://127.0.0.1/*"
],
"webOrigins": [
"+"
],
"grantTypes": [
"authorization_code",
"refresh_token",
"client_credentials",
"implicit"
],
"standardFlowEnabled": true,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"publicClient": false,
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"client.secret.creation.time": "1700000000",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"login.jansendata": "{}"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "frontend-client",
"clientId": "gardenplan-frontend",
"name": "GardenPlan Frontend (React/Vue)",
"description": "Frontend SPA Client für GardenPlan",
"enabled": true,
"publicClient": true,
"baseUrl": "http://localhost:3000/",
"redirectUris": [
"http://localhost:3000/*",
"http://127.0.0.1:3000/*"
],
"webOrigins": [
"http://localhost:3000",
"http://127.0.0.1:3000"
],
"grantTypes": [
"authorization_code",
"implicit"
],
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"oidc.ciba.grant.enabled": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"backchannel.logout.session.required": "true"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"defaultClientScopes": [
"web-origins",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
}
],
"clientScopes": [
{
"id": "scope-email",
"name": "email",
"description": "OpenID Connect built-in scope: email",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"consent.screen.text": "${emailScopeConsentText}",
"display.on.consent.screen": "true"
},
"icons": {},
"protocolMappers": [
{
"id": "mapper-email",
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String",
"multivalued": "false"
}
},
{
"id": "mapper-email-verified",
"name": "email verified",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "emailVerified",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email_verified",
"jsonType.label": "boolean",
"multivalued": "false"
}
}
]
},
{
"id": "scope-profile",
"name": "profile",
"description": "OpenID Connect built-in scope: profile",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"consent.screen.text": "${profileScopeConsentText}",
"display.on.consent.screen": "true"
},
"icons": {},
"protocolMappers": [
{
"id": "mapper-profile-name",
"name": "full name",
"protocol": "openid-connect",
"protocolMapper": "oidc-full-name-mapper",
"consentRequired": false,
"config": {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"id": "mapper-username",
"name": "username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
}
]
},
{
"id": "scope-roles",
"name": "roles",
"description": "OpenID Connect scope for role mapping",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"icons": {},
"protocolMappers": [
{
"id": "mapper-client-roles",
"name": "client roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "resource_access.${client_id}.roles",
"jsonType.label": "String"
}
},
{
"id": "mapper-realm-roles",
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String"
}
}
]
},
{
"id": "scope-web-origins",
"name": "web-origins",
"description": "OpenID Connect scope for web origins",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"consent.screen.text": "",
"display.on.consent.screen": "false"
},
"icons": {},
"protocolMappers": [
{
"id": "mapper-web-origins",
"name": "allowed web origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-allowed-origins-mapper",
"consentRequired": false,
"config": {}
}
]
},
{
"id": "scope-offline-access",
"name": "offline_access",
"description": "OpenID Connect built-in scope: offline_access",
"protocol": "openid-connect",
"attributes": {
"consent.screen.text": "${offlineAccessScopeConsentText}",
"display.on.consent.screen": "true"
},
"icons": {}
}
],
"defaultDefaultClientScopes": [
"role_list",
"profile",
"email",
"roles",
"web-origins"
],
"defaultOptionalClientScopes": [
"offline_access",
"address",
"phone"
]
}
+56
View File
@@ -0,0 +1,56 @@
# ==========================================
# GardenPlan Nginx Konfiguration
# Symfony Reverse Proxy + API Endpoint
# ==========================================
server {
listen 80;
server_name _;
root /var/www/html/public;
# Performance-Settings
client_max_body_size 50M;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Default Symfony Backend (REST API)
location / {
try_files $uri /index.php$is_args$args;
}
# PHP-FPM Verbindung
location ~ ^/index\.php(/|$) {
fastcgi_pass php-fpm:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS off;
# PHP-FPM Timeout-Settings
fastcgi_connect_timeout 600;
fastcgi_send_timeout 600;
fastcgi_read_timeout 600;
}
# Verzeichnis-Zugriffe blockieren
location ~ /^.ht {
deny all;
}
# Assets Cache-Control
location ~* \.(jpg|jpeg|gif|png|webp|svg|css|js|woff2?|ico)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Error Pages (optional)
error_page 404 /error.html;
error_page 500 502 503 504 /error.html;
}
+46
View File
@@ -0,0 +1,46 @@
# ==========================================
# GardenPlan PHP-FPM Dockerfile
# Symfony 7 / PHP 8.2 + erforderliche Extensions
# ==========================================
FROM php:8.4-fpm-bookworm AS base
# System-Abhängigkeiten installieren
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
libpq-dev \
libzip-dev \
zip \
unzip \
libfreetype6-dev \
libjpeg62-turbo-dev \
libwebp-dev \
&& rm -rf /var/lib/apt/lists/*
# PHP Extensions kompilieren & installieren
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_pgsql xml zip mbstring opcache
# Composer installieren
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions intl apcu
# Node.js & npm für Frontend-Build (optional in Backend)
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g pnpm
WORKDIR /var/www/html
# Symfony CLI installieren (optional, nützlich für Local-Tunnel etc.)
RUN curl -sS https://get.symfony.com/cli/installer | bash \
&& mv /root/.symfony5/bin/symfony /usr/local/bin/symfony
EXPOSE 9000
CMD ["php-fpm"]
+105
View File
@@ -0,0 +1,105 @@
# 🌿 GardenPlan Gartenplanung MVP
> Symfony-Basises für die Multi-Garten-Planung mit Pflanzen, Aussaat, Ernte & Aufgaben.
## 🏗️ Architektur
Strikte Schichtenarchitektur (Clean / Hexagonal):
**UI****Logic****Data**
Details: [Architektur-Dokumentation](Doku/architektur.md)
## 📋 Anforderungsprofil
[Hier ansehen](Doku/anforderungsprofil.md)
---
## 🚀 Schnellstart
### 1. DockerCompose-Setup starten
```bash
docker compose up -d
```
Startet folgende Services:
| Service | Port | Beschreibung |
|---------|------|--------------
| **nginx** | `80` / `443` | Webserver für Symfony REST-API
| **php-fpm** | (intern) | PHP 8.3 + Symfony Runtime
| **PostgreSQL** | `5432` | Datenbank (`gardenplan` + `keycloak_db`)
| **Keycloak** | `8080` | OIDC/JWT Identity Provider
### 🔑 Zugangsdaten (Dev)
| System | Benutzer | Passwort | URL |
|--------|----------|----------|-----|
| **Database** | `symfony` | `changeme` | `postgresql://database:5432/gardenplan` |
| **Keycloak Admin** | `admin` | `adminsecret` | http://localhost:8080 |
| **Test-Nutzer** | `testuser` | `testpassword123!` | (in Keycloak Realm `gardenplan`)
### 📦 Nach dem ersten Start
```bash
# In den PHP-Container wechseln
docker compose exec php-fpm bash
# Symfony Projekt setup
cd /var/www/html
composer install
# Datenbankmigrationen ausführen
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate --no-interaction
# JWT-Schlüssel für Lexik Bundle generieren
php bin/console lexik:jwt:generate-keypair
```
---
## 📐 Ordnerstruktur
```
src/
├── UI/ # Controller, Commands, MessageHandlers
├── Logic/ # UseCases, Manager, Domain Services
│ ├── Domain/ # Models, DTOs, Validators, Policies
│ └── InfrastructureInterface/ # Provider / Processor Interfaces
└── Data/ # Doctrine Entities, Mappers, Implementations
```
---
## 📖 Aufgabenplan
| Task | Beschreibung | Status |
|------|-------------|--------|
| [01 Infrastruktur](Doku/Tasks/01-Infra.md) | Docker, DB, Keycloak Setup | ✅ In Arbeit |
| 02 Datenbank-Schema | Entities + Migrationen | ⏳ Offen |
| 03 Planning-Domain | GardenPlan Model, UseCases | ⏳ Offen |
| 04 REST API | Controller + DTOs | ⏳ Offen |
| 05 Auth & RBAC Keycloak Integration | ⏳ Offen |
---
## 🔧 Entwicklung
```bash
# Logs einsehen
docker compose logs -f php-fpm
docker compose logs -f nginx
# Container neu starten
docker compose restart php-fpm
# Alles stoppen
docker compose down
```
## .env-Variabeln kopieren & anpassen. Siehe `.env`.
---
**🌱 Happy Gardening!**
+121
View File
@@ -0,0 +1,121 @@
# ==========================================
# GardenPlan Docker Compose (Development)
# ==========================================
services:
# --------------------------------------------------
# PHP-FPM Symfony Backend
# --------------------------------------------------
php-fpm:
build:
context: ./Infra/php-fpm
dockerfile: Dockerfile
container_name: gardenplan-php-fpm
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./Backend:/var/www/html:z
- phpsocket:/var/run/php
networks:
- gardenplan-network
depends_on:
database:
condition: service_healthy
# --------------------------------------------------
# Nginx Webserver & Reverse Proxy
# --------------------------------------------------
nginx:
image: nginx:1.25-alpine
container_name: gardenplan-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Infra/nginx/default.conf:/etc/nginx/conf.d/default.conf:z
- ./Backend/public:/var/www/html/public:ro,z
- nginx_cache:/var/cache/nginx
networks:
- gardenplan-network
depends_on:
- php-fpm
# --------------------------------------------------
# PostgreSQL Datenbank
# --------------------------------------------------
database:
image: postgres:16-alpine
container_name: gardenplan-database
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_DB: ${POSTGRES_DB:-gardenplan}
POSTGRES_USER: ${POSTGRES_USER:-symfony}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- ./Infra/database/init:/docker-entrypoint-initdb.d:z
networks:
- gardenplan-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-symfony} -d ${POSTGRES_DB:-gardenplan}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# --------------------------------------------------
# Keycloak Identity Provider (OIDC / JWT)
# --------------------------------------------------
keycloak:
image: quay.io/keycloak/keycloak:24.0
container_name: gardenplan-keycloak
restart: unless-stopped
ports:
- "8080:8080"
environment:
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-adminsecret}
KC_DATABASE: postgres
KC_DATABASE_URL: jdbc:postgresql://database:5432/keycloak_db
KC_DATABASE_USERNAME: keycloak
KC_DATABASE_PASSWORD: keycloak_secret
KC_HTTP_RELATIVE_PATH: /auth
KC_HOSTNAME: localhost
KC_HOSTNAME_STRICT: "false"
KC_FEATURES: scripts
command: start-dev
volumes:
- ./Infra/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:z
networks:
- gardenplan-network
depends_on:
database:
condition: service_healthy
# --------------------------------------------------
# Keycloak Database (getrennt, falls gewünscht)
# Hier verwenden wir die Haupt-DB mit eigenem Schema
# Alternative: extra DB-Container hier vereinfacht
# --------------------------------------------------
# ==========================================
# Volumes
# ==========================================
volumes:
postgres_data:
driver: local
phpsocket:
driver: local
nginx_cache:
driver: local
# ==========================================
# Networks
# ==========================================
networks:
gardenplan-network:
driver: bridge