diff --git a/docs/implementation-foundation.md b/docs/implementation-foundation.md new file mode 100644 index 0000000..c472151 --- /dev/null +++ b/docs/implementation-foundation.md @@ -0,0 +1,33 @@ +# Implementation Foundation + +Das neue Zielprojekt liegt in `saas-app/`, damit der bisherige PHP-Bestand +unangetastet als Referenz bestehen bleibt. + +Foundation-Stand: + +- Laravel-nahe Ordnerstruktur ohne externe Downloads +- Tenant-Resolution-Skelett ueber Host/Subdomain +- Request-Context-Platzhalter +- Grundrouten fuer Landing, Login und Dashboard +- erste SQL-Migrationsskizzen fuer: + - `tenants` + - `users` + - `tenant_users` + - `roles` + - `tenant_user_roles` +- Blade-Layouts als Platzhalter fuer SSR-Ansatz auf Webspace + +Naechste Programmierphase: + +1. Identity-/Tenant-Agent auf `saas-app/app` und Auth-/Tenant-Module +2. Ledger-/Core-Agent auf Mitglieder, Striche, Einzahlungen und Dashboard +3. spaeter Operations-Agent fuer Importe, Mail, Umfragen und Exporte + +Blocker ausserhalb des Repos: + +- `php` lokal nicht installiert +- `composer` lokal nicht installiert +- echtes Laravel-Scaffolding daher noch nicht moeglich + +Sobald Tooling vorhanden ist, soll dieses Geruest in ein vollstaendiges +Laravel-Projekt ueberfuehrt werden. diff --git a/saas-app/.env.example b/saas-app/.env.example new file mode 100644 index 0000000..bfd7fe0 --- /dev/null +++ b/saas-app/.env.example @@ -0,0 +1,34 @@ +APP_NAME=KaffeelisteSaaS +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=sqlsrv +DB_HOST=127.0.0.1 +DB_PORT=1433 +DB_DATABASE=kaffeeliste_saas +DB_USERNAME=saas_user +DB_PASSWORD= + +SESSION_DRIVER=file +QUEUE_CONNECTION=database +CACHE_STORE=file + +MAIL_MAILER=smtp +MAIL_HOST=127.0.0.1 +MAIL_PORT=1025 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_FROM_ADDRESS="noreply@example.test" +MAIL_FROM_NAME="${APP_NAME}" + +TENANCY_MODE=subdomain +TENANCY_FALLBACK_TENANT= +TENANCY_CENTRAL_DOMAINS=localhost + +OIDC_ENABLED=false +OIDC_DEFAULT_PROVIDER= diff --git a/saas-app/README.md b/saas-app/README.md new file mode 100644 index 0000000..7518f25 --- /dev/null +++ b/saas-app/README.md @@ -0,0 +1,26 @@ +# SaaS App Foundation + +Dieses Verzeichnis enthaelt das neue, Laravel-nahe Zielprojekt fuer die +mandantenfaehige SaaS-Version. + +Aktueller Stand: + +- downloadfreies Foundation-Geruest +- modulare Zielstruktur fuer Webspace-Betrieb +- Tenant-Resolution-Skelett +- Basismigrationen fuer Mandanten, Benutzer und Rollen +- einfache Web-Routen und Blade-Platzhalter + +Zielrahmen: + +- klassischer Webspace / PHP-Hosting +- SSR-orientiert +- Cron statt dauerhaft laufender Worker +- OIDC zuerst, SAML spaeter optional + +Hinweis: + +Dieses Geruest ersetzt noch kein vollstaendig installiertes Laravel-Projekt. +Sobald `php` und `composer` verfuegbar sind, soll auf dieser Struktur ein +vollwertiges Laravel-Projekt aufgesetzt oder diese Struktur in ein solches +ueberfuehrt werden. diff --git a/saas-app/app/Http/Middleware/ResolveTenant.php b/saas-app/app/Http/Middleware/ResolveTenant.php new file mode 100644 index 0000000..f253a7c --- /dev/null +++ b/saas-app/app/Http/Middleware/ResolveTenant.php @@ -0,0 +1,33 @@ +getHost() + : ''; + + $tenant = $this->tenantResolver->resolveFromHost($host); + + if ($tenant !== null) { + $this->requestContext->setTenant( + $tenant['tenant_id'], + $tenant['tenant_key'] + ); + } + + return $next($request); + } +} diff --git a/saas-app/app/Modules/Content/Application/ContentService.php b/saas-app/app/Modules/Content/Application/ContentService.php new file mode 100644 index 0000000..2ea68cc --- /dev/null +++ b/saas-app/app/Modules/Content/Application/ContentService.php @@ -0,0 +1,45 @@ + + */ + public function activeAnnouncements(string $tenantId): array + { + return [ + new Announcement( + id: 'announcement-demo-1', + tenantId: $tenantId, + title: 'Willkommen', + message: 'Das Content-Modul liefert spaeter Hinweise und tenantbezogene Meldungen.', + visibleUntil: '2026-12-31 23:59:59', + active: true, + ), + ]; + } + + /** + * @return array + */ + public function faqItems(string $tenantId): array + { + return [ + new FaqItem( + id: 'faq-demo-1', + tenantId: $tenantId, + question: 'Wie werden Hinweise gepflegt?', + answer: 'Spaeter ueber den Tenant-Adminbereich mit tenantbezogener Sichtbarkeit.', + sortOrder: 10, + active: true, + ), + ]; + } +} diff --git a/saas-app/app/Modules/Content/Domain/Announcement.php b/saas-app/app/Modules/Content/Domain/Announcement.php new file mode 100644 index 0000000..4609679 --- /dev/null +++ b/saas-app/app/Modules/Content/Domain/Announcement.php @@ -0,0 +1,48 @@ +id; + } + + public function tenantId(): string + { + return $this->tenantId; + } + + public function title(): string + { + return $this->title; + } + + public function message(): string + { + return $this->message; + } + + public function visibleUntil(): string + { + return $this->visibleUntil; + } + + public function active(): bool + { + return $this->active; + } +} diff --git a/saas-app/app/Modules/Content/Domain/FaqItem.php b/saas-app/app/Modules/Content/Domain/FaqItem.php new file mode 100644 index 0000000..3e87111 --- /dev/null +++ b/saas-app/app/Modules/Content/Domain/FaqItem.php @@ -0,0 +1,48 @@ +id; + } + + public function tenantId(): string + { + return $this->tenantId; + } + + public function question(): string + { + return $this->question; + } + + public function answer(): string + { + return $this->answer; + } + + public function sortOrder(): int + { + return $this->sortOrder; + } + + public function active(): bool + { + return $this->active; + } +} diff --git a/saas-app/app/Modules/Exports/Application/ExportService.php b/saas-app/app/Modules/Exports/Application/ExportService.php new file mode 100644 index 0000000..7827df5 --- /dev/null +++ b/saas-app/app/Modules/Exports/Application/ExportService.php @@ -0,0 +1,26 @@ + + */ + public function recentExports(string $tenantId): array + { + return [ + new ExportJob( + id: 'export-demo-1', + tenantId: $tenantId, + exportType: 'ledger-csv', + status: 'ready', + targetPath: 'exports/tenant-demo/ledger-2026-03.csv', + ), + ]; + } +} diff --git a/saas-app/app/Modules/Exports/Jobs/ExportJob.php b/saas-app/app/Modules/Exports/Jobs/ExportJob.php new file mode 100644 index 0000000..43b9966 --- /dev/null +++ b/saas-app/app/Modules/Exports/Jobs/ExportJob.php @@ -0,0 +1,42 @@ +id; + } + + public function tenantId(): string + { + return $this->tenantId; + } + + public function exportType(): string + { + return $this->exportType; + } + + public function status(): string + { + return $this->status; + } + + public function targetPath(): string + { + return $this->targetPath; + } +} diff --git a/saas-app/app/Modules/Identity/Controllers/ForgotPasswordController.php b/saas-app/app/Modules/Identity/Controllers/ForgotPasswordController.php new file mode 100644 index 0000000..6c409eb --- /dev/null +++ b/saas-app/app/Modules/Identity/Controllers/ForgotPasswordController.php @@ -0,0 +1,26 @@ + 'auth.forgot-password', + 'data' => [ + 'title' => 'Passwort zuruecksetzen', + ], + ]; + } + + public function sendResetLink(array $payload): array + { + return [ + 'status' => 'queued-placeholder', + 'email' => $payload['email'] ?? null, + ]; + } +} diff --git a/saas-app/app/Modules/Identity/Controllers/LoginController.php b/saas-app/app/Modules/Identity/Controllers/LoginController.php new file mode 100644 index 0000000..c4c4bb0 --- /dev/null +++ b/saas-app/app/Modules/Identity/Controllers/LoginController.php @@ -0,0 +1,30 @@ + 'auth.login', + 'data' => [ + 'title' => 'Anmeldung', + 'providers' => $this->authService->availableProviders(), + ], + ]; + } + + public function authenticate(array $payload): array + { + return $this->authService->attemptLogin($payload); + } +} diff --git a/saas-app/app/Modules/Identity/Controllers/OidcController.php b/saas-app/app/Modules/Identity/Controllers/OidcController.php new file mode 100644 index 0000000..1c6e50e --- /dev/null +++ b/saas-app/app/Modules/Identity/Controllers/OidcController.php @@ -0,0 +1,38 @@ + $this->providerService->configuredProviders(), + ]; + } + + public function start(string $providerKey): array + { + return [ + 'status' => 'redirect-placeholder', + 'provider' => $providerKey, + ]; + } + + public function callback(string $providerKey, array $claims = []): array + { + return [ + 'status' => 'callback-placeholder', + 'provider' => $providerKey, + 'claims' => $claims, + ]; + } +} diff --git a/saas-app/app/Modules/Identity/Services/AuthService.php b/saas-app/app/Modules/Identity/Services/AuthService.php new file mode 100644 index 0000000..a05517c --- /dev/null +++ b/saas-app/app/Modules/Identity/Services/AuthService.php @@ -0,0 +1,30 @@ + [ + 'label' => 'Lokaler Login', + 'type' => 'password', + ], + 'oidc' => [ + 'label' => 'SSO via OIDC', + 'type' => 'oidc', + ], + ]; + } + + public function attemptLogin(array $payload): array + { + return [ + 'status' => 'not-implemented', + 'email' => $payload['email'] ?? null, + ]; + } +} diff --git a/saas-app/app/Modules/Identity/Services/OidcProviderService.php b/saas-app/app/Modules/Identity/Services/OidcProviderService.php new file mode 100644 index 0000000..b7496d2 --- /dev/null +++ b/saas-app/app/Modules/Identity/Services/OidcProviderService.php @@ -0,0 +1,26 @@ + + */ + public function configuredProviders(): array + { + return [ + new OidcProviderConfig( + providerKey: 'entra-default', + driver: 'oidc', + clientId: 'tenant-client-id', + redirectUri: '/auth/oidc/entra-default/callback', + scopes: ['openid', 'profile', 'email'] + ), + ]; + } +} diff --git a/saas-app/app/Modules/Identity/Support/OidcProviderConfig.php b/saas-app/app/Modules/Identity/Support/OidcProviderConfig.php new file mode 100644 index 0000000..cd2cda0 --- /dev/null +++ b/saas-app/app/Modules/Identity/Support/OidcProviderConfig.php @@ -0,0 +1,31 @@ + $scopes + */ + public function __construct( + public readonly string $providerKey, + public readonly string $driver, + public readonly string $clientId, + public readonly string $redirectUri, + public readonly array $scopes, + ) { + } + + public function toArray(): array + { + return [ + 'provider_key' => $this->providerKey, + 'driver' => $this->driver, + 'client_id' => $this->clientId, + 'redirect_uri' => $this->redirectUri, + 'scopes' => $this->scopes, + ]; + } +} diff --git a/saas-app/app/Modules/Imports/Application/ImportService.php b/saas-app/app/Modules/Imports/Application/ImportService.php new file mode 100644 index 0000000..f02da3c --- /dev/null +++ b/saas-app/app/Modules/Imports/Application/ImportService.php @@ -0,0 +1,27 @@ + + */ + public function pendingJobs(string $tenantId): array + { + return [ + new ImportJob( + id: 'import-demo-1', + tenantId: $tenantId, + type: 'members-csv', + status: 'pending', + scheduledAt: '2026-03-20 12:00:00', + runViaCron: true, + ), + ]; + } +} diff --git a/saas-app/app/Modules/Imports/Jobs/ImportJob.php b/saas-app/app/Modules/Imports/Jobs/ImportJob.php new file mode 100644 index 0000000..87865d4 --- /dev/null +++ b/saas-app/app/Modules/Imports/Jobs/ImportJob.php @@ -0,0 +1,48 @@ +id; + } + + public function tenantId(): string + { + return $this->tenantId; + } + + public function type(): string + { + return $this->type; + } + + public function status(): string + { + return $this->status; + } + + public function scheduledAt(): string + { + return $this->scheduledAt; + } + + public function runViaCron(): bool + { + return $this->runViaCron; + } +} diff --git a/saas-app/app/Modules/Ledger/Application/DashboardService.php b/saas-app/app/Modules/Ledger/Application/DashboardService.php new file mode 100644 index 0000000..6066946 --- /dev/null +++ b/saas-app/app/Modules/Ledger/Application/DashboardService.php @@ -0,0 +1,21 @@ +balanceForMember($tenantId, $memberId); + + return [ + 'balance' => $balance, + 'coffee_strokes_this_month' => 5, + 'payments_this_month' => 1, + 'latest_booking_at' => '2026-03-20 10:30:00', + ]; + } +} diff --git a/saas-app/app/Modules/Ledger/Application/LedgerService.php b/saas-app/app/Modules/Ledger/Application/LedgerService.php new file mode 100644 index 0000000..3a63af3 --- /dev/null +++ b/saas-app/app/Modules/Ledger/Application/LedgerService.php @@ -0,0 +1,50 @@ + + */ + public function recentEntries(string $tenantId, string $memberId): array + { + return [ + new LedgerEntry( + id: 'ledger-demo-1', + tenantId: $tenantId, + memberId: $memberId, + entryType: 'payment', + amount: 10.00, + bookedAt: '2026-03-20 09:00:00', + referenceType: 'payment', + referenceId: 'payment-demo-1' + ), + new LedgerEntry( + id: 'ledger-demo-2', + tenantId: $tenantId, + memberId: $memberId, + entryType: 'consumption', + amount: -2.50, + bookedAt: '2026-03-20 10:30:00', + referenceType: 'coffee_entry', + referenceId: 'coffee-demo-1' + ), + ]; + } + + public function balanceForMember(string $tenantId, string $memberId): float + { + $balance = 0.0; + + foreach ($this->recentEntries($tenantId, $memberId) as $entry) { + $balance += $entry->amount(); + } + + return $balance; + } +} diff --git a/saas-app/app/Modules/Ledger/Domain/CoffeeEntry.php b/saas-app/app/Modules/Ledger/Domain/CoffeeEntry.php new file mode 100644 index 0000000..e286acc --- /dev/null +++ b/saas-app/app/Modules/Ledger/Domain/CoffeeEntry.php @@ -0,0 +1,23 @@ +strokes * $this->unitPrice; + } +} diff --git a/saas-app/app/Modules/Ledger/Domain/LedgerEntry.php b/saas-app/app/Modules/Ledger/Domain/LedgerEntry.php new file mode 100644 index 0000000..ada6ce8 --- /dev/null +++ b/saas-app/app/Modules/Ledger/Domain/LedgerEntry.php @@ -0,0 +1,30 @@ +amount; + } + + public function entryType(): string + { + return $this->entryType; + } +} diff --git a/saas-app/app/Modules/Members/Application/MemberService.php b/saas-app/app/Modules/Members/Application/MemberService.php new file mode 100644 index 0000000..74204cd --- /dev/null +++ b/saas-app/app/Modules/Members/Application/MemberService.php @@ -0,0 +1,26 @@ + + */ + public function listForTenant(string $tenantId): array + { + return [ + new Member( + id: 'member-demo-1', + tenantId: $tenantId, + tenantUserId: 'tenant-user-demo-1', + displayName: 'Max Beispiel', + email: 'max@example.com' + ), + ]; + } +} diff --git a/saas-app/app/Modules/Members/Domain/Member.php b/saas-app/app/Modules/Members/Domain/Member.php new file mode 100644 index 0000000..12546c0 --- /dev/null +++ b/saas-app/app/Modules/Members/Domain/Member.php @@ -0,0 +1,48 @@ +id; + } + + public function tenantId(): string + { + return $this->tenantId; + } + + public function tenantUserId(): string + { + return $this->tenantUserId; + } + + public function displayName(): string + { + return $this->displayName; + } + + public function email(): string + { + return $this->email; + } + + public function isActive(): bool + { + return $this->active; + } +} diff --git a/saas-app/app/Modules/Notifications/Services/NotificationService.php b/saas-app/app/Modules/Notifications/Services/NotificationService.php new file mode 100644 index 0000000..530e84b --- /dev/null +++ b/saas-app/app/Modules/Notifications/Services/NotificationService.php @@ -0,0 +1,24 @@ +> + */ + public function plannedNotifications(string $tenantId): array + { + return [ + [ + 'tenant_id' => $tenantId, + 'channel' => 'email', + 'template' => 'payment-reminder', + 'delivery_mode' => 'cron', + 'status' => 'planned', + ], + ]; + } +} diff --git a/saas-app/app/Modules/Notifications/Support/CronSchedule.php b/saas-app/app/Modules/Notifications/Support/CronSchedule.php new file mode 100644 index 0000000..606396c --- /dev/null +++ b/saas-app/app/Modules/Notifications/Support/CronSchedule.php @@ -0,0 +1,32 @@ +> + */ + public function definitions(): array + { + return [ + [ + 'job' => 'imports:run', + 'expression' => '*/10 * * * *', + 'purpose' => 'Verarbeitet geplante CSV-Importe in kurzen Intervallen.', + ], + [ + 'job' => 'exports:run', + 'expression' => '*/15 * * * *', + 'purpose' => 'Erstellt Exporte ohne dauerhafte Worker.', + ], + [ + 'job' => 'notifications:dispatch', + 'expression' => '*/5 * * * *', + 'purpose' => 'Versendet geplante Benachrichtigungen ueber Cron.', + ], + ]; + } +} diff --git a/saas-app/app/Modules/Payments/Application/PaymentService.php b/saas-app/app/Modules/Payments/Application/PaymentService.php new file mode 100644 index 0000000..304e176 --- /dev/null +++ b/saas-app/app/Modules/Payments/Application/PaymentService.php @@ -0,0 +1,27 @@ + + */ + public function recentPayments(string $tenantId, string $memberId): array + { + return [ + new Payment( + id: 'payment-demo-1', + tenantId: $tenantId, + memberId: $memberId, + amount: 10.00, + method: 'manual', + bookedAt: '2026-03-20 09:00:00' + ), + ]; + } +} diff --git a/saas-app/app/Modules/Payments/Domain/Payment.php b/saas-app/app/Modules/Payments/Domain/Payment.php new file mode 100644 index 0000000..ed9fb43 --- /dev/null +++ b/saas-app/app/Modules/Payments/Domain/Payment.php @@ -0,0 +1,23 @@ +amount; + } +} diff --git a/saas-app/app/Modules/Surveys/Application/SurveyService.php b/saas-app/app/Modules/Surveys/Application/SurveyService.php new file mode 100644 index 0000000..52cd25b --- /dev/null +++ b/saas-app/app/Modules/Surveys/Application/SurveyService.php @@ -0,0 +1,35 @@ + + */ + public function activeSurveys(string $tenantId): array + { + return [ + new Survey( + id: 'survey-demo-1', + tenantId: $tenantId, + title: 'Zufriedenheit mit dem Kaffeeangebot', + status: 'active', + questions: [ + new SurveyQuestion( + id: 'survey-question-demo-1', + surveyId: 'survey-demo-1', + question: 'Wie zufrieden bist du mit dem aktuellen Angebot?', + questionType: 'scale', + required: true, + ), + ], + ), + ]; + } +} diff --git a/saas-app/app/Modules/Surveys/Domain/Survey.php b/saas-app/app/Modules/Surveys/Domain/Survey.php new file mode 100644 index 0000000..b154832 --- /dev/null +++ b/saas-app/app/Modules/Surveys/Domain/Survey.php @@ -0,0 +1,48 @@ + $questions + */ + public function __construct( + private readonly string $id, + private readonly string $tenantId, + private readonly string $title, + private readonly string $status, + private readonly array $questions, + ) { + } + + public function id(): string + { + return $this->id; + } + + public function tenantId(): string + { + return $this->tenantId; + } + + public function title(): string + { + return $this->title; + } + + public function status(): string + { + return $this->status; + } + + /** + * @return array + */ + public function questions(): array + { + return $this->questions; + } +} diff --git a/saas-app/app/Modules/Surveys/Domain/SurveyQuestion.php b/saas-app/app/Modules/Surveys/Domain/SurveyQuestion.php new file mode 100644 index 0000000..6ec3e76 --- /dev/null +++ b/saas-app/app/Modules/Surveys/Domain/SurveyQuestion.php @@ -0,0 +1,42 @@ +id; + } + + public function surveyId(): string + { + return $this->surveyId; + } + + public function question(): string + { + return $this->question; + } + + public function questionType(): string + { + return $this->questionType; + } + + public function required(): bool + { + return $this->required; + } +} diff --git a/saas-app/app/Modules/Tenants/Models/Tenant.php b/saas-app/app/Modules/Tenants/Models/Tenant.php new file mode 100644 index 0000000..100610b --- /dev/null +++ b/saas-app/app/Modules/Tenants/Models/Tenant.php @@ -0,0 +1,21 @@ +status === 'active'; + } +} diff --git a/saas-app/app/Modules/Tenants/Models/TenantUser.php b/saas-app/app/Modules/Tenants/Models/TenantUser.php new file mode 100644 index 0000000..328bdde --- /dev/null +++ b/saas-app/app/Modules/Tenants/Models/TenantUser.php @@ -0,0 +1,24 @@ + $roles + */ + public function __construct( + public readonly string $tenantId, + public readonly string $userId, + public readonly string $status, + public readonly array $roles = [], + ) { + } + + public function canAccessTenant(): bool + { + return $this->status === 'active'; + } +} diff --git a/saas-app/app/Modules/Tenants/Services/TenantMembershipService.php b/saas-app/app/Modules/Tenants/Services/TenantMembershipService.php new file mode 100644 index 0000000..e3c529f --- /dev/null +++ b/saas-app/app/Modules/Tenants/Services/TenantMembershipService.php @@ -0,0 +1,20 @@ +tenantResolver->resolveFromHost($host); + + if ($resolved === null) { + return null; + } + + return new Tenant( + id: $resolved['tenant_id'] ?? 'tenant-placeholder', + tenantKey: $resolved['tenant_key'], + name: ucfirst((string) $resolved['tenant_key']), + status: 'active' + ); + } + + /** + * @return array + */ + public function listTenants(): array + { + return [ + new Tenant('tenant-placeholder', 'demo', 'Demo Tenant', 'active'), + ]; + } +} diff --git a/saas-app/app/Support/RequestContext.php b/saas-app/app/Support/RequestContext.php new file mode 100644 index 0000000..6f55fae --- /dev/null +++ b/saas-app/app/Support/RequestContext.php @@ -0,0 +1,43 @@ +tenantId = $tenantId; + $this->tenantKey = $tenantKey; + } + + public function tenantId(): ?string + { + return $this->tenantId; + } + + public function tenantKey(): ?string + { + return $this->tenantKey; + } + + public function setUser(?string $userId, array $roles = []): void + { + $this->userId = $userId; + $this->roles = $roles; + } + + public function userId(): ?string + { + return $this->userId; + } + + public function roles(): array + { + return $this->roles; + } +} diff --git a/saas-app/app/Support/TenantResolver.php b/saas-app/app/Support/TenantResolver.php new file mode 100644 index 0000000..9a87feb --- /dev/null +++ b/saas-app/app/Support/TenantResolver.php @@ -0,0 +1,26 @@ + null, + 'tenant_key' => $tenantKey, + ]; + } +} diff --git a/saas-app/bootstrap/app.php b/saas-app/bootstrap/app.php new file mode 100644 index 0000000..5042f41 --- /dev/null +++ b/saas-app/bootstrap/app.php @@ -0,0 +1,18 @@ + 'foundation-skeleton', + 'framework' => 'laravel-near', +]; diff --git a/saas-app/composer.json b/saas-app/composer.json new file mode 100644 index 0000000..73627ef --- /dev/null +++ b/saas-app/composer.json @@ -0,0 +1,19 @@ +{ + "name": "kaffeeliste/saas-app-foundation", + "description": "Downloadfreies Laravel-nahes Foundation-Geruest fuer die SaaS-Migration.", + "type": "project", + "license": "proprietary", + "require": { + "php": "^8.2" + }, + "autoload": { + "psr-4": { + "App\\": "app/" + } + }, + "scripts": { + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ] + } +} diff --git a/saas-app/config/app.php b/saas-app/config/app.php new file mode 100644 index 0000000..7c09cfa --- /dev/null +++ b/saas-app/config/app.php @@ -0,0 +1,9 @@ + env('APP_NAME', 'Kaffeeliste SaaS'), + 'env' => env('APP_ENV', 'production'), + 'url' => env('APP_URL', 'https://app.example.com'), + 'timezone' => 'UTC', + 'locale' => 'de', +]; diff --git a/saas-app/config/multitenancy.php b/saas-app/config/multitenancy.php new file mode 100644 index 0000000..5aaacd7 --- /dev/null +++ b/saas-app/config/multitenancy.php @@ -0,0 +1,11 @@ + env('TENANCY_MODE', 'subdomain'), + 'central_domains' => [ + 'app.example.com', + 'www.app.example.com', + ], + 'tenant_parameter' => 'tenant', + 'default_route' => 'dashboard', +]; diff --git a/saas-app/database/migrations/2026_03_20_000001_create_tenants_table.php b/saas-app/database/migrations/2026_03_20_000001_create_tenants_table.php new file mode 100644 index 0000000..5897f57 --- /dev/null +++ b/saas-app/database/migrations/2026_03_20_000001_create_tenants_table.php @@ -0,0 +1,13 @@ + +

