Initial
This commit is contained in:
@@ -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.
|
||||
@@ -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=
|
||||
@@ -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.
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Support\RequestContext;
|
||||
use App\Support\TenantResolver;
|
||||
|
||||
class ResolveTenant
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TenantResolver $tenantResolver,
|
||||
private readonly RequestContext $requestContext
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(object $request, callable $next): mixed
|
||||
{
|
||||
$host = method_exists($request, 'getHost')
|
||||
? (string) $request->getHost()
|
||||
: '';
|
||||
|
||||
$tenant = $this->tenantResolver->resolveFromHost($host);
|
||||
|
||||
if ($tenant !== null) {
|
||||
$this->requestContext->setTenant(
|
||||
$tenant['tenant_id'],
|
||||
$tenant['tenant_key']
|
||||
);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Content\Application;
|
||||
|
||||
use App\Modules\Content\Domain\Announcement;
|
||||
use App\Modules\Content\Domain\FaqItem;
|
||||
|
||||
class ContentService
|
||||
{
|
||||
/**
|
||||
* @return array<int, Announcement>
|
||||
*/
|
||||
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<int, FaqItem>
|
||||
*/
|
||||
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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Content\Domain;
|
||||
|
||||
class Announcement
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $title,
|
||||
private readonly string $message,
|
||||
private readonly string $visibleUntil,
|
||||
private readonly bool $active,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Content\Domain;
|
||||
|
||||
class FaqItem
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $question,
|
||||
private readonly string $answer,
|
||||
private readonly int $sortOrder,
|
||||
private readonly bool $active,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Exports\Application;
|
||||
|
||||
use App\Modules\Exports\Jobs\ExportJob;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
/**
|
||||
* @return array<int, ExportJob>
|
||||
*/
|
||||
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',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Exports\Jobs;
|
||||
|
||||
class ExportJob
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $exportType,
|
||||
private readonly string $status,
|
||||
private readonly string $targetPath,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Identity\Controllers;
|
||||
|
||||
class ForgotPasswordController
|
||||
{
|
||||
public function show(): array
|
||||
{
|
||||
return [
|
||||
'view' => 'auth.forgot-password',
|
||||
'data' => [
|
||||
'title' => 'Passwort zuruecksetzen',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function sendResetLink(array $payload): array
|
||||
{
|
||||
return [
|
||||
'status' => 'queued-placeholder',
|
||||
'email' => $payload['email'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Identity\Controllers;
|
||||
|
||||
use App\Modules\Identity\Services\AuthService;
|
||||
|
||||
class LoginController
|
||||
{
|
||||
public function __construct(private readonly AuthService $authService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(): array
|
||||
{
|
||||
return [
|
||||
'view' => 'auth.login',
|
||||
'data' => [
|
||||
'title' => 'Anmeldung',
|
||||
'providers' => $this->authService->availableProviders(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function authenticate(array $payload): array
|
||||
{
|
||||
return $this->authService->attemptLogin($payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Identity\Controllers;
|
||||
|
||||
use App\Modules\Identity\Services\OidcProviderService;
|
||||
|
||||
class OidcController
|
||||
{
|
||||
public function __construct(private readonly OidcProviderService $providerService)
|
||||
{
|
||||
}
|
||||
|
||||
public function providers(): array
|
||||
{
|
||||
return [
|
||||
'providers' => $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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Identity\Services;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
public function availableProviders(): array
|
||||
{
|
||||
return [
|
||||
'password' => [
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Identity\Services;
|
||||
|
||||
use App\Modules\Identity\Support\OidcProviderConfig;
|
||||
|
||||
class OidcProviderService
|
||||
{
|
||||
/**
|
||||
* @return array<int, OidcProviderConfig>
|
||||
*/
|
||||
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']
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Identity\Support;
|
||||
|
||||
class OidcProviderConfig
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Imports\Application;
|
||||
|
||||
use App\Modules\Imports\Jobs\ImportJob;
|
||||
|
||||
class ImportService
|
||||
{
|
||||
/**
|
||||
* @return array<int, ImportJob>
|
||||
*/
|
||||
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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Imports\Jobs;
|
||||
|
||||
class ImportJob
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $type,
|
||||
private readonly string $status,
|
||||
private readonly string $scheduledAt,
|
||||
private readonly bool $runViaCron,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Ledger\Application;
|
||||
|
||||
class DashboardService
|
||||
{
|
||||
public function summary(string $tenantId, string $memberId): array
|
||||
{
|
||||
$ledgerService = new LedgerService();
|
||||
$balance = $ledgerService->balanceForMember($tenantId, $memberId);
|
||||
|
||||
return [
|
||||
'balance' => $balance,
|
||||
'coffee_strokes_this_month' => 5,
|
||||
'payments_this_month' => 1,
|
||||
'latest_booking_at' => '2026-03-20 10:30:00',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Ledger\Application;
|
||||
|
||||
use App\Modules\Ledger\Domain\LedgerEntry;
|
||||
|
||||
class LedgerService
|
||||
{
|
||||
/**
|
||||
* @return array<int, LedgerEntry>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Ledger\Domain;
|
||||
|
||||
class CoffeeEntry
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $memberId,
|
||||
private readonly int $strokes,
|
||||
private readonly float $unitPrice,
|
||||
private readonly string $bookedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public function totalCost(): float
|
||||
{
|
||||
return $this->strokes * $this->unitPrice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Ledger\Domain;
|
||||
|
||||
class LedgerEntry
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $memberId,
|
||||
private readonly string $entryType,
|
||||
private readonly float $amount,
|
||||
private readonly string $bookedAt,
|
||||
private readonly string $referenceType,
|
||||
private readonly ?string $referenceId = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function amount(): float
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
public function entryType(): string
|
||||
{
|
||||
return $this->entryType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Members\Application;
|
||||
|
||||
use App\Modules\Members\Domain\Member;
|
||||
|
||||
class MemberService
|
||||
{
|
||||
/**
|
||||
* @return array<int, Member>
|
||||
*/
|
||||
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'
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Members\Domain;
|
||||
|
||||
class Member
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $tenantUserId,
|
||||
private readonly string $displayName,
|
||||
private readonly string $email,
|
||||
private readonly bool $active = true
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Notifications\Services;
|
||||
|
||||
class NotificationService
|
||||
{
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
public function plannedNotifications(string $tenantId): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'channel' => 'email',
|
||||
'template' => 'payment-reminder',
|
||||
'delivery_mode' => 'cron',
|
||||
'status' => 'planned',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Notifications\Support;
|
||||
|
||||
class CronSchedule
|
||||
{
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
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.',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Payments\Application;
|
||||
|
||||
use App\Modules\Payments\Domain\Payment;
|
||||
|
||||
class PaymentService
|
||||
{
|
||||
/**
|
||||
* @return array<int, Payment>
|
||||
*/
|
||||
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'
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Payments\Domain;
|
||||
|
||||
class Payment
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $tenantId,
|
||||
private readonly string $memberId,
|
||||
private readonly float $amount,
|
||||
private readonly string $method,
|
||||
private readonly string $bookedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public function amount(): float
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Surveys\Application;
|
||||
|
||||
use App\Modules\Surveys\Domain\Survey;
|
||||
use App\Modules\Surveys\Domain\SurveyQuestion;
|
||||
|
||||
class SurveyService
|
||||
{
|
||||
/**
|
||||
* @return array<int, Survey>
|
||||
*/
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Surveys\Domain;
|
||||
|
||||
class Survey
|
||||
{
|
||||
/**
|
||||
* @param array<int, SurveyQuestion> $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<int, SurveyQuestion>
|
||||
*/
|
||||
public function questions(): array
|
||||
{
|
||||
return $this->questions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Surveys\Domain;
|
||||
|
||||
class SurveyQuestion
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $id,
|
||||
private readonly string $surveyId,
|
||||
private readonly string $question,
|
||||
private readonly string $questionType,
|
||||
private readonly bool $required,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants\Models;
|
||||
|
||||
class Tenant
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $tenantKey,
|
||||
public readonly string $name,
|
||||
public readonly string $status,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 'active';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants\Models;
|
||||
|
||||
class TenantUser
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants\Services;
|
||||
|
||||
use App\Modules\Tenants\Models\TenantUser;
|
||||
|
||||
class TenantMembershipService
|
||||
{
|
||||
public function forUser(string $tenantId, string $userId): TenantUser
|
||||
{
|
||||
return new TenantUser(
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
status: 'active',
|
||||
roles: ['tenant_admin']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants\Services;
|
||||
|
||||
use App\Modules\Tenants\Models\Tenant;
|
||||
use App\Support\TenantResolver;
|
||||
|
||||
class TenantService
|
||||
{
|
||||
public function __construct(private readonly TenantResolver $tenantResolver)
|
||||
{
|
||||
}
|
||||
|
||||
public function resolveFromHost(string $host): ?Tenant
|
||||
{
|
||||
$resolved = $this->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<int, Tenant>
|
||||
*/
|
||||
public function listTenants(): array
|
||||
{
|
||||
return [
|
||||
new Tenant('tenant-placeholder', 'demo', 'Demo Tenant', 'active'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class RequestContext
|
||||
{
|
||||
private ?string $tenantId = null;
|
||||
private ?string $tenantKey = null;
|
||||
private ?string $userId = null;
|
||||
private array $roles = [];
|
||||
|
||||
public function setTenant(?string $tenantId, ?string $tenantKey = null): void
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class TenantResolver
|
||||
{
|
||||
public function resolveFromHost(string $host): ?array
|
||||
{
|
||||
$parts = explode('.', $host);
|
||||
|
||||
if (count($parts) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tenantKey = $parts[0];
|
||||
|
||||
if ($tenantKey === 'www' || $tenantKey === 'app') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'tenant_id' => null,
|
||||
'tenant_key' => $tenantKey,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Platzhalter fuer den spaeteren Laravel-Bootstrap.
|
||||
*
|
||||
* Dieses File dokumentiert, welche Foundation-Bausteine bereits vorgesehen sind:
|
||||
* - zentrale Konfigurationsdateien
|
||||
* - Tenant-Resolution pro Request
|
||||
* - Webspace-taugliche Jobs ueber Cron / Queue-Datenbank
|
||||
* - modulare Fachstruktur unter app/Modules
|
||||
*/
|
||||
|
||||
return [
|
||||
'status' => 'foundation-skeleton',
|
||||
'framework' => 'laravel-near',
|
||||
];
|
||||
@@ -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');\""
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => env('APP_NAME', 'Kaffeeliste SaaS'),
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
'url' => env('APP_URL', 'https://app.example.com'),
|
||||
'timezone' => 'UTC',
|
||||
'locale' => 'de',
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'mode' => env('TENANCY_MODE', 'subdomain'),
|
||||
'central_domains' => [
|
||||
'app.example.com',
|
||||
'www.app.example.com',
|
||||
],
|
||||
'tenant_parameter' => 'tenant',
|
||||
'default_route' => 'dashboard',
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE tenants (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_key VARCHAR(120) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (tenant_key)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE users (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
is_platform_admin BIT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (email)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE tenant_users (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (tenant_id, user_id),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE roles (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
role_key VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
scope VARCHAR(50) NOT NULL DEFAULT 'tenant',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (role_key, scope)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE tenant_user_roles (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_user_id CHAR(36) NOT NULL,
|
||||
role_id CHAR(36) NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
UNIQUE (tenant_user_id, role_id),
|
||||
FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE members (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
tenant_user_id CHAR(36) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (tenant_id, email),
|
||||
UNIQUE (tenant_id, tenant_user_id),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE password_reset_tokens (
|
||||
email VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (email)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE coffee_entries (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
member_id CHAR(36) NOT NULL,
|
||||
strokes INT NOT NULL,
|
||||
unit_price DECIMAL(10,2) NOT NULL,
|
||||
total_cost DECIMAL(10,2) NOT NULL,
|
||||
booking_source VARCHAR(50) NOT NULL DEFAULT 'manual',
|
||||
booked_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY (member_id) REFERENCES members(id)
|
||||
);
|
||||
SQL;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE tenant_identity_providers (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
provider_key VARCHAR(120) NOT NULL,
|
||||
driver VARCHAR(50) NOT NULL DEFAULT 'oidc',
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
client_secret_encrypted VARCHAR(1024) NULL,
|
||||
discovery_url VARCHAR(1024) NULL,
|
||||
authorization_url VARCHAR(1024) NULL,
|
||||
token_url VARCHAR(1024) NULL,
|
||||
userinfo_url VARCHAR(1024) NULL,
|
||||
redirect_uri VARCHAR(1024) NOT NULL,
|
||||
scopes TEXT NULL,
|
||||
is_enabled BIT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (tenant_id, provider_key),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE payment_entries (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
member_id CHAR(36) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
payment_method VARCHAR(50) NOT NULL DEFAULT 'manual',
|
||||
booked_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY (member_id) REFERENCES members(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE user_identities (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
tenant_id CHAR(36) NULL,
|
||||
provider_key VARCHAR(120) NOT NULL,
|
||||
external_subject VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (provider_key, external_subject),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE ledger_entries (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
member_id CHAR(36) NOT NULL,
|
||||
entry_type VARCHAR(50) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
reference_type VARCHAR(100) NOT NULL,
|
||||
reference_id CHAR(36) NULL,
|
||||
booked_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY (member_id) REFERENCES members(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE announcements (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
visible_until DATETIME NULL,
|
||||
is_active BIT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE faq_items (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
question VARCHAR(255) NOT NULL,
|
||||
answer TEXT NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_active BIT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE surveys (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'draft',
|
||||
starts_at DATETIME NULL,
|
||||
ends_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE survey_questions (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
survey_id CHAR(36) NOT NULL,
|
||||
question TEXT NOT NULL,
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
is_required BIT NOT NULL DEFAULT 0,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (survey_id) REFERENCES surveys(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE survey_answers (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
survey_id CHAR(36) NOT NULL,
|
||||
question_id CHAR(36) NOT NULL,
|
||||
tenant_user_id CHAR(36) NOT NULL,
|
||||
answer_text TEXT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (survey_id) REFERENCES surveys(id),
|
||||
FOREIGN KEY (question_id) REFERENCES survey_questions(id),
|
||||
FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE import_jobs (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
import_type VARCHAR(100) NOT NULL,
|
||||
source_path VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||||
scheduled_at DATETIME NULL,
|
||||
processed_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE export_jobs (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
export_type VARCHAR(100) NOT NULL,
|
||||
target_path VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
CREATE TABLE notification_logs (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
channel VARCHAR(50) NOT NULL,
|
||||
template_key VARCHAR(100) NOT NULL,
|
||||
recipient VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'planned',
|
||||
sent_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
SQL;
|
||||
@@ -0,0 +1,14 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Passwort zuruecksetzen</h2>
|
||||
<p>Platzhalter fuer den webspace-tauglichen Passwort-Reset-Prozess per E-Mail.</p>
|
||||
|
||||
<form method="post" action="/forgot-password">
|
||||
<label for="email">E-Mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email">
|
||||
<button type="submit">Reset-Link anfordern</button>
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,28 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Login</h2>
|
||||
<p>Mandantenbezogener Einstieg fuer lokalen Login oder spaetere SSO-Anmeldung.</p>
|
||||
|
||||
<form method="post" action="/login">
|
||||
<label for="email">E-Mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="username">
|
||||
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password">
|
||||
|
||||
<button type="submit">Mit Passwort anmelden</button>
|
||||
</form>
|
||||
|
||||
<p><a href="/forgot-password">Passwort vergessen?</a></p>
|
||||
|
||||
<section>
|
||||
<h3>Single Sign-on</h3>
|
||||
<p>Hier werden spaeter tenantbezogene OIDC-/ADFS-Provider angeboten.</p>
|
||||
<ul>
|
||||
<li><a href="/auth/oidc/entra-default">Mit SSO anmelden</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Hinweise und FAQ</h2>
|
||||
<p>Platzhalter fuer tenantbezogene Hinweise, FAQ-Eintraege und spaetere Redaktionsfunktionen.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,13 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Dashboard</h2>
|
||||
<p>Platzhalter fuer Kontostand, Monatsverbrauch und letzte Buchungen.</p>
|
||||
<ul>
|
||||
<li>Kontostand: 7,50 EUR</li>
|
||||
<li>Striche diesen Monat: 5</li>
|
||||
<li>Einzahlungen diesen Monat: 1</li>
|
||||
</ul>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Exporte</h2>
|
||||
<p>Platzhalter fuer Reports, Export-Jobs und spaetere Download-Ansichten.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Importe</h2>
|
||||
<p>Platzhalter fuer CSV-Importe, Cron-gesteuerte Verarbeitungen und Statusanzeigen.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ $title ?? 'Kaffeeliste SaaS' }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Kaffeeliste SaaS</h1>
|
||||
<p>Mandantenfaehige Webspace-Zielanwendung</p>
|
||||
</header>
|
||||
<main>
|
||||
@yield('content')
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Ledger</h2>
|
||||
<p>Platzhalter fuer Buchungen, Kontostand und Korrekturen.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Mitglieder</h2>
|
||||
<p>Platzhalter fuer tenantbezogene Mitgliederverwaltung und Statusanzeige.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Benachrichtigungen</h2>
|
||||
<p>Platzhalter fuer Mailversand, Versandstatus und Cron-basierte Zustellung.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Einzahlungen</h2>
|
||||
<p>Platzhalter fuer Zahlungseintraege und spaetere Zahlungsreferenzen.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Umfragen</h2>
|
||||
<p>Platzhalter fuer Umfragen, Fragen, Antworten und spaetere Auswertungen.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,25 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Tenant-Verwaltung</h2>
|
||||
<p>Platzhalter fuer die spaetere Verwaltung von Mandanten, Domains, Rollen und SSO-Providern.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mandant</th>
|
||||
<th>Tenant Key</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Demo Tenant</td>
|
||||
<td>demo</td>
|
||||
<td>active</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section>
|
||||
<h2>Neue SaaS-Plattform</h2>
|
||||
<p>Dieses Grundgeruest dient als Startpunkt fuer die mandantenfaehige Neuimplementierung.</p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => '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],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => '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],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => '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],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => 'GET',
|
||||
'uri' => '/tenants',
|
||||
'action' => 'App\\Modules\\Tenants\\Services\\TenantService@listTenants',
|
||||
'name' => 'tenants.index',
|
||||
'middleware' => [ResolveTenant::class],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => '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],
|
||||
],
|
||||
];
|
||||
Reference in New Issue
Block a user