instances added god may help

This commit is contained in:
2026-02-23 12:58:19 +01:00
parent 25370a1a55
commit 99db252f60
290 changed files with 9858 additions and 4914 deletions

View File

@@ -4,15 +4,21 @@ namespace MintyPHP\Service\Auth;
use MintyPHP\DB;
use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Repository\User\UserRepository;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Repository\User\UserReadRepository;
class ApiTokenService
{
private const MAX_TOKENS_PER_USER = 10;
private static function isTokenExpired(?string $expiresAt): bool
public function __construct(
private readonly UserReadRepository $userReadRepository,
private readonly ApiTokenRepository $apiTokenRepository,
private readonly AuthSettingsGateway $settingsGateway,
private readonly AuthScopeGateway $scopeGateway
) {
}
private function isTokenExpired(?string $expiresAt): bool
{
$expiresAt = trim((string) ($expiresAt ?? ''));
if ($expiresAt === '') {
@@ -32,7 +38,7 @@ class ApiTokenService
* Returns ['ok' => true, 'token' => 'selector:plaintext', 'id' => int] on success.
* The plain-text token is shown ONCE and never stored.
*/
public static function create(
public function create(
int $userId,
string $name,
?int $tenantId = null,
@@ -47,24 +53,24 @@ class ApiTokenService
return ['ok' => false, 'error' => 'invalid_user'];
}
$user = UserRepository::find($userId);
$user = $this->userReadRepository->find($userId);
if (!$user || !((int) ($user['active'] ?? 0))) {
return ['ok' => false, 'error' => 'user_inactive'];
}
$activeCount = ApiTokenRepository::countActiveForUser($userId);
$activeCount = $this->apiTokenRepository->countActiveForUser($userId);
if ($activeCount >= self::MAX_TOKENS_PER_USER) {
return ['ok' => false, 'error' => 'max_tokens_reached'];
}
if ($tenantId !== null && $tenantId > 0) {
$userTenantIds = TenantScopeService::getUserTenantIds($userId);
$userTenantIds = $this->scopeGateway->getUserTenantIds($userId);
if (!in_array($tenantId, $userTenantIds, true)) {
return ['ok' => false, 'error' => 'tenant_not_assigned'];
}
}
$resolvedExpiresAt = self::resolveExpiresAt($expiresAt);
$resolvedExpiresAt = $this->resolveExpiresAt($expiresAt);
if ($resolvedExpiresAt === null) {
return ['ok' => false, 'error' => 'invalid_expires_at'];
}
@@ -73,7 +79,7 @@ class ApiTokenService
$plainToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $plainToken);
$id = ApiTokenRepository::create(
$id = $this->apiTokenRepository->create(
$userId,
$name,
$selector,
@@ -87,7 +93,7 @@ class ApiTokenService
return ['ok' => false, 'error' => 'create_failed'];
}
$createdToken = ApiTokenRepository::find($id);
$createdToken = $this->apiTokenRepository->find($id);
if (!$createdToken) {
return ['ok' => false, 'error' => 'create_failed'];
}
@@ -103,22 +109,22 @@ class ApiTokenService
];
}
public static function rotateCurrentToken(int $userId, int $currentTokenId, ?string $expiresAt = null): array
public function rotateCurrentToken(int $userId, int $currentTokenId, ?string $expiresAt = null): array
{
if ($userId <= 0 || $currentTokenId <= 0) {
return ['ok' => false, 'error' => 'current_token_not_found'];
}
$currentToken = ApiTokenRepository::find($currentTokenId);
$currentToken = $this->apiTokenRepository->find($currentTokenId);
if (!$currentToken || (int) ($currentToken['user_id'] ?? 0) !== $userId) {
return ['ok' => false, 'error' => 'current_token_not_found'];
}
if (!empty($currentToken['revoked_at']) || self::isTokenExpired($currentToken['expires_at'] ?? null)) {
if (!empty($currentToken['revoked_at']) || $this->isTokenExpired($currentToken['expires_at'] ?? null)) {
return ['ok' => false, 'error' => 'current_token_invalid'];
}
$activeCountExcludingCurrent = ApiTokenRepository::countActiveForUserExcludingId($userId, $currentTokenId);
$activeCountExcludingCurrent = $this->apiTokenRepository->countActiveForUserExcludingId($userId, $currentTokenId);
if ($activeCountExcludingCurrent >= self::MAX_TOKENS_PER_USER) {
return ['ok' => false, 'error' => 'max_tokens_reached'];
}
@@ -139,12 +145,12 @@ class ApiTokenService
$db->begin_transaction();
$transactionStarted = true;
if (!ApiTokenRepository::revoke($currentTokenId)) {
if (!$this->apiTokenRepository->revoke($currentTokenId)) {
$db->rollback();
return ['ok' => false, 'error' => 'rotate_failed'];
}
$created = self::create($userId, $name, $tenantId, $expiresAt, $userId);
$created = $this->create($userId, $name, $tenantId, $expiresAt, $userId);
if (!($created['ok'] ?? false)) {
$db->rollback();
$error = (string) ($created['error'] ?? 'rotate_failed');
@@ -172,15 +178,15 @@ class ApiTokenService
}
}
private static function resolveExpiresAt(?string $expiresAt): ?string
private function resolveExpiresAt(?string $expiresAt): ?string
{
$nowUtc = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$maxTtlDays = SettingService::getApiTokenMaxTtlDays();
$maxTtlDays = $this->settingsGateway->getApiTokenMaxTtlDays();
$maxExpiryUtc = $nowUtc->modify('+' . $maxTtlDays . ' days');
$raw = trim((string) ($expiresAt ?? ''));
if ($raw === '') {
$defaultDays = SettingService::getApiTokenDefaultTtlDays();
$defaultDays = $this->settingsGateway->getApiTokenDefaultTtlDays();
$defaultExpiry = $nowUtc->modify('+' . $defaultDays . ' days');
if ($defaultExpiry > $maxExpiryUtc) {
$defaultExpiry = $maxExpiryUtc;
@@ -210,7 +216,7 @@ class ApiTokenService
*
* @return array{user: array, token_record: array, tenant_id: ?int}|null
*/
public static function validate(string $bearerToken): ?array
public function validate(string $bearerToken): ?array
{
if ($bearerToken === '' || strpos($bearerToken, ':') === false) {
return null;
@@ -223,10 +229,10 @@ class ApiTokenService
return null;
}
$record = ApiTokenRepository::findBySelector($selector);
$record = $this->apiTokenRepository->findBySelector($selector);
if (!$record) {
// Perform a dummy hash to prevent timing-based selector enumeration.
hash('sha256', $plainToken);
$_ = hash('sha256', $plainToken);
return null;
}
@@ -234,7 +240,7 @@ class ApiTokenService
return null;
}
if (self::isTokenExpired($record['expires_at'] ?? null)) {
if ($this->isTokenExpired($record['expires_at'] ?? null)) {
return null;
}
@@ -248,13 +254,13 @@ class ApiTokenService
return null;
}
$user = UserRepository::find($userId);
$user = $this->userReadRepository->find($userId);
if (!$user || !((int) ($user['active'] ?? 0))) {
return null;
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
ApiTokenRepository::updateLastUsed((int) $record['id'], $ip);
$this->apiTokenRepository->updateLastUsed((int) $record['id'], $ip);
return [
'user' => $user,
@@ -266,35 +272,35 @@ class ApiTokenService
/**
* Revoke a specific token by ID.
*/
public static function revoke(int $tokenId): array
public function revoke(int $tokenId): array
{
$token = ApiTokenRepository::find($tokenId);
$token = $this->apiTokenRepository->find($tokenId);
if (!$token) {
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
}
ApiTokenRepository::revoke($tokenId);
$this->apiTokenRepository->revoke($tokenId);
return ['ok' => true];
}
/**
* Revoke all active tokens for a user.
*/
public static function revokeAllForUser(int $userId, ?int $tenantId = null): array
public function revokeAllForUser(int $userId, ?int $tenantId = null): array
{
$count = ApiTokenRepository::revokeAllForUser($userId, $tenantId);
$count = $this->apiTokenRepository->revokeAllForUser($userId, $tenantId);
return ['ok' => true, 'count' => $count];
}
public static function revokeAllByAdmin(): int
public function revokeAllByAdmin(): int
{
return ApiTokenRepository::revokeAllActiveByAdmin();
return $this->apiTokenRepository->revokeAllActiveByAdmin();
}
/**
* List tokens for a user (for admin UI display -- never exposes hashes).
*/
public static function listForUser(int $userId): array
public function listForUser(int $userId): array
{
return ApiTokenRepository::listByUserId($userId);
return $this->apiTokenRepository->listByUserId($userId);
}
}