fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled

- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
2025-11-09 14:46:15 +01:00
parent 85c369e846
commit 36ef2a1e2c
1366 changed files with 104925 additions and 28719 deletions

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\Mfa\ValueObjects;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* Represents an active MFA challenge/verification attempt
*
* Tracks the state of an MFA verification process, including:
* - Which MFA method is being used
* - When the challenge was created
* - When it expires
* - How many attempts have been made
*/
final readonly class MfaChallenge
{
public function __construct(
public string $challengeId,
public MfaMethod $method,
public DateTimeImmutable $createdAt,
public DateTimeImmutable $expiresAt,
public int $attempts = 0,
public int $maxAttempts = 3
) {
if (empty($challengeId)) {
throw new InvalidArgumentException('Challenge ID cannot be empty');
}
if ($attempts < 0) {
throw new InvalidArgumentException('Attempts cannot be negative');
}
if ($maxAttempts < 1) {
throw new InvalidArgumentException('Max attempts must be at least 1');
}
if ($expiresAt <= $createdAt) {
throw new InvalidArgumentException('Expiration must be after creation time');
}
}
public static function create(
string $challengeId,
MfaMethod $method,
int $validitySeconds = 300,
int $maxAttempts = 3
): self {
$now = new DateTimeImmutable();
$expiresAt = $now->modify("+{$validitySeconds} seconds");
return new self(
challengeId: $challengeId,
method: $method,
createdAt: $now,
expiresAt: $expiresAt,
attempts: 0,
maxAttempts: $maxAttempts
);
}
public function isExpired(): bool
{
return new DateTimeImmutable() >= $this->expiresAt;
}
public function hasAttemptsRemaining(): bool
{
return $this->attempts < $this->maxAttempts;
}
public function isValid(): bool
{
return !$this->isExpired() && $this->hasAttemptsRemaining();
}
public function withIncrementedAttempts(): self
{
return new self(
challengeId: $this->challengeId,
method: $this->method,
createdAt: $this->createdAt,
expiresAt: $this->expiresAt,
attempts: $this->attempts + 1,
maxAttempts: $this->maxAttempts
);
}
public function getRemainingAttempts(): int
{
return max(0, $this->maxAttempts - $this->attempts);
}
public function getSecondsUntilExpiry(): int
{
$now = new DateTimeImmutable();
$diff = $this->expiresAt->getTimestamp() - $now->getTimestamp();
return max(0, $diff);
}
public function toArray(): array
{
return [
'challenge_id' => $this->challengeId,
'method' => $this->method->value,
'method_display' => $this->method->getDisplayName(),
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
'expires_at' => $this->expiresAt->format('Y-m-d H:i:s'),
'attempts' => $this->attempts,
'max_attempts' => $this->maxAttempts,
'remaining_attempts' => $this->getRemainingAttempts(),
'seconds_until_expiry' => $this->getSecondsUntilExpiry(),
'is_valid' => $this->isValid(),
'is_expired' => $this->isExpired(),
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Framework\Mfa\ValueObjects;
use InvalidArgumentException;
/**
* MFA verification code value object
*/
final readonly class MfaCode
{
public function __construct(
public string $value
) {
if (empty($value)) {
throw new InvalidArgumentException('MFA code cannot be empty');
}
if (!preg_match('/^[0-9]{4,8}$/', $value)) {
throw new InvalidArgumentException('MFA code must be 4-8 digits');
}
}
public static function fromString(string $value): self
{
return new self(trim($value));
}
public function equals(self $other): bool
{
return hash_equals($this->value, $other->value);
}
public function length(): int
{
return strlen($this->value);
}
public function toString(): string
{
return $this->value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\Mfa\ValueObjects;
/**
* Multi-Factor Authentication methods supported by the framework
*/
enum MfaMethod: string
{
case TOTP = 'totp'; // Time-based One-Time Password (Google Authenticator, Authy)
case SMS = 'sms'; // SMS verification code
case EMAIL = 'email'; // Email verification code
case BACKUP_CODE = 'backup'; // Pre-generated backup codes
public function getDisplayName(): string
{
return match ($this) {
self::TOTP => 'Authenticator App',
self::SMS => 'SMS Code',
self::EMAIL => 'Email Code',
self::BACKUP_CODE => 'Backup Code',
};
}
public function requiresCodeGeneration(): bool
{
return match ($this) {
self::SMS, self::EMAIL => true,
self::TOTP, self::BACKUP_CODE => false,
};
}
public function getCodeLength(): int
{
return match ($this) {
self::TOTP => 6,
self::SMS, self::EMAIL => 6,
self::BACKUP_CODE => 8,
};
}
public function getCodeValiditySeconds(): int
{
return match ($this) {
self::TOTP => 30, // TOTP codes rotate every 30 seconds
self::SMS, self::EMAIL => 300, // SMS/Email codes valid for 5 minutes
self::BACKUP_CODE => 0, // Backup codes don't expire
};
}
public function isTimeBased(): bool
{
return $this === self::TOTP;
}
public function supportsResend(): bool
{
return match ($this) {
self::SMS, self::EMAIL => true,
self::TOTP, self::BACKUP_CODE => false,
};
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Framework\Mfa\ValueObjects;
use InvalidArgumentException;
/**
* MFA secret for TOTP authentication
*
* Immutable value object representing a base32-encoded secret
* used for Time-based One-Time Password (TOTP) generation.
*/
final readonly class MfaSecret
{
public function __construct(
public string $value
) {
if (empty($value)) {
throw new InvalidArgumentException('MFA secret cannot be empty');
}
// Validate base32 format (A-Z, 2-7, no padding for simplicity)
if (!preg_match('/^[A-Z2-7]+$/', $value)) {
throw new InvalidArgumentException('MFA secret must be base32 encoded (A-Z, 2-7)');
}
// TOTP secrets should be at least 128 bits (20 bytes = 32 base32 chars)
if (strlen($value) < 32) {
throw new InvalidArgumentException('MFA secret must be at least 32 characters (128 bits)');
}
}
public static function fromString(string $value): self
{
return new self(strtoupper(trim($value)));
}
/**
* Get the secret as a QR code URI for authenticator apps
*
* @param string $issuer The service name (e.g., "MyApp")
* @param string $accountName The user's account identifier (e.g., email)
*/
public function toQrCodeUri(string $issuer, string $accountName): string
{
$encodedIssuer = rawurlencode($issuer);
$encodedAccount = rawurlencode($accountName);
return sprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s',
$encodedIssuer,
$encodedAccount,
$this->value,
$encodedIssuer
);
}
/**
* Get a masked version of the secret for logging/display
* Shows first 4 and last 4 characters only
*/
public function getMasked(): string
{
if (strlen($this->value) <= 8) {
return '****';
}
return substr($this->value, 0, 4) . '****' . substr($this->value, -4);
}
public function toString(): string
{
return $this->value;
}
public function __toString(): string
{
return $this->value;
}
/**
* Base32 encode binary data (RFC 4648)
*
* @internal Used by MfaSecretFactory
*/
public static function base32Encode(string $data): string
{
$base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$encoded = '';
$buffer = 0;
$bitsLeft = 0;
foreach (str_split($data) as $byte) {
$buffer = ($buffer << 8) | ord($byte);
$bitsLeft += 8;
while ($bitsLeft >= 5) {
$bitsLeft -= 5;
$index = ($buffer >> $bitsLeft) & 0x1F;
$encoded .= $base32Chars[$index];
}
}
// Handle remaining bits
if ($bitsLeft > 0) {
$index = ($buffer << (5 - $bitsLeft)) & 0x1F;
$encoded .= $base32Chars[$index];
}
return $encoded;
}
}