diff --git a/core/lib/Routing/Router.php b/core/lib/Routing/Router.php index 7bdddfc..227943e 100644 --- a/core/lib/Routing/Router.php +++ b/core/lib/Routing/Router.php @@ -7,6 +7,7 @@ use KTXC\Http\Request\Request; use KTXC\Http\Response\Response; use KTXC\Injection\Container; use KTXC\Module\ModuleManager; +use KTXF\Routing\Attributes\AnonymousPrefixRoute; use KTXF\Routing\Attributes\AnonymousRoute; use KTXF\Routing\Attributes\AuthenticatedRoute; use Psr\Log\LoggerInterface; @@ -19,6 +20,8 @@ class Router private Container $container; /** @var array> */ private array $routes = []; // [method][path] => Route + /** @var array> prefix routes [method][prefix] => Route */ + private array $prefixRoutes = []; private bool $initialized = false; private string $cacheFile; @@ -40,8 +43,9 @@ class Router // load cached routes in production if ($this->environment === 'prod' && file_exists($this->cacheFile)) { $data = include $this->cacheFile; - if (is_array($data)) { - $this->routes = $data; + if (is_array($data) && isset($data['routes'])) { + $this->routes = $data['routes']; + $this->prefixRoutes = $data['prefix'] ?? []; $this->initialized = true; return; } @@ -52,7 +56,8 @@ class Router // write cache $dir = dirname($this->cacheFile); if (!is_dir($dir)) @mkdir($dir, 0775, true); - file_put_contents($this->cacheFile, 'routes, true) . ';'); + $cache = ['routes' => $this->routes, 'prefix' => $this->prefixRoutes]; + file_put_contents($this->cacheFile, 'getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { $attributes = array_merge( $reflectionMethod->getAttributes(AnonymousRoute::class), - $reflectionMethod->getAttributes(AuthenticatedRoute::class) + $reflectionMethod->getAttributes(AuthenticatedRoute::class), + $reflectionMethod->getAttributes(AnonymousPrefixRoute::class), ); foreach ($attributes as $attribute) { $route = $attribute->newInstance(); - $httpPath = $routePrefix . $route->path; + $isPrefix = $route instanceof AnonymousPrefixRoute; + $httpPath = ($isPrefix && $route->absolute) + ? $route->path + : $routePrefix . $route->path; foreach ($route->methods as $httpMethod) { - $this->routes[$httpMethod][$httpPath] = new Route( + $routeObject = new Route( method: $httpMethod, path: $httpPath, name: $route->name, @@ -111,6 +120,11 @@ class Router classMethodParameters: $reflectionMethod->getParameters(), permissions: $route instanceof AuthenticatedRoute ? $route->permissions : [], ); + if ($isPrefix) { + $this->prefixRoutes[$httpMethod][$httpPath] = $routeObject; + } else { + $this->routes[$httpMethod][$httpPath] = $routeObject; + } } } } @@ -123,7 +137,7 @@ class Router /** * Match a Request to a Route, or return null if no match. * Supports exact matches and simple {param} patterns. - * Prioritizes: 1) exact matches, 2) specific patterns, 3) catch-all patterns + * Prioritizes: 1) exact matches, 2) specific patterns, 3) prefix routes, 4) catch-all patterns */ public function match(Request $request): ?Route { @@ -161,6 +175,20 @@ class Router return $routeObj->withParams($params); } } + // Try prefix routes before catch-all — longest matching prefix wins + $bestPrefix = null; + $bestRoute = null; + foreach ($this->prefixRoutes[$method] ?? [] as $prefix => $routeObj) { + if (str_starts_with($path, $prefix) || str_starts_with($path . '/', $prefix)) { + if ($bestPrefix === null || strlen($prefix) > strlen($bestPrefix)) { + $bestPrefix = $prefix; + $bestRoute = $routeObj; + } + } + } + if ($bestRoute !== null) { + return $bestRoute; + } // Try catch-all pattern last if ($catchAllPattern !== null) { [$routePath, $routeObj] = $catchAllPattern; diff --git a/core/lib/Service/SecurityService.php b/core/lib/Service/SecurityService.php index beaba8d..f29b252 100644 --- a/core/lib/Service/SecurityService.php +++ b/core/lib/Service/SecurityService.php @@ -142,4 +142,19 @@ class SecurityService { return $this->tokenService->validateToken($token, $this->securityCode, checkBlacklist: false); } + + /** + * Authenticate using a plain-text identity + password credential. + * + * Used by the DAV auth backend which receives credentials from the HTTP + * Basic challenge after they have been extracted from the Authorization header. + * + * @param string $identity Username / e-mail + * @param string $password Plain-text password + * @return User|null Authenticated user, or null on failure + */ + public function authenticatePassword(string $identity, string $password): ?User + { + return $this->authenticateBasic($identity, $password); + } } diff --git a/core/lib/SessionIdentity.php b/core/lib/SessionIdentity.php index 63b7374..cbd7979 100644 --- a/core/lib/SessionIdentity.php +++ b/core/lib/SessionIdentity.php @@ -36,17 +36,17 @@ class SessionIdentity public function mailAddress(): ?string { - return $this->identityData?->getEmail(); + return $this->identityData?->getIdentity(); } public function nameFirst(): ?string { - return $this->identityData?->getFirstName(); + return null; } public function nameLast(): ?string { - return $this->identityData?->getLastName(); + return null; } public function permissions(): array diff --git a/shared/lib/Routing/Attributes/AnonymousPrefixRoute.php b/shared/lib/Routing/Attributes/AnonymousPrefixRoute.php new file mode 100644 index 0000000..fd63d32 --- /dev/null +++ b/shared/lib/Routing/Attributes/AnonymousPrefixRoute.php @@ -0,0 +1,26 @@ +