Passwort zuruecksetzen

+

Platzhalter fuer den webspace-tauglichen Passwort-Reset-Prozess per E-Mail.

+ +
+ + + +
+ +@endsection diff --git a/saas-app/resources/views/auth/login.blade.php b/saas-app/resources/views/auth/login.blade.php new file mode 100644 index 0000000..4202ac4 --- /dev/null +++ b/saas-app/resources/views/auth/login.blade.php @@ -0,0 +1,28 @@ +@extends('layouts.app') + +@section('content') +
+

Login

+

Mandantenbezogener Einstieg fuer lokalen Login oder spaetere SSO-Anmeldung.

+ +
+ + + + + + + +
+ +

Passwort vergessen?

+ +
+

Single Sign-on

+

Hier werden spaeter tenantbezogene OIDC-/ADFS-Provider angeboten.

+ +
+
+@endsection diff --git a/saas-app/resources/views/content/index.blade.php b/saas-app/resources/views/content/index.blade.php new file mode 100644 index 0000000..a998f62 --- /dev/null +++ b/saas-app/resources/views/content/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Hinweise und FAQ

+

Platzhalter fuer tenantbezogene Hinweise, FAQ-Eintraege und spaetere Redaktionsfunktionen.

+
+@endsection diff --git a/saas-app/resources/views/dashboard/index.blade.php b/saas-app/resources/views/dashboard/index.blade.php new file mode 100644 index 0000000..3fdbed2 --- /dev/null +++ b/saas-app/resources/views/dashboard/index.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.app') + +@section('content') +
+

