feat: add prefixed route
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use KTXC\Http\Request\Request;
|
|||||||
use KTXC\Http\Response\Response;
|
use KTXC\Http\Response\Response;
|
||||||
use KTXC\Injection\Container;
|
use KTXC\Injection\Container;
|
||||||
use KTXC\Module\ModuleManager;
|
use KTXC\Module\ModuleManager;
|
||||||
|
use KTXF\Routing\Attributes\AnonymousPrefixRoute;
|
||||||
use KTXF\Routing\Attributes\AnonymousRoute;
|
use KTXF\Routing\Attributes\AnonymousRoute;
|
||||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -19,6 +20,8 @@ class Router
|
|||||||
private Container $container;
|
private Container $container;
|
||||||
/** @var array<string,array<string,Route>> */
|
/** @var array<string,array<string,Route>> */
|
||||||
private array $routes = []; // [method][path] => Route
|
private array $routes = []; // [method][path] => Route
|
||||||
|
/** @var array<string,array<string,Route>> prefix routes [method][prefix] => Route */
|
||||||
|
private array $prefixRoutes = [];
|
||||||
private bool $initialized = false;
|
private bool $initialized = false;
|
||||||
private string $cacheFile;
|
private string $cacheFile;
|
||||||
|
|
||||||
@@ -40,8 +43,9 @@ class Router
|
|||||||
// load cached routes in production
|
// load cached routes in production
|
||||||
if ($this->environment === 'prod' && file_exists($this->cacheFile)) {
|
if ($this->environment === 'prod' && file_exists($this->cacheFile)) {
|
||||||
$data = include $this->cacheFile;
|
$data = include $this->cacheFile;
|
||||||
if (is_array($data)) {
|
if (is_array($data) && isset($data['routes'])) {
|
||||||
$this->routes = $data;
|
$this->routes = $data['routes'];
|
||||||
|
$this->prefixRoutes = $data['prefix'] ?? [];
|
||||||
$this->initialized = true;
|
$this->initialized = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,7 +56,8 @@ class Router
|
|||||||
// write cache
|
// write cache
|
||||||
$dir = dirname($this->cacheFile);
|
$dir = dirname($this->cacheFile);
|
||||||
if (!is_dir($dir)) @mkdir($dir, 0775, true);
|
if (!is_dir($dir)) @mkdir($dir, 0775, true);
|
||||||
file_put_contents($this->cacheFile, '<?php return ' . var_export($this->routes, true) . ';');
|
$cache = ['routes' => $this->routes, 'prefix' => $this->prefixRoutes];
|
||||||
|
file_put_contents($this->cacheFile, '<?php return ' . var_export($cache, true) . ';');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -95,13 +100,17 @@ class Router
|
|||||||
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
|
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
|
||||||
$attributes = array_merge(
|
$attributes = array_merge(
|
||||||
$reflectionMethod->getAttributes(AnonymousRoute::class),
|
$reflectionMethod->getAttributes(AnonymousRoute::class),
|
||||||
$reflectionMethod->getAttributes(AuthenticatedRoute::class)
|
$reflectionMethod->getAttributes(AuthenticatedRoute::class),
|
||||||
|
$reflectionMethod->getAttributes(AnonymousPrefixRoute::class),
|
||||||
);
|
);
|
||||||
foreach ($attributes as $attribute) {
|
foreach ($attributes as $attribute) {
|
||||||
$route = $attribute->newInstance();
|
$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) {
|
foreach ($route->methods as $httpMethod) {
|
||||||
$this->routes[$httpMethod][$httpPath] = new Route(
|
$routeObject = new Route(
|
||||||
method: $httpMethod,
|
method: $httpMethod,
|
||||||
path: $httpPath,
|
path: $httpPath,
|
||||||
name: $route->name,
|
name: $route->name,
|
||||||
@@ -111,6 +120,11 @@ class Router
|
|||||||
classMethodParameters: $reflectionMethod->getParameters(),
|
classMethodParameters: $reflectionMethod->getParameters(),
|
||||||
permissions: $route instanceof AuthenticatedRoute ? $route->permissions : [],
|
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.
|
* Match a Request to a Route, or return null if no match.
|
||||||
* Supports exact matches and simple {param} patterns.
|
* 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
|
public function match(Request $request): ?Route
|
||||||
{
|
{
|
||||||
@@ -161,6 +175,20 @@ class Router
|
|||||||
return $routeObj->withParams($params);
|
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
|
// Try catch-all pattern last
|
||||||
if ($catchAllPattern !== null) {
|
if ($catchAllPattern !== null) {
|
||||||
[$routePath, $routeObj] = $catchAllPattern;
|
[$routePath, $routeObj] = $catchAllPattern;
|
||||||
|
|||||||
@@ -142,4 +142,19 @@ class SecurityService
|
|||||||
{
|
{
|
||||||
return $this->tokenService->validateToken($token, $this->securityCode, checkBlacklist: false);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,17 +36,17 @@ class SessionIdentity
|
|||||||
|
|
||||||
public function mailAddress(): ?string
|
public function mailAddress(): ?string
|
||||||
{
|
{
|
||||||
return $this->identityData?->getEmail();
|
return $this->identityData?->getIdentity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nameFirst(): ?string
|
public function nameFirst(): ?string
|
||||||
{
|
{
|
||||||
return $this->identityData?->getFirstName();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nameLast(): ?string
|
public function nameLast(): ?string
|
||||||
{
|
{
|
||||||
return $this->identityData?->getLastName();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function permissions(): array
|
public function permissions(): array
|
||||||
|
|||||||
26
shared/lib/Routing/Attributes/AnonymousPrefixRoute.php
Normal file
26
shared/lib/Routing/Attributes/AnonymousPrefixRoute.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTXF\Routing\Attributes;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a controller method as a prefix-matched anonymous route.
|
||||||
|
*
|
||||||
|
* Unlike AnonymousRoute (exact match), this attribute matches any request
|
||||||
|
* whose path *starts with* the given $path prefix, making it suitable for
|
||||||
|
* mounting sub-frameworks (e.g. sabre/dav) under a well-known URL tree.
|
||||||
|
*
|
||||||
|
* When $absolute is true the module prefix (/m/{handle}) is NOT prepended,
|
||||||
|
* so the route is registered exactly at $path from the root.
|
||||||
|
*/
|
||||||
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
|
||||||
|
class AnonymousPrefixRoute
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $path,
|
||||||
|
public readonly string $name,
|
||||||
|
public readonly array $methods = ['GET'],
|
||||||
|
public readonly bool $absolute = false,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user