Files
breadcrumb-the-shire/lib/Http/ApiAuth.php
fs 25370a1a55 add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00

182 lines
5.2 KiB
PHP

<?php
namespace MintyPHP\Http;
use MintyPHP\Repository\Access\RolePermissionRepository;
use MintyPHP\Repository\Access\UserRoleRepository;
use MintyPHP\Service\Auth\ApiTokenService;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Service\User\UserService;
class ApiAuth
{
private static ?array $currentUser = null;
private static ?array $currentPermissions = null;
private static ?int $currentTenantId = null;
private static ?int $tokenTenantId = null;
private static ?array $currentTokenRecord = null;
private static bool $authenticated = false;
/**
* Extract the full Bearer token from the Authorization header.
*/
public static function extractBearerToken(): string
{
$header = trim((string) ($_SERVER['HTTP_AUTHORIZATION'] ?? ''));
if ($header === '') {
$header = trim((string) ($_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''));
}
if ($header === '' || stripos($header, 'Bearer ') !== 0) {
return '';
}
return trim(substr($header, 7));
}
/**
* Attempt to authenticate from Authorization: Bearer header.
*/
public static function authenticate(): bool
{
self::$currentUser = null;
self::$currentPermissions = null;
self::$currentTenantId = null;
self::$tokenTenantId = null;
self::$currentTokenRecord = null;
self::$authenticated = false;
$bearerToken = self::extractBearerToken();
if ($bearerToken === '') {
return false;
}
$result = ApiTokenService::validate($bearerToken);
if ($result === null) {
return false;
}
$user = $result['user'];
$userId = (int) ($user['id'] ?? 0);
// Load permissions directly (bypass session cache)
$roleIds = UserRoleRepository::listRoleIdsByUserId($userId);
$permissions = RolePermissionRepository::listPermissionKeysByRoleIds($roleIds);
// Resolve tenant context
$tokenTenantId = $result['tenant_id'];
if ($tokenTenantId !== null) {
$userTenantIds = TenantScopeService::getUserTenantIds($userId);
if (!in_array($tokenTenantId, $userTenantIds, true)) {
return false;
}
$currentTenantId = $tokenTenantId;
self::$tokenTenantId = $tokenTenantId;
} else {
$currentTenantId = UserService::getCurrentTenantId($userId);
self::$tokenTenantId = null;
}
self::$currentUser = $user;
self::$currentPermissions = $permissions;
self::$currentTenantId = $currentTenantId;
self::$currentTokenRecord = $result['token_record'];
self::$authenticated = true;
return true;
}
public static function isAuthenticated(): bool
{
return self::$authenticated;
}
public static function user(): ?array
{
return self::$currentUser;
}
public static function userId(): int
{
return (int) (self::$currentUser['id'] ?? 0);
}
public static function permissions(): array
{
return self::$currentPermissions ?? [];
}
public static function hasPermission(string $key): bool
{
return in_array($key, self::$currentPermissions ?? [], true);
}
public static function tenantId(): ?int
{
return self::$currentTenantId;
}
public static function scopedTenantId(): ?int
{
return self::$tokenTenantId;
}
public static function tokenRecord(): ?array
{
return self::$currentTokenRecord;
}
public static function tokenId(): ?int
{
$id = (int) (self::$currentTokenRecord['id'] ?? 0);
return $id > 0 ? $id : null;
}
public static function isTenantScopedToken(): bool
{
return (self::$tokenTenantId ?? 0) > 0;
}
/**
* Require both tenant-scope and token-tenant access for a resource.
*/
public static function requireResourceAccess(string $resource, int $resourceId): void
{
if (!TenantScopeService::canAccess($resource, $resourceId, self::userId())) {
ApiResponse::notFound();
}
self::requireTokenTenantAccess($resource, $resourceId);
}
public static function requireTokenTenantAccess(string $resource, int $resourceId): void
{
if (!self::isTenantScopedToken()) {
return;
}
$tenantId = (int) (self::$tokenTenantId ?? 0);
if ($tenantId <= 0) {
ApiResponse::forbidden();
}
if (!TenantScopeService::resourceBelongsToTenant($resource, $resourceId, $tenantId)) {
ApiResponse::notFound();
}
}
/**
* Check if the current user can self-manage API tokens.
*/
public static function canSelfManageTokens(): bool
{
return self::hasPermission(\MintyPHP\Service\Access\PermissionService::USERS_SELF_UPDATE)
|| self::hasPermission(\MintyPHP\Service\Access\PermissionService::API_TOKENS_MANAGE);
}
public static function requireSelfManageTokens(): void
{
if (!self::canSelfManageTokens()) {
ApiResponse::forbidden('api_tokens_self_manage_forbidden');
}
}
}