- auth / Mac app rebuild

This commit is contained in:
Ty Clifford
2026-07-05 16:23:17 -04:00
parent 729b29276f
commit ddaefbcc27
20 changed files with 1017 additions and 1253 deletions
+137 -14
View File
@@ -9,6 +9,8 @@ $repo = $app->repository();
$comments = $app->comments();
$config = $app->config();
tcms_load_env(BLOG_ROOT . '/.env');
header('Content-Type: application/json; charset=utf-8');
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') {
@@ -25,7 +27,9 @@ try {
'name' => 'Ty Clifford Content Management System API',
'site' => $config->get('site', []),
'authenticated' => tcms_is_authenticated($config),
'token_configured' => tcms_configured_token($config) !== '',
'auth_required' => true,
'credentials_configured' => tcms_credentials_configured(),
'token_configured' => tcms_configured_token() !== '',
'capabilities' => [
'content' => ['list', 'get', 'save', 'delete'],
'media' => ['list', 'upload'],
@@ -34,7 +38,7 @@ try {
]);
}
if (!$config->get('api.enabled', true)) {
if (!tcms_api_enabled($config)) {
tcms_error('The API is disabled for this TCMS site.', 403);
}
tcms_require_auth($config);
@@ -67,6 +71,39 @@ function tcms_payload(): array
return $_POST;
}
function tcms_load_env(string $path): void
{
if (!is_file($path)) {
return;
}
foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if ($key === '' || !preg_match('/^[A-Z0-9_]+$/', $key)) {
continue;
}
if (
(str_starts_with($value, '"') && str_ends_with($value, '"')) ||
(str_starts_with($value, "'") && str_ends_with($value, "'"))
) {
$value = substr($value, 1, -1);
}
if (getenv($key) === false) {
putenv($key . '=' . $value);
}
$_ENV[$key] = $value;
$_SERVER[$key] ??= $value;
}
}
/** @param array<string, mixed> $data */
function tcms_json(array $data, int $status = 200): never
{
@@ -83,26 +120,79 @@ function tcms_error(string $message, int $status = 400, array $extra = []): neve
function tcms_require_auth($config): void
{
if (!tcms_credentials_configured()) {
tcms_error('API credentials are not configured. Create a site .env file with TCMS_API_USERNAME and TCMS_API_PASSWORD, TCMS_API_PASSWORD_HASH, or TCMS_API_TOKEN.', 503);
}
if (!tcms_is_authenticated($config)) {
tcms_error('API authentication failed. Send the configured token as a Bearer token or X-TCMS-Token header.', 401);
tcms_error('API authentication failed. Use HTTP Basic credentials from .env or send TCMS_API_TOKEN as a Bearer token or X-TCMS-Token header.', 401);
}
}
function tcms_is_authenticated($config): bool
{
$configured = tcms_configured_token($config);
if ($configured === '') {
return (bool) $config->get('api.allow_local_without_token', true) && tcms_is_local_request();
if (!tcms_credentials_configured()) {
return false;
}
$provided = tcms_request_token();
return $provided !== '' && hash_equals($configured, $provided);
$configuredToken = tcms_configured_token();
$providedToken = tcms_request_token();
if ($configuredToken !== '' && $providedToken !== '' && hash_equals($configuredToken, $providedToken)) {
return true;
}
$basic = tcms_request_basic_credentials();
if ($basic === null) {
return false;
}
[$username, $password] = $basic;
return tcms_username_matches($username) && tcms_password_matches($password);
}
function tcms_configured_token($config): string
function tcms_api_enabled($config): bool
{
$envToken = trim((string) getenv('TCMS_API_TOKEN'));
return $envToken !== '' ? $envToken : trim((string) $config->get('api.token', ''));
$envValue = tcms_env('TCMS_API_ENABLED');
if ($envValue !== '') {
return filter_var($envValue, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true;
}
return (bool) $config->get('api.enabled', true);
}
function tcms_credentials_configured(): bool
{
return tcms_configured_token() !== ''
|| (tcms_configured_username() !== '' && (tcms_configured_password() !== '' || tcms_configured_password_hash() !== ''));
}
function tcms_configured_token(): string
{
return tcms_env('TCMS_API_TOKEN');
}
function tcms_configured_username(): string
{
return tcms_env('TCMS_API_USERNAME');
}
function tcms_configured_password(): string
{
return tcms_env('TCMS_API_PASSWORD');
}
function tcms_configured_password_hash(): string
{
return tcms_env('TCMS_API_PASSWORD_HASH');
}
function tcms_env(string $key): string
{
$value = getenv($key);
if ($value === false) {
$value = $_ENV[$key] ?? $_SERVER[$key] ?? '';
}
return trim((string) $value);
}
function tcms_request_token(): string
@@ -116,10 +206,43 @@ function tcms_request_token(): string
return trim((string) ($_SERVER['HTTP_X_TCMS_TOKEN'] ?? $headers['X-TCMS-Token'] ?? $headers['x-tcms-token'] ?? ''));
}
function tcms_is_local_request(): bool
/** @return array{0: string, 1: string}|null */
function tcms_request_basic_credentials(): ?array
{
$remote = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
return in_array($remote, ['127.0.0.1', '::1', 'localhost', ''], true);
if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
return [(string) $_SERVER['PHP_AUTH_USER'], (string) $_SERVER['PHP_AUTH_PW']];
}
$headers = function_exists('getallheaders') ? getallheaders() : [];
$auth = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? $headers['Authorization'] ?? $headers['authorization'] ?? '');
if (!preg_match('/^Basic\s+(.+)$/i', $auth, $matches)) {
return null;
}
$decoded = base64_decode(trim($matches[1]), true);
if (!is_string($decoded) || !str_contains($decoded, ':')) {
return null;
}
[$username, $password] = explode(':', $decoded, 2);
return [$username, $password];
}
function tcms_username_matches(string $username): bool
{
$configured = tcms_configured_username();
return $configured !== '' && hash_equals($configured, $username);
}
function tcms_password_matches(string $password): bool
{
$hash = tcms_configured_password_hash();
if ($hash !== '') {
return password_verify($password, $hash);
}
$configured = tcms_configured_password();
return $configured !== '' && hash_equals($configured, $password);
}
function tcms_content_list(ContentRepository $repo): never