Files
michaelschiemer/src/Framework/OAuth/Storage/OAuthTokenRepository.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

217 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\OAuth\Storage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\OAuth\ValueObjects\OAuthToken;
/**
* OAuth Token Repository
*
* Handles persistence of OAuth tokens with automatic refresh handling
*/
final readonly class OAuthTokenRepository implements OAuthTokenRepositoryInterface
{
private const TABLE = 'oauth_tokens';
public function __construct(
private ConnectionInterface $connection,
) {
}
/**
* Find token for user and provider
*/
public function findForUser(string $userId, string $provider): ?StoredOAuthToken
{
$sql = "SELECT * FROM " . self::TABLE . "
WHERE user_id = ? AND provider = ?
LIMIT 1";
$row = $this->connection->fetch($sql, [$userId, $provider]);
return $row ? StoredOAuthToken::fromArray($row) : null;
}
/**
* Find token by ID
*/
public function findById(int $id): ?StoredOAuthToken
{
$sql = "SELECT * FROM " . self::TABLE . " WHERE id = ? LIMIT 1";
$row = $this->connection->fetch($sql, [$id]);
return $row ? StoredOAuthToken::fromArray($row) : null;
}
/**
* Save or update token
*/
public function save(StoredOAuthToken $storedToken): StoredOAuthToken
{
if ($storedToken->id === null) {
return $this->insert($storedToken);
}
return $this->update($storedToken);
}
/**
* Save token for user (upsert by user_id + provider)
*/
public function saveForUser(
string $userId,
string $provider,
OAuthToken $token
): StoredOAuthToken {
$existing = $this->findForUser($userId, $provider);
if ($existing !== null) {
$updated = $existing->withRefreshedToken($token);
return $this->update($updated);
}
$storedToken = StoredOAuthToken::create($userId, $provider, $token);
return $this->insert($storedToken);
}
/**
* Delete token for user and provider
*/
public function deleteForUser(string $userId, string $provider): bool
{
$sql = "DELETE FROM " . self::TABLE . " WHERE user_id = ? AND provider = ?";
$this->connection->execute($sql, [$userId, $provider]);
return true;
}
/**
* Delete expired tokens without refresh token (cleanup job)
*/
public function deleteExpiredWithoutRefreshToken(): int
{
$sql = "DELETE FROM " . self::TABLE . "
WHERE expires_at < ? AND refresh_token IS NULL";
$this->connection->execute($sql, [time()]);
return $this->connection->rowCount();
}
/**
* Get all tokens for a user
*
* @return array<StoredOAuthToken>
*/
public function findAllForUser(string $userId): array
{
$sql = "SELECT * FROM " . self::TABLE . " WHERE user_id = ?";
$rows = $this->connection->fetchAll($sql, [$userId]);
return array_map(
fn (array $row) => StoredOAuthToken::fromArray($row),
$rows
);
}
/**
* Get all tokens expiring soon (for refresh worker)
*
* @return array<StoredOAuthToken>
*/
public function findExpiringSoon(int $withinSeconds = 300): array
{
$expiryThreshold = time() + $withinSeconds;
$sql = "SELECT * FROM " . self::TABLE . "
WHERE expires_at < ?
AND refresh_token IS NOT NULL";
$rows = $this->connection->fetchAll($sql, [$expiryThreshold]);
return array_map(
fn (array $row) => StoredOAuthToken::fromArray($row),
$rows
);
}
/**
* Insert new token
*/
private function insert(StoredOAuthToken $storedToken): StoredOAuthToken
{
$data = $storedToken->toArray();
unset($data['id']); // Don't insert ID
$sql = "INSERT INTO " . self::TABLE . "
(user_id, provider, access_token, refresh_token, token_type, scope, expires_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$this->connection->execute($sql, [
$data['user_id'],
$data['provider'],
$data['access_token'],
$data['refresh_token'],
$data['token_type'],
$data['scope'],
$data['expires_at'],
$data['created_at'],
$data['updated_at'],
]);
$id = (int) $this->connection->lastInsertId();
return new StoredOAuthToken(
id: $id,
userId: $storedToken->userId,
provider: $storedToken->provider,
token: $storedToken->token,
createdAt: $storedToken->createdAt,
updatedAt: $storedToken->updatedAt,
);
}
/**
* Update existing token
*/
private function update(StoredOAuthToken $storedToken): StoredOAuthToken
{
if ($storedToken->id === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
'Cannot update token without ID'
);
}
$data = $storedToken->toArray();
$sql = "UPDATE " . self::TABLE . "
SET access_token = ?,
refresh_token = ?,
token_type = ?,
scope = ?,
expires_at = ?,
updated_at = ?
WHERE id = ?";
$this->connection->execute($sql, [
$data['access_token'],
$data['refresh_token'],
$data['token_type'],
$data['scope'],
$data['expires_at'],
$data['updated_at'],
$storedToken->id,
]);
return $storedToken;
}
}