instances added god may help
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user