Add stage 1: authentication REST API
Slim 4 + SQLite todo-list API providing email/password registration, login, and an authenticated GET /me endpoint. Stateless HS256 JWTs, bcrypt password hashing, uniform JSON error envelope, and a SQL migration runner. Includes PHPUnit feature tests and stage-1 docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Immutable application configuration, resolved from environment variables with
|
||||
* development-friendly defaults.
|
||||
*/
|
||||
final class Config
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $databasePath,
|
||||
public readonly string $jwtSecret,
|
||||
public readonly int $jwtTtl,
|
||||
public readonly bool $displayErrors,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function load(string $basePath): self
|
||||
{
|
||||
if (is_file($basePath . '/.env')) {
|
||||
\Dotenv\Dotenv::createImmutable($basePath)->safeLoad();
|
||||
}
|
||||
|
||||
$storagePath = $basePath . '/storage';
|
||||
if (!is_dir($storagePath)) {
|
||||
mkdir($storagePath, 0775, true);
|
||||
}
|
||||
|
||||
$databasePath = self::env('DATABASE_PATH', $storagePath . '/database.sqlite');
|
||||
if (!self::isAbsolutePath($databasePath)) {
|
||||
$databasePath = $basePath . '/' . ltrim($databasePath, '/');
|
||||
}
|
||||
|
||||
$jwtSecret = self::env('JWT_SECRET') ?? self::resolveSecret($storagePath . '/secret.key');
|
||||
$jwtTtl = (int) (self::env('JWT_TTL') ?? '86400');
|
||||
$displayErrors = filter_var(self::env('APP_DEBUG', 'false'), FILTER_VALIDATE_BOOL);
|
||||
|
||||
return new self($databasePath, $jwtSecret, $jwtTtl, $displayErrors);
|
||||
}
|
||||
|
||||
private static function env(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
|
||||
|
||||
if ($value === false || $value === null || $value === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private static function isAbsolutePath(string $path): bool
|
||||
{
|
||||
return str_starts_with($path, '/') || preg_match('#^[A-Za-z]:[\\\\/]#', $path) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the persisted signing secret, generating and storing one on first run
|
||||
* so local development works with zero configuration.
|
||||
*/
|
||||
private static function resolveSecret(string $path): string
|
||||
{
|
||||
if (is_file($path)) {
|
||||
return trim((string) file_get_contents($path));
|
||||
}
|
||||
|
||||
$secret = bin2hex(random_bytes(32));
|
||||
file_put_contents($path, $secret);
|
||||
@chmod($path, 0600);
|
||||
|
||||
return $secret;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user