docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace App\Framework\OAuth\Storage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\OAuth\ValueObjects\OAuthToken;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
/**
* OAuth Token Repository
*
* Handles persistence of OAuth tokens with automatic refresh handling
*/
final readonly class OAuthTokenRepository
{
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 (cleanup job)
*/
public function deleteExpired(): 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;
}
}