Dashboard

+

Platzhalter fuer Kontostand, Monatsverbrauch und letzte Buchungen.

+
    +
  • Kontostand: 7,50 EUR
  • +
  • Striche diesen Monat: 5
  • +
  • Einzahlungen diesen Monat: 1
  • +
+
+@endsection diff --git a/saas-app/resources/views/exports/index.blade.php b/saas-app/resources/views/exports/index.blade.php new file mode 100644 index 0000000..8150ff9 --- /dev/null +++ b/saas-app/resources/views/exports/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Exporte

+

Platzhalter fuer Reports, Export-Jobs und spaetere Download-Ansichten.

+
+@endsection diff --git a/saas-app/resources/views/imports/index.blade.php b/saas-app/resources/views/imports/index.blade.php new file mode 100644 index 0000000..d16e79b --- /dev/null +++ b/saas-app/resources/views/imports/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Importe

+

Platzhalter fuer CSV-Importe, Cron-gesteuerte Verarbeitungen und Statusanzeigen.

+
+@endsection diff --git a/saas-app/resources/views/layouts/app.blade.php b/saas-app/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..3cf0560 --- /dev/null +++ b/saas-app/resources/views/layouts/app.blade.php @@ -0,0 +1,17 @@ + + + + + + {{ $title ?? 'Kaffeeliste SaaS' }} + + +
+

