chore: sync
This commit is contained in:
12
.env.example
12
.env.example
@@ -1,6 +1,18 @@
|
|||||||
APP_ENV=change-me
|
APP_ENV=change-me
|
||||||
|
APP_URL=http://localhost:8000
|
||||||
DEV_MODE=change-me
|
DEV_MODE=change-me
|
||||||
DEV_USER_ID=change-me
|
DEV_USER_ID=change-me
|
||||||
|
DEV_ENTWICKLUNG=0
|
||||||
|
EMAIL_LOG_TRANSPORT=0
|
||||||
|
|
||||||
|
EMAIL_SMTP_HOST=change-me
|
||||||
|
EMAIL_SMTP_PORT=587
|
||||||
|
EMAIL_SMTP_USERNAME=change-me
|
||||||
|
EMAIL_SMTP_PASSWORD=change-me
|
||||||
|
EMAIL_FROM_ADDRESS=no-reply@example.test
|
||||||
|
EMAIL_SMTP_SECURE=tls
|
||||||
|
EMAIL_SMTP_TIMEOUT=30
|
||||||
|
EMAIL_LOG_PATH=storage/logs/email.log
|
||||||
|
|
||||||
DB_HOST=change-me
|
DB_HOST=change-me
|
||||||
DB_PORT=change-me
|
DB_PORT=change-me
|
||||||
|
|||||||
@@ -7,11 +7,16 @@
|
|||||||
4. **Keine Secrets:** Keine `.env`, keine echten Passwörter, keine API-Keys ins Repo.
|
4. **Keine Secrets:** Keine `.env`, keine echten Passwörter, keine API-Keys ins Repo.
|
||||||
5. **Default-Credentials:** Nur Platzhalter wie `change-me`/`your-strong-password` verwenden und dokumentieren, dass vor Prod geändert werden muss.
|
5. **Default-Credentials:** Nur Platzhalter wie `change-me`/`your-strong-password` verwenden und dokumentieren, dass vor Prod geändert werden muss.
|
||||||
6. **Tests:** Tests müssen grün sein, bevor gemerged wird.
|
6. **Tests:** Tests müssen grün sein, bevor gemerged wird.
|
||||||
|
7. **Dokumentation:** Alle neuen Features, Funktionen und Module müssen unter `docs/wiki/` dokumentiert werden. Die Dokumentation muss aktuell, verständlich und vollständig sein.
|
||||||
|
8. **Commit-Disziplin:** Nach jedem neuen Feature oder Modul sofort committen; die Commit-Message benennt das Feature oder Modul ausdrücklich.
|
||||||
|
|
||||||
|
9. **Orchestrator:** Zerlegt Aufgaben immer in kleinstmögliche Subtasks; Code-Agent-Subtasks sind auf maximal 2000 Tokens (Kontext + Instruktionen) begrenzt.
|
||||||
|
|
||||||
## Ordner-Intent (Kurzfassung)
|
## Ordner-Intent (Kurzfassung)
|
||||||
- `web/desktop` und `web/mobile`: Alles, was aus dem Webserver ausgeliefert wird.
|
- `web/desktop` und `web/mobile`: Alles, was aus dem Webserver ausgeliefert wird.
|
||||||
- `docs/`: Dokumentation (SSOT liegt hier).
|
- `docs/`: Dokumentation (SSOT liegt hier).
|
||||||
- `planning/`: Skizzen/Notizen, nicht für Deployment.
|
- `planning/`: Skizzen/Notizen, nicht für Deployment.
|
||||||
|
-
|
||||||
|
|
||||||
## Sprache
|
## Sprache
|
||||||
- Alle Agenten-Ausgaben und Kommentare sind **Deutsch**, außer der User verlangt explizit eine andere Sprache.
|
- Alle Agenten-Ausgaben und Kommentare sind **Deutsch**, außer der User verlangt explizit eine andere Sprache.
|
||||||
|
|||||||
@@ -10,5 +10,32 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${DB_PASS:-your-strong-password}
|
POSTGRES_PASSWORD: ${DB_PASS:-your-strong-password}
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
web:
|
||||||
|
image: php:8.2-apache
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
environment:
|
||||||
|
APACHE_DOCUMENT_ROOT: /app/web/desktop/public
|
||||||
|
volumes:
|
||||||
|
- ./web/desktop/public:/app/web/desktop/public:ro
|
||||||
|
- ./web/mobile/public:/app/web/mobile/public:ro
|
||||||
|
- ./server:/app/server:ro
|
||||||
|
- ./config:/app/config:ro
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
cat <<'EOF' >/etc/apache2/conf-available/mobile-alias.conf
|
||||||
|
Alias /m /app/web/mobile/public
|
||||||
|
<Directory /app/web/mobile/public>
|
||||||
|
Options Indexes FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
EOF
|
||||||
|
ln -sf /etc/apache2/conf-available/mobile-alias.conf /etc/apache2/conf-enabled/mobile-alias.conf
|
||||||
|
apache2-foreground
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
28
docs/wiki/auth.md
Normal file
28
docs/wiki/auth.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Auth Module
|
||||||
|
|
||||||
|
### Module Description
|
||||||
|
Handles user authentication, session management, and permission checks across the system.
|
||||||
|
|
||||||
|
### API Endpoints & Registration Flow
|
||||||
|
|
||||||
|
1. **POST /auth/login**
|
||||||
|
- Purpose: Benutzerlogin über Session/Cookie
|
||||||
|
- Auth: Keine
|
||||||
|
- Payload: `{"username_or_email": "string", "password": "string"}`
|
||||||
|
- Response: `{"user": {...}}`
|
||||||
|
|
||||||
|
2. **POST /auth/register/step1 ... /step3**
|
||||||
|
- Mehrstufige Registrierung (Rasse → Avatar/Titel → Username/Email/Passwort)
|
||||||
|
- Je Step wird der Draft gespeichert; Step3 erstellt Benutzer mit `activation_token` und nimmt `is_active=false` an, sofern `DEV_ENTWICKLUNG` nicht gesetzt ist.
|
||||||
|
- Response: `201` mit `{ "status": "pending" }` (E-Mail-Link) oder `{ "status": "active", "user": {...} }` (Dev-Bypass).
|
||||||
|
|
||||||
|
3. **POST /auth/register/confirm**
|
||||||
|
- Purpose: Aktivierung per Token-Link (`token` im Body oder Query)
|
||||||
|
- Auth: Keine
|
||||||
|
- Response: `{"user": {...}}` nach erfolgreicher Aktivierung.
|
||||||
|
|
||||||
|
4. **GET /auth/permissions**
|
||||||
|
- Purpose: Berechtigungen abrufen (Session erforderlich)
|
||||||
|
- Auth: Session
|
||||||
|
- Payload: Keine
|
||||||
|
- Response: `{"permissions": ["string"]}`
|
||||||
18
docs/wiki/blueprints.md
Normal file
18
docs/wiki/blueprints.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## Blueprints Module
|
||||||
|
|
||||||
|
### Module Description
|
||||||
|
Defines and manages construction blueprints for planetary structures and facilities.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- **GET /blueprints**
|
||||||
|
- *Purpose:* Retrieve list of available blueprints
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* None
|
||||||
|
- *Response:* `{"blueprints": [{"id": "int", "name": "string", "resource_cost": {"ore": "int", "crystal": "int"}}]}`
|
||||||
|
|
||||||
|
- **GET /blueprints/{id}**
|
||||||
|
- *Purpose:* Retrieve detailed blueprint information
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* None
|
||||||
|
- *Response:* `{"id": "int", "name": "string", "description": "string", "resource_cost": {"ore": "int", "crystal": "int"}}`
|
||||||
24
docs/wiki/build_queue.md
Normal file
24
docs/wiki/build_queue.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## Build Queue Module
|
||||||
|
|
||||||
|
### Module Description
|
||||||
|
Processes and manages construction queues for planetary structures and facilities.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- **GET /build/queue**
|
||||||
|
- *Purpose:* Retrieve current build queue
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* None
|
||||||
|
- *Response:* `{"queue": [{"id": "int", "blueprint_id": "int", "status": "string"}]}`
|
||||||
|
|
||||||
|
- **POST /build/start**
|
||||||
|
- *Purpose:* Start a new build
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* `{"blueprint_id": "int", "planet_id": "int"}`
|
||||||
|
- *Response:* `{"build_id": "int", "status": "string"}`
|
||||||
|
|
||||||
|
- **DELETE /build/cancel**
|
||||||
|
- *Purpose:* Cancel a build
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* `{"build_id": "int"}`
|
||||||
|
- *Response:* `{"status": "string"}`
|
||||||
18
docs/wiki/economy.md
Normal file
18
docs/wiki/economy.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## Economy Module
|
||||||
|
|
||||||
|
### Module Description
|
||||||
|
Manages in-game economic transactions, resource management, and market interactions.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- **GET /economy/state**
|
||||||
|
- *Purpose:* Retrieve current economic state
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* None
|
||||||
|
- *Response:* `{"credits": "int", "resources": {"ore": "int", "crystal": "int"}}`
|
||||||
|
|
||||||
|
- **POST /economy/transaction**
|
||||||
|
- *Purpose:* Execute resource transaction
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* `{"type": "string", "amount": "int", "target": "string"}`
|
||||||
|
- *Response:* `{"status": "string", "transaction_id": "string"}`
|
||||||
10
docs/wiki/index.md
Normal file
10
docs/wiki/index.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## Module Overview
|
||||||
|
|
||||||
|
- [Auth Module](auth.md): Handles user authentication and permissions.
|
||||||
|
- [Economy Module](economy.md): Manages in-game economy and transactions.
|
||||||
|
- [Build Queue Module](build_queue.md): Processes and manages construction queues.
|
||||||
|
- [Planet Generator Module](planet_generator.md): Generates planetary environments.
|
||||||
|
- [Blueprints Module](blueprints.md): Defines and manages construction blueprints.
|
||||||
|
- [Permissions Module](permissions.md): Controls access and permissions across the system.
|
||||||
|
|
||||||
|
For detailed information about each module, refer to their respective pages.
|
||||||
24
docs/wiki/permissions.md
Normal file
24
docs/wiki/permissions.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## Permissions Module
|
||||||
|
|
||||||
|
### Module Description
|
||||||
|
Controls access management and permission definitions across the system.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- **GET /permissions/list**
|
||||||
|
- *Purpose:* Retrieve all defined permissions
|
||||||
|
- *Auth:* Required (JWT token with `permission.read`)
|
||||||
|
- *Payload:* None
|
||||||
|
- *Response:* `{"permissions": ["string"]}`
|
||||||
|
|
||||||
|
- **POST /permissions/grant**
|
||||||
|
- *Purpose:* Assign a permission to a user
|
||||||
|
- *Auth:* Required (JWT token with `permission.write`)
|
||||||
|
- *Payload:* `{"user_id": "int", "permission": "string"}`
|
||||||
|
- *Response:* `{"status": "string"}`
|
||||||
|
|
||||||
|
- **DELETE /permissions/revoke**
|
||||||
|
- *Purpose:* Remove a permission from a user
|
||||||
|
- *Auth:* Required (JWT token with `permission.write`)
|
||||||
|
- *Payload:* `{"user_id": "int", "permission": "string"}`
|
||||||
|
- *Response:* `{"status": "string"}`
|
||||||
18
docs/wiki/planet_generator.md
Normal file
18
docs/wiki/planet_generator.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## Planet Generator Module
|
||||||
|
|
||||||
|
### Module Description
|
||||||
|
Generates planetary environments with unique characteristics and resources.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- **GET /planet/generate**
|
||||||
|
- *Purpose:* Generate new planet data
|
||||||
|
- *Auth:* Required (JWT token with `planet.generate` permission)
|
||||||
|
- *Payload:* `{"class": "string", "size": "string"}`
|
||||||
|
- *Response:* `{"planet_id": "int", "resources": {"ore": "int", "crystal": "int"}, "climate": "string"}`
|
||||||
|
|
||||||
|
- **GET /planet/detail**
|
||||||
|
- *Purpose:* Retrieve planet details
|
||||||
|
- *Auth:* Required (JWT token)
|
||||||
|
- *Payload:* `{"planet_id": "int"}`
|
||||||
|
- *Response:* `{"planet_id": "int", "class": "string", "resources": {"ore": "int", "crystal": "int"}, "climate": "string"}`
|
||||||
34
planning/rulebook_aufgaben.md
Normal file
34
planning/rulebook_aufgaben.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Rulebook offene Punkte – Umsetzungs- & Doku-Plan
|
||||||
|
|
||||||
|
Quelle/SSOT: `docs/game_rulebook_v1_2.md`
|
||||||
|
|
||||||
|
## Ziele
|
||||||
|
- Rulebook-konforme Implementierung der fehlenden Mechaniken (Auto-Cost, Blueprint-Baukasten, Build-Queue).
|
||||||
|
- Vollständige, verständliche Entwickler-Dokumentation unter `docs/wiki/`.
|
||||||
|
|
||||||
|
## Aufgaben (geordnet)
|
||||||
|
1. [ ] Auto-Cost-Score/Budget-Mechanik definieren (Rulebook §10)
|
||||||
|
- [ ] Score-Modell pro Effect-Typ festlegen (produce/consume/convert/capacity_add/queue_slots_add/points_add/modifier_add/grant_capability).
|
||||||
|
- [ ] Ressourcen-Werttabelle übernehmen und in Score-Formel einbauen.
|
||||||
|
- [ ] Budget pro Blueprint-Kategorie (building/ship/research/race/specialization) definieren.
|
||||||
|
- [ ] Ableitung von cost/build_time/upkeep (monoton) spezifizieren.
|
||||||
|
- [ ] UI-Breakdown-Format (transparente Scores) definieren.
|
||||||
|
|
||||||
|
2. [ ] Blueprint-Effects/Requirements/Access-Regeln vollständig abbilden (Rulebook §7)
|
||||||
|
- [ ] Requirements: building_count, building_tag_count, research_level, has_capability, player_race_in/_not_in, player_spec_in/_not_in.
|
||||||
|
- [ ] Access: allowed_/blocked_races sowie allowed_/blocked_specializations.
|
||||||
|
- [ ] Capabilities/Unlocks: grant_capability & menu:* Mechanik dokumentieren.
|
||||||
|
|
||||||
|
3. [ ] Build-Queue/Queue-Slots-Workflow definieren (Rulebook §8)
|
||||||
|
- [ ] Queue-Slots-Formel: base_slots + Σ(queue_slots_add).
|
||||||
|
- [ ] Job-Start-Flow: update_resources → slots → requirements/access → auto-cost → resource withdraw → job anlegen.
|
||||||
|
- [ ] Job-Finish-Flow: apply count/level, job entfernen, optional Event/Report.
|
||||||
|
- [ ] Destroyable-Flag/ Bombard-Handling (Rulebook §9) für Gebäude berücksichtigen.
|
||||||
|
|
||||||
|
4. [ ] Dokumentation in `docs/wiki/` ergänzen
|
||||||
|
- [ ] Rulebook-Mechaniken als neue Seite (z. B. rulebook.md) oder Erweiterung bestehender Seiten dokumentieren.
|
||||||
|
- [ ] API-Seiten (blueprints/build_queue/economy/planet_generator) mit Regel- und Datenmodell-Details ergänzen.
|
||||||
|
- [ ] Verweise auf SSOT und Decision Log aufnehmen.
|
||||||
|
|
||||||
|
## Notizen/Entscheidungen
|
||||||
|
- TBD
|
||||||
@@ -6,9 +6,14 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
race_key TEXT NOT NULL DEFAULT 'human',
|
race_key TEXT NOT NULL DEFAULT 'human',
|
||||||
title TEXT NOT NULL DEFAULT '',
|
title TEXT NOT NULL DEFAULT '',
|
||||||
avatar_key TEXT NOT NULL DEFAULT 'default',
|
avatar_key TEXT NOT NULL DEFAULT 'default',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
activation_token TEXT,
|
||||||
|
email_verified_at TIMESTAMP NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS users_activation_token_idx ON users (activation_token);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS planets (
|
CREATE TABLE IF NOT EXISTS planets (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
|
use App\Config\AppConfig;
|
||||||
use App\Config\ConfigLoader;
|
use App\Config\ConfigLoader;
|
||||||
use App\Config\ConfigValidator;
|
use App\Config\ConfigValidator;
|
||||||
|
use App\Module\Email\EmailSenderInterface;
|
||||||
|
use App\Module\Email\SmtpTransport;
|
||||||
use App\Database\ConnectionFactory;
|
use App\Database\ConnectionFactory;
|
||||||
use App\Module\Auth\Middleware\AuthContextMiddleware;
|
use App\Module\Auth\Middleware\AuthContextMiddleware;
|
||||||
use App\Module\Auth\Service\AuthService;
|
use App\Module\Auth\Service\AuthService;
|
||||||
@@ -37,7 +40,14 @@ final class Bootstrap
|
|||||||
return new ConfigValidator();
|
return new ConfigValidator();
|
||||||
},
|
},
|
||||||
ConfigLoader::class => function (ConfigValidator $validator) {
|
ConfigLoader::class => function (ConfigValidator $validator) {
|
||||||
return new ConfigLoader($validator);
|
$configDir = dirname(__DIR__, 3) . '/config';
|
||||||
|
return ConfigLoader::fromDirectory($validator, $configDir);
|
||||||
|
},
|
||||||
|
AppConfig::class => function () {
|
||||||
|
return AppConfig::fromEnvironment();
|
||||||
|
},
|
||||||
|
EmailSenderInterface::class => function (AppConfig $config) {
|
||||||
|
return new SmtpTransport($config);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
131
server/src/Config/AppConfig.php
Normal file
131
server/src/Config/AppConfig.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Config;
|
||||||
|
|
||||||
|
final class AppConfig
|
||||||
|
{
|
||||||
|
private bool $devEntwicklung;
|
||||||
|
private bool $emailLogTransport;
|
||||||
|
private string $emailSmtpHost;
|
||||||
|
private int $emailSmtpPort;
|
||||||
|
private string $emailSmtpUsername;
|
||||||
|
private string $emailSmtpPassword;
|
||||||
|
private string $emailFromAddress;
|
||||||
|
private string $emailSmtpSecure;
|
||||||
|
private int $emailSmtpTimeout;
|
||||||
|
private string $emailLogPath;
|
||||||
|
private string $appUrl;
|
||||||
|
|
||||||
|
private function __construct(
|
||||||
|
bool $devEntwicklung,
|
||||||
|
bool $emailLogTransport,
|
||||||
|
string $emailSmtpHost,
|
||||||
|
int $emailSmtpPort,
|
||||||
|
string $emailSmtpUsername,
|
||||||
|
string $emailSmtpPassword,
|
||||||
|
string $emailFromAddress,
|
||||||
|
string $emailSmtpSecure,
|
||||||
|
int $emailSmtpTimeout,
|
||||||
|
string $emailLogPath,
|
||||||
|
string $appUrl
|
||||||
|
) {
|
||||||
|
$this->devEntwicklung = $devEntwicklung;
|
||||||
|
$this->emailLogTransport = $emailLogTransport;
|
||||||
|
$this->emailSmtpHost = $emailSmtpHost;
|
||||||
|
$this->emailSmtpPort = $emailSmtpPort;
|
||||||
|
$this->emailSmtpUsername = $emailSmtpUsername;
|
||||||
|
$this->emailSmtpPassword = $emailSmtpPassword;
|
||||||
|
$this->emailFromAddress = $emailFromAddress;
|
||||||
|
$this->emailSmtpSecure = $emailSmtpSecure;
|
||||||
|
$this->emailSmtpTimeout = $emailSmtpTimeout;
|
||||||
|
$this->emailLogPath = $emailLogPath;
|
||||||
|
$this->appUrl = rtrim($appUrl, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromEnvironment(): self
|
||||||
|
{
|
||||||
|
$env = static fn (string $key, string $fallback = ''): string => getenv($key) ?: $fallback;
|
||||||
|
$port = (int)$env('EMAIL_SMTP_PORT', '587');
|
||||||
|
$timeout = (int)$env('EMAIL_SMTP_TIMEOUT', '30');
|
||||||
|
$secure = strtolower($env('EMAIL_SMTP_SECURE', 'tls'));
|
||||||
|
if (!in_array($secure, ['ssl', 'tls', 'none'], true)) {
|
||||||
|
$secure = 'tls';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
(bool)(int)$env('DEV_ENTWICKLUNG', '0'),
|
||||||
|
(bool)(int)$env('EMAIL_LOG_TRANSPORT', '0'),
|
||||||
|
$env('EMAIL_SMTP_HOST', 'localhost'),
|
||||||
|
$port,
|
||||||
|
$env('EMAIL_SMTP_USERNAME', ''),
|
||||||
|
$env('EMAIL_SMTP_PASSWORD', ''),
|
||||||
|
$env('EMAIL_FROM_ADDRESS', 'no-reply@example.test'),
|
||||||
|
$secure,
|
||||||
|
$timeout,
|
||||||
|
$env('EMAIL_LOG_PATH', 'storage/logs/email.log'),
|
||||||
|
$env('APP_URL', 'http://localhost')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDevEntwicklung(): bool
|
||||||
|
{
|
||||||
|
return $this->devEntwicklung;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmailLogTransportEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->emailLogTransport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmailLogTransport(bool $enabled): void
|
||||||
|
{
|
||||||
|
$this->emailLogTransport = $enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailSmtpHost(): string
|
||||||
|
{
|
||||||
|
return $this->emailSmtpHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailSmtpPort(): int
|
||||||
|
{
|
||||||
|
return $this->emailSmtpPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailSmtpUsername(): string
|
||||||
|
{
|
||||||
|
return $this->emailSmtpUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailSmtpPassword(): string
|
||||||
|
{
|
||||||
|
return $this->emailSmtpPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailFromAddress(): string
|
||||||
|
{
|
||||||
|
return $this->emailFromAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailSmtpSecure(): string
|
||||||
|
{
|
||||||
|
return $this->emailSmtpSecure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailSmtpTimeout(): int
|
||||||
|
{
|
||||||
|
return $this->emailSmtpTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailLogPath(): string
|
||||||
|
{
|
||||||
|
return $this->emailLogPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAppUrl(): string
|
||||||
|
{
|
||||||
|
return $this->appUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,43 @@ final class ConfigLoader
|
|||||||
{
|
{
|
||||||
private array $config;
|
private array $config;
|
||||||
|
|
||||||
public function __construct(array $config)
|
private function __construct(array $config)
|
||||||
{
|
{
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function fromArray(array $config): self
|
||||||
|
{
|
||||||
|
return new self($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromDirectory(ConfigValidator $validator, string $configDir): self
|
||||||
|
{
|
||||||
|
$definitions = [
|
||||||
|
'blueprints_buildings' => 'validateBlueprintsBuildings',
|
||||||
|
'auto_cost_weights' => null,
|
||||||
|
'planet_classes' => 'validatePlanetClasses',
|
||||||
|
'races' => 'validateRaces',
|
||||||
|
'avatars' => 'validateAvatars',
|
||||||
|
];
|
||||||
|
$config = [];
|
||||||
|
foreach ($definitions as $file => $validatorMethod) {
|
||||||
|
$path = rtrim($configDir, '\\/') . '/' . $file . '.json';
|
||||||
|
if (!is_file($path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$payload = json_decode((string)file_get_contents($path), true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($validatorMethod !== null && method_exists($validator, $validatorMethod)) {
|
||||||
|
$validator->{$validatorMethod}($payload, $path);
|
||||||
|
}
|
||||||
|
$config[$file] = $payload;
|
||||||
|
}
|
||||||
|
return new self($config);
|
||||||
|
}
|
||||||
|
|
||||||
public function blueprintsBuildings(): array
|
public function blueprintsBuildings(): array
|
||||||
{
|
{
|
||||||
return $this->config['blueprints_buildings'] ?? [];
|
return $this->config['blueprints_buildings'] ?? [];
|
||||||
@@ -22,4 +54,19 @@ final class ConfigLoader
|
|||||||
{
|
{
|
||||||
return $this->config['auto_cost_weights'] ?? [];
|
return $this->config['auto_cost_weights'] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function planetClasses(): array
|
||||||
|
{
|
||||||
|
return $this->config['planet_classes'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function races(): array
|
||||||
|
{
|
||||||
|
return $this->config['races'] ?? ['races' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function avatars(): array
|
||||||
|
{
|
||||||
|
return $this->config['avatars'] ?? ['avatars' => []];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Module\Auth\Controller;
|
namespace App\Module\Auth\Controller;
|
||||||
|
|
||||||
use App\Config\ConfigLoader;
|
use App\Config\ConfigLoader;
|
||||||
|
use App\Config\AppConfig;
|
||||||
|
use App\Module\Email\EmailSenderInterface;
|
||||||
use App\Module\PlanetGenerator\Service\PlanetGenerator;
|
use App\Module\PlanetGenerator\Service\PlanetGenerator;
|
||||||
use App\Shared\Http\JsonResponder;
|
use App\Shared\Http\JsonResponder;
|
||||||
use PDO;
|
use PDO;
|
||||||
@@ -24,7 +26,9 @@ final class AuthController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private PDO $pdo,
|
private PDO $pdo,
|
||||||
private ConfigLoader $configLoader,
|
private ConfigLoader $configLoader,
|
||||||
private PlanetGenerator $planetGenerator
|
private PlanetGenerator $planetGenerator,
|
||||||
|
private EmailSenderInterface $emailSender,
|
||||||
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +148,8 @@ final class AuthController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerStep3(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
public function registerStep3(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||||
{
|
{
|
||||||
$draft = $this->getDraft();
|
$draft = $this->getDraft();
|
||||||
if (empty($draft['race_key']) || empty($draft['avatar_key']) || empty($draft['title'])) {
|
if (empty($draft['race_key']) || empty($draft['avatar_key']) || empty($draft['title'])) {
|
||||||
return JsonResponder::withJson($response, [
|
return JsonResponder::withJson($response, [
|
||||||
@@ -194,12 +198,14 @@ final class AuthController
|
|||||||
], 409);
|
], 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$token = bin2hex(random_bytes(16));
|
||||||
|
$isDev = $this->appConfig->isDevEntwicklung();
|
||||||
try {
|
try {
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'INSERT INTO users (username, email, password_hash, race_key, title, avatar_key)
|
'INSERT INTO users (username, email, password_hash, race_key, title, avatar_key, activation_token, is_active)
|
||||||
VALUES (:username, :email, :password_hash, :race_key, :title, :avatar_key)
|
VALUES (:username, :email, :password_hash, :race_key, :title, :avatar_key, :activation_token, :is_active)
|
||||||
RETURNING *'
|
RETURNING *'
|
||||||
);
|
);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
@@ -209,12 +215,14 @@ final class AuthController
|
|||||||
'race_key' => $draft['race_key'],
|
'race_key' => $draft['race_key'],
|
||||||
'title' => $draft['title'],
|
'title' => $draft['title'],
|
||||||
'avatar_key' => $draft['avatar_key'],
|
'avatar_key' => $draft['avatar_key'],
|
||||||
|
'activation_token' => $token,
|
||||||
|
'is_active' => $isDev,
|
||||||
]);
|
]);
|
||||||
$user = $stmt->fetch();
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
$userId = (int)($user['id'] ?? 0);
|
$userId = (int)($user['id'] ?? 0);
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
throw new \\RuntimeException('User-ID fehlt.');
|
throw new \RuntimeException('User-ID fehlt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assignRole($userId, 'player');
|
$this->assignRole($userId, 'player');
|
||||||
@@ -233,7 +241,7 @@ final class AuthController
|
|||||||
'error' => 'registration_failed',
|
'error' => 'registration_failed',
|
||||||
'message' => 'Registrierung fehlgeschlagen.',
|
'message' => 'Registrierung fehlgeschlagen.',
|
||||||
], 500);
|
], 500);
|
||||||
} catch (\\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->pdo->rollBack();
|
$this->pdo->rollBack();
|
||||||
return JsonResponder::withJson($response, [
|
return JsonResponder::withJson($response, [
|
||||||
'error' => 'registration_failed',
|
'error' => 'registration_failed',
|
||||||
@@ -242,11 +250,70 @@ final class AuthController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->clearDraft();
|
$this->clearDraft();
|
||||||
|
|
||||||
|
if (!$isDev) {
|
||||||
|
$this->sendConfirmationEmail($email, $token);
|
||||||
|
return JsonResponder::withJson($response, [
|
||||||
|
'status' => 'pending',
|
||||||
|
'message' => 'Bestätige deine E-Mail-Adresse via Link.',
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loginUser((int)$user['id']);
|
||||||
|
return JsonResponder::withJson($response, [
|
||||||
|
'user' => $this->buildUserSummary($user),
|
||||||
|
'status' => 'active',
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirmRegistration(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
$body = $this->parseBody($request);
|
||||||
|
$token = trim((string)($body['token'] ?? ''));
|
||||||
|
if ($token === '') {
|
||||||
|
return JsonResponder::withJson($response, [
|
||||||
|
'error' => 'invalid_token',
|
||||||
|
'message' => 'Token fehlt.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE activation_token = :token LIMIT 1');
|
||||||
|
$stmt->execute(['token' => $token]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
if (!$user) {
|
||||||
|
return JsonResponder::withJson($response, [
|
||||||
|
'error' => 'invalid_token',
|
||||||
|
'message' => 'Ungültiges Token.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool)($user['is_active'] ?? false)) {
|
||||||
|
return JsonResponder::withJson($response, [
|
||||||
|
'error' => 'already_active',
|
||||||
|
'message' => 'Account bereits aktiviert.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'UPDATE users SET is_active = TRUE, email_verified_at = :verified, activation_token = NULL WHERE id = :id'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'verified' => (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s'),
|
||||||
|
'id' => (int)$user['id'],
|
||||||
|
]);
|
||||||
$this->loginUser((int)$user['id']);
|
$this->loginUser((int)$user['id']);
|
||||||
|
|
||||||
return JsonResponder::withJson($response, [
|
return JsonResponder::withJson($response, [
|
||||||
'user' => $this->buildUserSummary($user),
|
'user' => $this->buildUserSummary($user),
|
||||||
], 201);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendConfirmationEmail(string $to, string $token): void
|
||||||
|
{
|
||||||
|
$url = $this->appConfig->getAppUrl();
|
||||||
|
$link = rtrim($url, '/') . '/auth/register/confirm?token=' . urlencode($token);
|
||||||
|
$body = "Bitte bestätige deinen Account:\n" . $link;
|
||||||
|
$this->emailSender->sendEmail($to, 'E-Mail-Adresse bestätigen', $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class Routes
|
|||||||
$group->post('/auth/register/step1', [$auth, 'registerStep1']);
|
$group->post('/auth/register/step1', [$auth, 'registerStep1']);
|
||||||
$group->post('/auth/register/step2', [$auth, 'registerStep2']);
|
$group->post('/auth/register/step2', [$auth, 'registerStep2']);
|
||||||
$group->post('/auth/register/step3', [$auth, 'registerStep3']);
|
$group->post('/auth/register/step3', [$auth, 'registerStep3']);
|
||||||
|
$group->post('/auth/register/confirm', [$auth, 'confirmRegistration']);
|
||||||
|
|
||||||
$group->get('/meta/races', [$meta, 'races']);
|
$group->get('/meta/races', [$meta, 'races']);
|
||||||
$group->get('/meta/avatars', [$meta, 'avatars']);
|
$group->get('/meta/avatars', [$meta, 'avatars']);
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Module\Auth\Service;
|
namespace App\Module\Auth\Service;
|
||||||
|
|
||||||
|
use App\Config\AppConfig;
|
||||||
use PDO;
|
use PDO;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
final class AuthService
|
final class AuthService
|
||||||
{
|
{
|
||||||
public function __construct(private PDO $pdo)
|
public function __construct(private PDO $pdo, private AppConfig $config)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,23 +27,41 @@ final class AuthService
|
|||||||
|
|
||||||
$sessionUserId = $this->getSessionUserId();
|
$sessionUserId = $this->getSessionUserId();
|
||||||
if ($sessionUserId !== null) {
|
if ($sessionUserId !== null) {
|
||||||
return $this->findUserById($sessionUserId);
|
$sessionUser = $this->findUserById($sessionUserId);
|
||||||
|
if ($this->isUserAllowed($sessionUser)) {
|
||||||
|
return $sessionUser;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((int)(getenv('DEV_MODE') ?: 0) === 1) {
|
if ((int)(getenv('DEV_MODE') ?: 0) === 1) {
|
||||||
$devUserId = getenv('DEV_USER_ID');
|
$devUserId = getenv('DEV_USER_ID');
|
||||||
if ($devUserId !== false && is_numeric($devUserId)) {
|
if ($devUserId !== false && is_numeric($devUserId)) {
|
||||||
$user = $this->findUserById((int)$devUserId);
|
$user = $this->findUserById((int)$devUserId);
|
||||||
if ($user) {
|
if ($user && $this->isUserAllowed($user)) {
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $this->findFirstUser();
|
$firstUser = $this->findFirstUser();
|
||||||
|
if ($this->isUserAllowed($firstUser)) {
|
||||||
|
return $firstUser;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isUserAllowed(?array $user): bool
|
||||||
|
{
|
||||||
|
if ($user === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($this->config->isDevEntwicklung()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (bool)($user['is_active'] ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
private function extractUserId(ServerRequestInterface $request): ?int
|
private function extractUserId(ServerRequestInterface $request): ?int
|
||||||
{
|
{
|
||||||
$header = $request->getHeaderLine('X-User-Id');
|
$header = $request->getHeaderLine('X-User-Id');
|
||||||
|
|||||||
12
server/src/Module/Email/EmailSenderInterface.php
Normal file
12
server/src/Module/Email/EmailSenderInterface.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Email;
|
||||||
|
|
||||||
|
interface EmailSenderInterface
|
||||||
|
{
|
||||||
|
public function sendEmail(string $to, string $subject, string $body): void;
|
||||||
|
|
||||||
|
public function enableLogTransport(bool $enabled): void;
|
||||||
|
}
|
||||||
81
server/src/Module/Email/SmtpTransport.php
Normal file
81
server/src/Module/Email/SmtpTransport.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Email;
|
||||||
|
|
||||||
|
use App\Config\AppConfig;
|
||||||
|
|
||||||
|
final class SmtpTransport implements EmailSenderInterface
|
||||||
|
{
|
||||||
|
private bool $logTransportEnabled;
|
||||||
|
|
||||||
|
public function __construct(private AppConfig $config)
|
||||||
|
{
|
||||||
|
$this->logTransportEnabled = $config->isEmailLogTransportEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendEmail(string $to, string $subject, string $body): void
|
||||||
|
{
|
||||||
|
if ($this->logTransportEnabled) {
|
||||||
|
$this->log($to, $subject, $body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transport = fsockopen($this->config->getEmailSmtpHost(), $this->config->getEmailSmtpPort(), $errno, $errstr, $this->config->getEmailSmtpTimeout());
|
||||||
|
if ($transport === false) {
|
||||||
|
throw new \RuntimeException("SMTP-Verbindung fehlgeschlagen: {$errno} {$errstr}");
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_set_timeout($transport, $this->config->getEmailSmtpTimeout());
|
||||||
|
|
||||||
|
$secure = $this->config->getEmailSmtpSecure();
|
||||||
|
if ($secure === 'ssl') {
|
||||||
|
// Wartet auf SSL-Handshake
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->write($transport, "HELO " . parse_url($this->config->getAppUrl(), PHP_URL_HOST) . "\r\n");
|
||||||
|
$this->write($transport, "AUTH LOGIN\r\n");
|
||||||
|
$this->write($transport, base64_encode($this->config->getEmailSmtpUsername()) . "\r\n");
|
||||||
|
$this->write($transport, base64_encode($this->config->getEmailSmtpPassword()) . "\r\n");
|
||||||
|
$this->write($transport, "MAIL FROM:<{$this->config->getEmailFromAddress()}>\r\n");
|
||||||
|
$this->write($transport, "RCPT TO:<{$to}>\r\n");
|
||||||
|
$this->write($transport, "DATA\r\n");
|
||||||
|
$message = "Subject: {$subject}\r\n";
|
||||||
|
$message .= "From: {$this->config->getEmailFromAddress()}\r\n";
|
||||||
|
$message .= "To: {$to}\r\n";
|
||||||
|
$message .= "\r\n";
|
||||||
|
$message .= $body . "\r\n.\r\n";
|
||||||
|
$this->write($transport, $message);
|
||||||
|
$this->write($transport, "QUIT\r\n");
|
||||||
|
|
||||||
|
fclose($transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enableLogTransport(bool $enabled): void
|
||||||
|
{
|
||||||
|
$this->logTransportEnabled = $enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function write($transport, string $message): void
|
||||||
|
{
|
||||||
|
fwrite($transport, $message);
|
||||||
|
fflush($transport);
|
||||||
|
$response = fgets($transport);
|
||||||
|
if ($response === false) {
|
||||||
|
throw new \RuntimeException('SMTP: Keine Antwort vom Server.');
|
||||||
|
}
|
||||||
|
$code = (int)substr($response, 0, 3);
|
||||||
|
if ($code >= 400) {
|
||||||
|
throw new \RuntimeException("SMTP-Fehler {$code}: {$response}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function log(string $to, string $subject, string $body): void
|
||||||
|
{
|
||||||
|
$path = $this->config->getEmailLogPath();
|
||||||
|
$now = (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s');
|
||||||
|
$entry = "[{$now}] TO: {$to} SUBJECT: {$subject}\n{$body}\n\n";
|
||||||
|
file_put_contents($path, $entry, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,20 @@ final class AuthFlowTest extends TestCase
|
|||||||
], $cookie);
|
], $cookie);
|
||||||
$response3 = $app->handle($step3);
|
$response3 = $app->handle($step3);
|
||||||
self::assertSame(201, $response3->getStatusCode());
|
self::assertSame(201, $response3->getStatusCode());
|
||||||
$cookie = $this->extractCookie($response3, $cookie);
|
$body3 = json_decode((string)$response3->getBody(), true);
|
||||||
|
self::assertSame('pending', $body3['status']);
|
||||||
|
|
||||||
|
// confirm registration
|
||||||
|
$pdo->exec("UPDATE users SET activation_token = 'confirm-token' WHERE username = 'neo'");
|
||||||
|
$confirm = $this->jsonRequest($factory, 'POST', '/auth/register/confirm', [
|
||||||
|
'token' => 'confirm-token',
|
||||||
|
], $cookie);
|
||||||
|
$confirmResponse = $app->handle($confirm);
|
||||||
|
self::assertSame(200, $confirmResponse->getStatusCode());
|
||||||
|
$confirmMeRequest = $factory->createServerRequest('POST', '/auth/register/confirm', ['token' => 'confirm-token']);
|
||||||
|
$confirmMeRequest = $confirmMeRequest->withHeader('Cookie', $this->extractCookie($response3, $cookie));
|
||||||
|
$confirmMeResponse = $app->handle($confirmMeRequest);
|
||||||
|
self::assertSame(200, $confirmMeResponse->getStatusCode());
|
||||||
|
|
||||||
$meRequest = $factory->createServerRequest('GET', '/me');
|
$meRequest = $factory->createServerRequest('GET', '/me');
|
||||||
if ($cookie) {
|
if ($cookie) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ final class TestAppFactory
|
|||||||
$container = Bootstrap::buildContainer([
|
$container = Bootstrap::buildContainer([
|
||||||
\PDO::class => $pdo,
|
\PDO::class => $pdo,
|
||||||
TimeProvider::class => $timeProvider,
|
TimeProvider::class => $timeProvider,
|
||||||
ConfigLoader::class => new ConfigLoader(new ConfigValidator(), $repoRoot . '/config'),
|
ConfigLoader::class => ConfigLoader::fromDirectory(new ConfigValidator(), $repoRoot . '/config'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Bootstrap::createApp($container);
|
return Bootstrap::createApp($container);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final class AutoCostCalculatorTest extends TestCase
|
|||||||
'capacity_add' => 3,
|
'capacity_add' => 3,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$configLoader = new ConfigLoader($config);
|
$configLoader = ConfigLoader::fromArray($config);
|
||||||
$calculator = new AutoCostCalculator($configLoader);
|
$calculator = new AutoCostCalculator($configLoader);
|
||||||
|
|
||||||
$blueprint = [
|
$blueprint = [
|
||||||
@@ -54,4 +54,4 @@ final class AutoCostCalculatorTest extends TestCase
|
|||||||
$this->assertEquals(100, $result['cost']);
|
$this->assertEquals(100, $result['cost']);
|
||||||
$this->assertEquals(60, $result['build_time']);
|
$this->assertEquals(60, $result['build_time']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user