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
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:
121
src/Framework/Mfa/ValueObjects/MfaChallenge.php
Normal file
121
src/Framework/Mfa/ValueObjects/MfaChallenge.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
50
src/Framework/Mfa/ValueObjects/MfaCode.php
Normal file
50
src/Framework/Mfa/ValueObjects/MfaCode.php
Normal 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;
|
||||
}
|
||||
}
|
||||
65
src/Framework/Mfa/ValueObjects/MfaMethod.php
Normal file
65
src/Framework/Mfa/ValueObjects/MfaMethod.php
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
114
src/Framework/Mfa/ValueObjects/MfaSecret.php
Normal file
114
src/Framework/Mfa/ValueObjects/MfaSecret.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user