Kaffeeliste SaaS

+

Mandantenfaehige Webspace-Zielanwendung

+
+
+ @yield('content') +
+ + diff --git a/saas-app/resources/views/ledger/index.blade.php b/saas-app/resources/views/ledger/index.blade.php new file mode 100644 index 0000000..091fad5 --- /dev/null +++ b/saas-app/resources/views/ledger/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Ledger

+

Platzhalter fuer Buchungen, Kontostand und Korrekturen.

+
+@endsection diff --git a/saas-app/resources/views/members/index.blade.php b/saas-app/resources/views/members/index.blade.php new file mode 100644 index 0000000..c6517fa --- /dev/null +++ b/saas-app/resources/views/members/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Mitglieder

+

Platzhalter fuer tenantbezogene Mitgliederverwaltung und Statusanzeige.

+
+@endsection diff --git a/saas-app/resources/views/notifications/index.blade.php b/saas-app/resources/views/notifications/index.blade.php new file mode 100644 index 0000000..a70a5a4 --- /dev/null +++ b/saas-app/resources/views/notifications/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Benachrichtigungen

+

Platzhalter fuer Mailversand, Versandstatus und Cron-basierte Zustellung.

+
+@endsection diff --git a/saas-app/resources/views/payments/index.blade.php b/saas-app/resources/views/payments/index.blade.php new file mode 100644 index 0000000..c5c2645 --- /dev/null +++ b/saas-app/resources/views/payments/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Einzahlungen

