$max) { return $max; } return $limit; } /** * @return array> */ public function listSelfTokens(int $userId, int $limit, ?int $scopedTenantId, ?int $currentTokenId): array { if ($userId <= 0) { return []; } $tokens = $this->apiTokenRepository->listByUserId($userId, $limit); $tenantUuidById = $this->buildTenantUuidByIdMap($tokens); $rows = []; foreach ($tokens as $token) { $tokenTenantId = $this->normalizeTenantId($token['tenant_id'] ?? null); if ($scopedTenantId !== null && $scopedTenantId > 0 && $tokenTenantId !== $scopedTenantId) { continue; } $rows[] = [ 'uuid' => (string) ($token['uuid'] ?? ''), 'name' => (string) ($token['name'] ?? ''), 'selector_prefix' => substr((string) ($token['selector'] ?? ''), 0, 8), 'tenant_uuid' => $tokenTenantId !== null ? ($tenantUuidById[$tokenTenantId] ?? null) : null, 'last_used_at' => $token['last_used_at'] ?? null, 'last_ip' => (string) ($token['last_ip'] ?? ''), 'expires_at' => $token['expires_at'] ?? null, 'revoked_at' => $token['revoked_at'] ?? null, 'created' => $token['created'] ?? null, 'is_current_token' => (int) ($token['id'] ?? 0) === (int) ($currentTokenId ?? 0), ]; } return $rows; } /** * @return array|null */ public function findSelfToken(string $tokenUuid, int $userId, ?int $scopedTenantId, ?int $currentTokenId): ?array { $tokenUuid = trim($tokenUuid); if ($userId <= 0 || !$this->apiTokenRepository->isUuid($tokenUuid)) { return null; } $token = $this->apiTokenRepository->findByUuidForUser($tokenUuid, $userId); if (!$token) { return null; } $tokenTenantId = $this->normalizeTenantId($token['tenant_id'] ?? null); if ($scopedTenantId !== null && $scopedTenantId > 0 && $tokenTenantId !== $scopedTenantId) { return null; } return [ 'uuid' => (string) ($token['uuid'] ?? ''), 'name' => (string) ($token['name'] ?? ''), 'selector_prefix' => substr((string) ($token['selector'] ?? ''), 0, 8), 'tenant_uuid' => $this->resolveTenantUuidById($tokenTenantId), 'last_used_at' => $token['last_used_at'] ?? null, 'last_ip' => (string) ($token['last_ip'] ?? ''), 'expires_at' => $token['expires_at'] ?? null, 'revoked_at' => $token['revoked_at'] ?? null, 'created' => $token['created'] ?? null, 'is_current_token' => (int) ($token['id'] ?? 0) === (int) ($currentTokenId ?? 0), ]; } public function revokeSelfToken(string $tokenUuid, int $userId): bool { $tokenUuid = trim($tokenUuid); if ($userId <= 0 || !$this->apiTokenRepository->isUuid($tokenUuid)) { return false; } return $this->apiTokenRepository->revokeByUuidForUser($tokenUuid, $userId); } /** * @return array{ok: bool, tenant_id?: int|null, tenant_uuid?: string|null, status?: int, error?: string, field?: string} */ public function resolveSelfCreateTenant(int $userId, ?int $scopedTenantId, string $tenantUuidInput): array { $tenantUuidInput = trim($tenantUuidInput); $tenantId = null; $tenantUuid = null; if ($tenantUuidInput !== '') { $tenant = $this->tenantRepository->findByUuid($tenantUuidInput); $tenantId = (int) ($tenant['id'] ?? 0); $tenantUuid = trim((string) ($tenant['uuid'] ?? '')); if ($tenantId <= 0 || $tenantUuid === '') { return [ 'ok' => false, 'status' => 422, 'error' => 'invalid_tenant_uuid', 'field' => 'tenant_uuid', ]; } } if ($scopedTenantId !== null && $scopedTenantId > 0) { $scopedTenantUuid = $this->resolveTenantUuidById($scopedTenantId); if ($scopedTenantUuid === null) { return ['ok' => false, 'status' => 403, 'error' => 'tenant_scoped_token_forbidden']; } if ($tenantId !== null && $tenantId !== $scopedTenantId) { return ['ok' => false, 'status' => 403, 'error' => 'tenant_scoped_token_forbidden']; } $tenantId = $scopedTenantId; $tenantUuid = $scopedTenantUuid; } if ($tenantId !== null) { $tenantIds = $this->authScopeGateway->getUserTenantIds($userId); if (!in_array($tenantId, $tenantIds, true)) { return ['ok' => false, 'status' => 403, 'error' => 'tenant_not_assigned']; } } return [ 'ok' => true, 'tenant_id' => $tenantId, 'tenant_uuid' => $tenantUuid, ]; } /** * @return array{ok: bool, expires_at?: string|null, error?: string} */ public function parseExpiresAt(string $expiresInput): array { $expiresInput = trim($expiresInput); if ($expiresInput === '') { return ['ok' => true, 'expires_at' => null]; } try { $expiresDate = new \DateTimeImmutable($expiresInput); $expiresDateUtc = $expiresDate->setTimezone(new \DateTimeZone('UTC')); $nowUtc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); if ($expiresDateUtc <= $nowUtc) { return ['ok' => false, 'error' => 'must_be_in_future']; } return ['ok' => true, 'expires_at' => $expiresDateUtc->format('Y-m-d H:i:s')]; } catch (\Throwable $exception) { return ['ok' => false, 'error' => 'invalid_datetime']; } } public function capExpiryToParent(?string $requestedExpiresAt, ?string $parentExpiresAt): ?string { $parentExpiresAt = trim((string) ($parentExpiresAt ?? '')); if ($parentExpiresAt === '') { return $requestedExpiresAt; } try { $parentExpiry = new \DateTimeImmutable($parentExpiresAt, new \DateTimeZone('UTC')); if ($requestedExpiresAt === null || trim($requestedExpiresAt) === '') { return $parentExpiresAt; } $requestedExpiry = new \DateTimeImmutable($requestedExpiresAt, new \DateTimeZone('UTC')); if ($requestedExpiry > $parentExpiry) { return $parentExpiresAt; } } catch (\Throwable $exception) { return $requestedExpiresAt; } return $requestedExpiresAt; } /** * @return array{ok: bool, tenant_id?: int|null, error?: string, status?: int} */ public function resolveLoginTenant(int $userId, string $tenantUuidInput): array { $tenantUuidInput = trim($tenantUuidInput); if ($tenantUuidInput === '') { return ['ok' => true, 'tenant_id' => null]; } $tenant = $this->tenantRepository->findByUuid($tenantUuidInput); $tenantId = (int) ($tenant['id'] ?? 0); if ($tenantId <= 0) { return ['ok' => false, 'error' => 'invalid_tenant_uuid', 'status' => 422]; } $userTenantIds = $this->authScopeGateway->getUserTenantIds($userId); if (!in_array($tenantId, $userTenantIds, true)) { return ['ok' => false, 'error' => 'tenant_not_assigned', 'status' => 403]; } return ['ok' => true, 'tenant_id' => $tenantId]; } public function resolveTenantUuidById(?int $tenantId): ?string { $tenantId = (int) ($tenantId ?? 0); if ($tenantId <= 0) { return null; } $tenant = $this->tenantRepository->find($tenantId); $tenantUuid = trim((string) ($tenant['uuid'] ?? '')); return $tenantUuid !== '' ? $tenantUuid : null; } /** * @param array> $tokens * @return array */ private function buildTenantUuidByIdMap(array $tokens): array { $tenantIds = []; foreach ($tokens as $token) { $tenantId = $this->normalizeTenantId($token['tenant_id'] ?? null); if ($tenantId !== null) { $tenantIds[] = $tenantId; } } $tenantIds = array_values(array_unique($tenantIds)); if (!$tenantIds) { return []; } $map = []; foreach ($this->tenantRepository->listByIds($tenantIds) as $tenant) { $tenantId = (int) ($tenant['id'] ?? 0); $tenantUuid = trim((string) ($tenant['uuid'] ?? '')); if ($tenantId > 0 && $tenantUuid !== '') { $map[$tenantId] = $tenantUuid; } } return $map; } private function normalizeTenantId(mixed $tenantId): ?int { $tenantId = (int) ($tenantId ?? 0); return $tenantId > 0 ? $tenantId : null; } }