- 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.
217 lines
5.8 KiB
PHP
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;
|
|
}
|
|
}
|