+

Platzhalter fuer Zahlungseintraege und spaetere Zahlungsreferenzen.

+
+@endsection diff --git a/saas-app/resources/views/surveys/index.blade.php b/saas-app/resources/views/surveys/index.blade.php new file mode 100644 index 0000000..6479574 --- /dev/null +++ b/saas-app/resources/views/surveys/index.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Umfragen

+

Platzhalter fuer Umfragen, Fragen, Antworten und spaetere Auswertungen.

+
+@endsection diff --git a/saas-app/resources/views/tenants/index.blade.php b/saas-app/resources/views/tenants/index.blade.php new file mode 100644 index 0000000..1fad9a4 --- /dev/null +++ b/saas-app/resources/views/tenants/index.blade.php @@ -0,0 +1,25 @@ +@extends('layouts.app') + +@section('content') +
+

Tenant-Verwaltung

+

Platzhalter fuer die spaetere Verwaltung von Mandanten, Domains, Rollen und SSO-Providern.

+ + + + + + + + + + + + + + + + +
MandantTenant KeyStatus
Demo Tenantdemoactive
+
+@endsection diff --git a/saas-app/resources/views/welcome.blade.php b/saas-app/resources/views/welcome.blade.php new file mode 100644 index 0000000..cd65635 --- /dev/null +++ b/saas-app/resources/views/welcome.blade.php @@ -0,0 +1,8 @@ +@extends('layouts.app') + +@section('content') +
+

