diff --git a/.gitignore b/.gitignore index b71b1ea..656cf1b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Homestead.json Homestead.yaml Thumbs.db +/docker-compose.yaml diff --git a/Makefile b/Makefile index fbaa382..8baf640 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ FRONTEND_DIR ?= . + setup: + rm -f docker-compose.yaml + cp docker-compose.dev docker-compose.yaml + docker-compose up -d + frontend: @cd $(FRONTEND_DIR) && \ export QT_QPA_PLATFORM=offscreen && \ diff --git a/app/Enumerations/CostUnitType.php b/app/Enumerations/CostUnitType.php new file mode 100644 index 0000000..855409b --- /dev/null +++ b/app/Enumerations/CostUnitType.php @@ -0,0 +1,21 @@ +installTenants(); + $this->installUsers(); + } + + + + private function installTenants() { + Tenant::create([ + 'slug' => 'wilde-moehre', + 'local_group_name' => 'Stamm Wilde Möhre', + 'url' => 'wilde-moehre.mareike.local', + 'account_iban' => 'DE12345678901234567890', + 'email' => 'test@example1.com', + 'city' => 'Halle (Saale)', + 'postcode' => '06120', + 'is_active_local_group' => true, + 'has_active_instance' => true, + ]); + } + + private function installUsers() { + User::create([ + 'firstname' => 'Development', + 'lastname' => 'User', + 'user_role' => UserRole::USER_ROLE_ADMIN, + 'tenant' => 'lv', + 'email' => 'th.guenther@saale-mail.de', + 'password' => bcrypt('development'), + 'local_group_id' => 1, + 'username' => 'development', + ]); + } +} diff --git a/app/Installer/ProductionDataSeeder.php b/app/Installer/ProductionDataSeeder.php new file mode 100644 index 0000000..dcebdd5 --- /dev/null +++ b/app/Installer/ProductionDataSeeder.php @@ -0,0 +1,71 @@ +installUserRoles(); + $this->installCostUnitTypes(); + $this->installSwimmingPermissions(); + $this->installEatingHabits(); + $this->installFirstAidPermissions(); + $this->installTenants(); + } + + private function installUserRoles() { + UserRole::create(['name' => 'Administrator*in', 'slug' => UserRole::USER_ROLE_ADMIN]); + UserRole::create(['name' => 'Vorstandsmitglied', 'slug' => UserRole::USER_ROLE_GROUP_LEADER]); + UserRole::create(['name' => 'Benutzer*in', 'slug' => UserRole::USER_ROLE_USER]); + } + + private function installSwimmingPermissions() { + SwimmingPermission::create(['name' => 'Mein Kind darf baden und kann schwimmen', 'slug' => SwimmingPermission::SWIMMING_PERMISSION_ALLOWED]); + SwimmingPermission::create(['name' => 'Mein Kind darf baden und kann NICHT schwimmen', 'slug' => SwimmingPermission::SWIMMING_PERMISSION_LIMITED]); + SwimmingPermission::create(['name' => 'Mein Kind darf nicht baden', 'slug' => SwimmingPermission::SWIMMING_PERMISSION_DENIED]); + } + + private function installEatingHabits() { + EatingHabit::create(['name' => 'Vegan', 'slug' => EatingHabit::EATING_HABIT_VEGAN]); + EatingHabit::create(['name' => 'Vegetarisch', 'slug' => EatingHabit::EATING_HABIT_VEGETARIAN]); + EatingHabit::create(['name' => 'Omnivor', 'slug' => EatingHabit::EATING_HABIT_OMNIVOR]); + + } + private function installFirstAidPermissions() { + FirstAidPermission::create([ + 'name' => 'Zugestimmt', + 'description' => 'Ich STIMME der Anwendung von erweiteren Erste-Hilfe-Maßnahmen an meinem Kind explizit ZU.', + 'slug' => FirstAidPermission::FIRST_AID_PERMISSION_ALLOWED]); + + FirstAidPermission::create([ + 'name' => 'Verweigert', + 'description' => 'Ich LEHNE die Anwendung von erweiteren Erste-Hilfe-Maßnahmen an meinem Kind explizit AB.', + 'slug' => FirstAidPermission::FIRST_AID_PERMISSION_DENIED]); + } + + private function installCostUnitTypes() { + CostUnitType::create(['slug' => CostUnitType::COST_UNIT_TYPE_EVENT, 'name' => 'Veranstaltung']); + CostUnitType::create(['slug' => CostUnitType::COST_UNIT_TYPE_RUNNING_JOB, 'name' => 'Laufende Tätigkeit']); + + } + + private function installTenants() { + Tenant::create([ + 'slug' => 'lv', + 'local_group_name' => 'Landesunmittelbare Mitglieder', + 'url' => 'mareike.local', + 'account_iban' => 'DE12345678901234567890', + 'email' => 'test@example.com', + 'city' => 'Lommatzsch', + 'postcode' => '01623', + 'is_active_local_group' => true, + 'has_active_instance' => true, + ]); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Middleware/HandleInertiaRequests.php similarity index 96% rename from app/Http/Middleware/HandleInertiaRequests.php rename to app/Middleware/HandleInertiaRequests.php index c19ce18..e20fd21 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Middleware/HandleInertiaRequests.php @@ -1,6 +1,6 @@ getHost(); + $tenant = Tenant::where(['url' => $host, 'has_active_instance' => true])->first(); + + if (! $tenant) { + throw new NotFoundHttpException('Tenant not found'); + } + + app()->instance('tenant', $tenant); + return $next($request); + } +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php new file mode 100644 index 0000000..35bed28 --- /dev/null +++ b/app/Models/Tenant.php @@ -0,0 +1,39 @@ + */ - use HasFactory, Notifiable; + use Notifiable; /** * The attributes that are mass assignable. @@ -18,8 +15,28 @@ class User extends Authenticatable * @var list */ protected $fillable = [ - 'name', + 'tenant_id', + 'user_role', + 'username', + 'firstname', + 'nickname', + 'lastname', + 'local_group_id', + 'membership_id', + 'address_1', + 'address_2', + 'postcode', + 'city', 'email', + 'phone', + 'birthday', + 'medications', + 'allergies', + 'intolerances', + 'eating_habits', + 'swimming_permission', + 'first_aid_permission', + 'bank_account_iban', 'password', ]; @@ -29,7 +46,6 @@ class User extends Authenticatable * @var list */ protected $hidden = [ - 'password', 'remember_token', ]; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..7a8c57f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +20,11 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Auth::provider('tenant-users', function ($app, array $config) { + return new TenantUserProvider( + $app['hash'], + $config['model'] + ); + }); } } diff --git a/app/Providers/InertiaProvider.php b/app/Providers/InertiaProvider.php new file mode 100644 index 0000000..e4cf233 --- /dev/null +++ b/app/Providers/InertiaProvider.php @@ -0,0 +1,26 @@ +vueFile = $vueFile; + $this->props = $props; + + } + + + public function render() : Response { + return Inertia::render( + str_replace('/', '/Views/', $this->vueFile), + $this->props + ); + } +} diff --git a/app/Providers/TenantUserProvider.php b/app/Providers/TenantUserProvider.php new file mode 100644 index 0000000..51702b9 --- /dev/null +++ b/app/Providers/TenantUserProvider.php @@ -0,0 +1,23 @@ +createModel()->newQuery(); + + foreach ($credentials as $key => $value) { + if (! str_contains($key, 'password')) { + $query->where($key, $value); + } + } + + $query->where('tenant', app('tenant')->slug); + + return $query->first(); + } +} diff --git a/app/Scopes/CommonModel.php b/app/Scopes/CommonModel.php new file mode 100644 index 0000000..b937fc2 --- /dev/null +++ b/app/Scopes/CommonModel.php @@ -0,0 +1,10 @@ +where($model->getTable() . '.tenant', app('tenant')->slug); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..2b5ff81 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', + api: __DIR__.'/../routes/api.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->append(IdentifyTenant::class); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..c93fc13 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + ]; diff --git a/composer.json b/composer.json index 22311da..e7da8ea 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "keywords": ["laravel", "framework"], "license": "MIT", "require": { - "php": "^8.2", + "php": "^8.5", "inertiajs/inertia-laravel": "^2.0", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1" diff --git a/composer.lock b/composer.lock index 452325b..13f363a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0e42fe1a7066e7a110e956ae26703d94", + "content-hash": "587caff9de06de75c1e22cceac366334", "packages": [ { "name": "brick/math", @@ -2194,16 +2194,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.0", + "version": "3.11.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1" + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", "shasum": "" }, "require": { @@ -2227,7 +2227,7 @@ "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", - "squizlabs/php_codesniffer": "^3.13.4" + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -2270,14 +2270,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -2295,7 +2295,7 @@ "type": "tidelift" } ], - "time": "2025-12-02T21:04:28+00:00" + "time": "2026-01-29T09:26:29+00:00" }, { "name": "nette/schema", @@ -8436,7 +8436,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.5" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..e6e4ace 100644 --- a/config/auth.php +++ b/config/auth.php @@ -61,8 +61,8 @@ return [ 'providers' => [ 'users' => [ - 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\User::class), + 'driver' => 'tenant-users', + 'model' => App\Models\User::class, ], // 'users' => [ diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php deleted file mode 100644 index 584104c..0000000 --- a/database/factories/UserFactory.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class UserFactory extends Factory -{ - /** - * The current password being used by the factory. - */ - protected static ?string $password; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), - ]; - } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } -} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 05fb5d9..0000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/database/migrations/2026_01_30_140001_create_tenants.php b/database/migrations/2026_01_30_140001_create_tenants.php new file mode 100644 index 0000000..ddd4f7e --- /dev/null +++ b/database/migrations/2026_01_30_140001_create_tenants.php @@ -0,0 +1,32 @@ +id(); + $table->string('slug')->unique(); + $table->string('local_group_name'); + $table->string('email'); + $table->string('url'); + $table->string('account_iban'); + $table->string('city'); + $table->string('postcode'); + $table->string('gdpr_text')-> nullable(); + $table->string('impress_text')->nullable(); + $table->string('url_participation_rules')->nullable(); + $table->boolean('has_active_instance')->default(true); + $table->boolean('is_active_local_group')->default(true); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('tenants'); + } +}; diff --git a/database/migrations/2026_01_30_140002_create_users_table.php b/database/migrations/2026_01_30_140002_create_users_table.php new file mode 100644 index 0000000..5833b76 --- /dev/null +++ b/database/migrations/2026_01_30_140002_create_users_table.php @@ -0,0 +1,106 @@ +string('slug')->unique()->primary(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('eating_habits', function (Blueprint $table) { + $table->string('slug')->unique()->primary(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('swimming_permissions', function (Blueprint $table) { + $table->string('slug')->unique()->primary(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('first_aid_permissions', function (Blueprint $table) { + $table->string('slug')->unique()->primary(); + $table->string('name'); + $table->string('description'); + $table->timestamps(); + }); + + Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->string('tenant'); + $table->string('user_role'); + $table->string('username')->unique(); + $table->string('password')->nullable(); + $table->string('firstname'); + $table->string('nickname')->nullable(); + $table->string('lastname'); + $table->foreignId('local_group_id')->references('id')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate(); + $table->string('membership_id')->nullable(); + $table->string('address_1')->nullable(); + $table->string('address_2')->nullable(); + $table->string('postcode')->nullable(); + $table->string('city')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->date('birthday')->nullable(); + $table->string('medications')->nullable(); + $table->string('allergies')->nullable(); + $table->string('intolerances')->nullable(); + $table->string('eating_habits')->nullable(); + $table->string('swimming_permission')->nullable(); + $table->string('first_aid_permission')->nullable(); + $table->string('bank_account_iban')->nullable(); + $table->string('activation_token')->nullable(); + $table->boolean('activated')->default(false); + + $table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('user_role')->references('slug')->on('user_roles')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('swimming_permission')->references('slug')->on('swimming_permissions')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('eating_habits')->references('slug')->on('eating_habits')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('first_aid_permission')->references('slug')->on('first_aid_permissions')->cascadeOnDelete()->cascadeOnUpdate(); + + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + Schema::dropIfExists('user_roles'); + Schema::dropIfExists('eating_habits'); + Schema::dropIfExists('swimming_permissions'); + Schema::dropIfExists('first_aid_permissions'); + } +}; diff --git a/database/migrations/2026_01_30_140010_create_cost_units.php b/database/migrations/2026_01_30_140010_create_cost_units.php new file mode 100644 index 0000000..8856e89 --- /dev/null +++ b/database/migrations/2026_01_30_140010_create_cost_units.php @@ -0,0 +1,43 @@ +string('slug')->primary(); + $table->string('name'); + $table->timestamps(); + }); + + + + Schema::create('cost_units', function (Blueprint $table) { + $table->id(); + $table->string('tenant'); + $table->string('name'); + $table->string('type'); + $table->dateTime('billing_deadline'); + $table->float('distance_allowance'); + $table->boolean('mail_on_new')->default(true); + $table->boolean('allow_new')->default(true); + $table->boolean('archived')->default(false); + $table->string('treasurers'); + + $table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('type')->references('slug')->on('cost_unit_types')->cascadeOnDelete()->cascadeOnUpdate(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('cost_units'); + Schema::dropIfExists('cost_unit_types'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..a8a01af 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,7 +2,9 @@ namespace Database\Seeders; -use App\Models\User; +use App\Installer\DevelopmentDataSeeder; +use App\Installer\ProductionData; +use App\Installer\ProductionDataSeeder; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -15,11 +17,12 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); + $productionSeeeder = new ProductionDataSeeder(); + $productionSeeeder->execute(); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + if (str_ends_with(env('APP_URL'), 'mareike.local')) { + $deveopmentDataSeeder = new DevelopmentDataSeeder(); + $deveopmentDataSeeder->execute(); + } } } diff --git a/docker-compose.yaml b/docker-compose.dev similarity index 92% rename from docker-compose.yaml rename to docker-compose.dev index 95a230a..6a7134d 100644 --- a/docker-compose.yaml +++ b/docker-compose.dev @@ -18,12 +18,12 @@ services: labels: - "traefik.enable=true" - - "traefik.http.routers.mareike.rule=Host(`mareike.local`)" + - "traefik.http.routers.mareike.rule=Host(`mareike.local`) || Host(`admin.mareike.local`) || Host(`wilde-moehre.mareike.local`)" - "traefik.http.routers.mareike.entrypoints=websecure" - "traefik.http.routers.mareike.tls=true" - "traefik.http.services.mareike.loadbalancer.server.port=80" - - "traefik.http.routers.mareike-http.rule=Host(`mareike.local`)" + - "traefik.http.routers.mareike-http.rule=Host(`mareike.local`) || Host(`admin.mareike.local`) || Host(`wilde-moehre.mareike.local`)" - "traefik.http.routers.mareike-http.entrypoints=web" - "traefik.http.routers.mareike-http.middlewares=redirect-to-https" diff --git a/resources/js/app.js b/resources/js/app.js index 104602f..05e5ddc 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -5,43 +5,32 @@ import { InertiaProgress } from '@inertiajs/progress' import Vue3Toastify, { toast } from 'vue3-toastify' import 'vue3-toastify/dist/index.css' -// Optional: Lade-Balken für Inertia InertiaProgress.init() -// Inertia App starten createInertiaApp({ - // Alle Pages in app/Views/Pages/**/*.vue werden automatisch importiert resolve: name => { - // Vite scannt die Pages dynamisch - const pages = import.meta.glob('@views/**/*.vue') + const pages = import.meta.glob('@domains/**/*.vue') - // Suche nach der richtigen Page-Datei const key = Object.keys(pages).find(k => k.endsWith(`/${name}.vue`) || k.endsWith(`/${name}/index.vue`) ) if (!key) throw new Error(`Page not found: ${name}`) - // Unterstützt sowohl