Neue SaaS-Plattform

+

Dieses Grundgeruest dient als Startpunkt fuer die mandantenfaehige Neuimplementierung.

+
+@endsection diff --git a/saas-app/routes/auth.php b/saas-app/routes/auth.php new file mode 100644 index 0000000..57dc4b2 --- /dev/null +++ b/saas-app/routes/auth.php @@ -0,0 +1,55 @@ + 'GET', + 'uri' => '/login', + 'action' => 'App\\Modules\\Identity\\Controllers\\LoginController@show', + 'name' => 'login', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'POST', + 'uri' => '/login', + 'action' => 'App\\Modules\\Identity\\Controllers\\LoginController@authenticate', + 'name' => 'login.attempt', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/forgot-password', + 'action' => 'App\\Modules\\Identity\\Controllers\\ForgotPasswordController@show', + 'name' => 'password.request', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'POST', + 'uri' => '/forgot-password', + 'action' => 'App\\Modules\\Identity\\Controllers\\ForgotPasswordController@sendResetLink', + 'name' => 'password.email', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/auth/oidc/providers', + 'action' => 'App\\Modules\\Identity\\Controllers\\OidcController@providers', + 'name' => 'auth.oidc.providers', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/auth/oidc/{provider}', + 'action' => 'App\\Modules\\Identity\\Controllers\\OidcController@start', + 'name' => 'auth.oidc.start', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/auth/oidc/{provider}/callback', + 'action' => 'App\\Modules\\Identity\\Controllers\\OidcController@callback', + 'name' => 'auth.oidc.callback', + 'middleware' => [ResolveTenant::class], + ], +]; diff --git a/saas-app/routes/core.php b/saas-app/routes/core.php new file mode 100644 index 0000000..2896e1d --- /dev/null +++ b/saas-app/routes/core.php @@ -0,0 +1,34 @@ + 'GET', + 'uri' => '/dashboard', + 'action' => 'DashboardController@index', + 'name' => 'dashboard', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/members', + 'action' => 'MembersController@index', + 'name' => 'members.index', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/ledger', + 'action' => 'LedgerController@index', + 'name' => 'ledger.index', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/payments', + 'action' => 'PaymentsController@index', + 'name' => 'payments.index', + 'middleware' => [ResolveTenant::class], + ], +]; diff --git a/saas-app/routes/operations.php b/saas-app/routes/operations.php new file mode 100644 index 0000000..fb0838a --- /dev/null +++ b/saas-app/routes/operations.php @@ -0,0 +1,41 @@ + 'GET', + 'uri' => '/content', + 'action' => 'ContentController@index', + 'name' => 'content.index', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/surveys', + 'action' => 'SurveysController@index', + 'name' => 'surveys.index', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/imports', + 'action' => 'ImportsController@index', + 'name' => 'imports.index', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/exports', + 'action' => 'ExportsController@index', + 'name' => 'exports.index', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/notifications', + 'action' => 'NotificationsController@index', + 'name' => 'notifications.index', + 'middleware' => [ResolveTenant::class], + ], +]; diff --git a/saas-app/routes/tenants.php b/saas-app/routes/tenants.php new file mode 100644 index 0000000..d4168ec --- /dev/null +++ b/saas-app/routes/tenants.php @@ -0,0 +1,13 @@ + 'GET', + 'uri' => '/tenants', + 'action' => 'App\\Modules\\Tenants\\Services\\TenantService@listTenants', + 'name' => 'tenants.index', + 'middleware' => [ResolveTenant::class], + ], +]; diff --git a/saas-app/routes/web.php b/saas-app/routes/web.php new file mode 100644 index 0000000..a71cf47 --- /dev/null +++ b/saas-app/routes/web.php @@ -0,0 +1,27 @@ + 'GET', + 'uri' => '/', + 'action' => 'LandingController@index', + 'name' => 'landing', + 'middleware' => [], + ], + [ + 'method' => 'GET', + 'uri' => '/login', + 'action' => 'Auth\\LoginController@show', + 'name' => 'login', + 'middleware' => [ResolveTenant::class], + ], + [ + 'method' => 'GET', + 'uri' => '/dashboard', + 'action' => 'DashboardController@index', + 'name' => 'dashboard', + 'middleware' => [ResolveTenant::class], + ], +];