Files
registry/ts/api/router.ts
Juergen Kunz ab88ac896f feat: implement account settings and API tokens management
- Added SettingsComponent for user profile management, including display name and password change functionality.
- Introduced TokensComponent for managing API tokens, including creation and revocation.
- Created LayoutComponent for consistent application layout with navigation and user information.
- Established main application structure in index.html and main.ts.
- Integrated Tailwind CSS for styling and responsive design.
- Configured TypeScript settings for strict type checking and module resolution.
2025-11-27 22:15:38 +00:00

278 lines
9.5 KiB
TypeScript

/**
* API Router - Routes REST API requests to appropriate handlers
*/
import type { IStackGalleryActor } from '../providers/auth.provider.ts';
import { AuthService } from '../services/auth.service.ts';
import { TokenService } from '../services/token.service.ts';
import { PermissionService } from '../services/permission.service.ts';
import { AuditService } from '../services/audit.service.ts';
// Import API handlers
import { AuthApi } from './handlers/auth.api.ts';
import { UserApi } from './handlers/user.api.ts';
import { OrganizationApi } from './handlers/organization.api.ts';
import { RepositoryApi } from './handlers/repository.api.ts';
import { PackageApi } from './handlers/package.api.ts';
import { TokenApi } from './handlers/token.api.ts';
import { AuditApi } from './handlers/audit.api.ts';
export interface IApiContext {
request: Request;
url: URL;
path: string;
method: string;
params: Record<string, string>;
actor?: IStackGalleryActor;
ip?: string;
userAgent?: string;
}
export interface IApiResponse {
status: number;
body?: unknown;
headers?: Record<string, string>;
}
type RouteHandler = (ctx: IApiContext) => Promise<IApiResponse>;
interface IRoute {
method: string;
pattern: RegExp;
paramNames: string[];
handler: RouteHandler;
}
export class ApiRouter {
private routes: IRoute[] = [];
private authService: AuthService;
private tokenService: TokenService;
private permissionService: PermissionService;
// API handlers
private authApi: AuthApi;
private userApi: UserApi;
private organizationApi: OrganizationApi;
private repositoryApi: RepositoryApi;
private packageApi: PackageApi;
private tokenApi: TokenApi;
private auditApi: AuditApi;
constructor() {
this.authService = new AuthService();
this.tokenService = new TokenService();
this.permissionService = new PermissionService();
// Initialize API handlers
this.authApi = new AuthApi(this.authService);
this.userApi = new UserApi(this.permissionService);
this.organizationApi = new OrganizationApi(this.permissionService);
this.repositoryApi = new RepositoryApi(this.permissionService);
this.packageApi = new PackageApi(this.permissionService);
this.tokenApi = new TokenApi(this.tokenService);
this.auditApi = new AuditApi(this.permissionService);
this.registerRoutes();
}
/**
* Register all API routes
*/
private registerRoutes(): void {
// Auth routes
this.addRoute('POST', '/api/v1/auth/login', (ctx) => this.authApi.login(ctx));
this.addRoute('POST', '/api/v1/auth/refresh', (ctx) => this.authApi.refresh(ctx));
this.addRoute('POST', '/api/v1/auth/logout', (ctx) => this.authApi.logout(ctx));
this.addRoute('GET', '/api/v1/auth/me', (ctx) => this.authApi.me(ctx));
// User routes
this.addRoute('GET', '/api/v1/users', (ctx) => this.userApi.list(ctx));
this.addRoute('GET', '/api/v1/users/:id', (ctx) => this.userApi.get(ctx));
this.addRoute('POST', '/api/v1/users', (ctx) => this.userApi.create(ctx));
this.addRoute('PUT', '/api/v1/users/:id', (ctx) => this.userApi.update(ctx));
this.addRoute('DELETE', '/api/v1/users/:id', (ctx) => this.userApi.delete(ctx));
// Organization routes
this.addRoute('GET', '/api/v1/organizations', (ctx) => this.organizationApi.list(ctx));
this.addRoute('GET', '/api/v1/organizations/:id', (ctx) => this.organizationApi.get(ctx));
this.addRoute('POST', '/api/v1/organizations', (ctx) => this.organizationApi.create(ctx));
this.addRoute('PUT', '/api/v1/organizations/:id', (ctx) => this.organizationApi.update(ctx));
this.addRoute('DELETE', '/api/v1/organizations/:id', (ctx) => this.organizationApi.delete(ctx));
this.addRoute('GET', '/api/v1/organizations/:id/members', (ctx) => this.organizationApi.listMembers(ctx));
this.addRoute('POST', '/api/v1/organizations/:id/members', (ctx) => this.organizationApi.addMember(ctx));
this.addRoute('PUT', '/api/v1/organizations/:id/members/:userId', (ctx) => this.organizationApi.updateMember(ctx));
this.addRoute('DELETE', '/api/v1/organizations/:id/members/:userId', (ctx) => this.organizationApi.removeMember(ctx));
// Repository routes
this.addRoute('GET', '/api/v1/organizations/:orgId/repositories', (ctx) => this.repositoryApi.list(ctx));
this.addRoute('GET', '/api/v1/repositories/:id', (ctx) => this.repositoryApi.get(ctx));
this.addRoute('POST', '/api/v1/organizations/:orgId/repositories', (ctx) => this.repositoryApi.create(ctx));
this.addRoute('PUT', '/api/v1/repositories/:id', (ctx) => this.repositoryApi.update(ctx));
this.addRoute('DELETE', '/api/v1/repositories/:id', (ctx) => this.repositoryApi.delete(ctx));
// Package routes
this.addRoute('GET', '/api/v1/packages', (ctx) => this.packageApi.search(ctx));
this.addRoute('GET', '/api/v1/packages/:id', (ctx) => this.packageApi.get(ctx));
this.addRoute('GET', '/api/v1/packages/:id/versions', (ctx) => this.packageApi.listVersions(ctx));
this.addRoute('DELETE', '/api/v1/packages/:id', (ctx) => this.packageApi.delete(ctx));
this.addRoute('DELETE', '/api/v1/packages/:id/versions/:version', (ctx) => this.packageApi.deleteVersion(ctx));
// Token routes
this.addRoute('GET', '/api/v1/tokens', (ctx) => this.tokenApi.list(ctx));
this.addRoute('POST', '/api/v1/tokens', (ctx) => this.tokenApi.create(ctx));
this.addRoute('DELETE', '/api/v1/tokens/:id', (ctx) => this.tokenApi.revoke(ctx));
// Audit routes
this.addRoute('GET', '/api/v1/audit', (ctx) => this.auditApi.query(ctx));
}
/**
* Add a route
*/
private addRoute(method: string, path: string, handler: RouteHandler): void {
const paramNames: string[] = [];
const patternStr = path.replace(/:(\w+)/g, (_, name) => {
paramNames.push(name);
return '([^/]+)';
});
const pattern = new RegExp(`^${patternStr}$`);
this.routes.push({ method, pattern, paramNames, handler });
}
/**
* Handle an API request
*/
public async handle(request: Request): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
// Find matching route
for (const route of this.routes) {
if (route.method !== method) continue;
const match = path.match(route.pattern);
if (!match) continue;
// Extract params
const params: Record<string, string> = {};
route.paramNames.forEach((name, i) => {
params[name] = match[i + 1];
});
// Build context
const ctx: IApiContext = {
request,
url,
path,
method,
params,
ip: this.getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
};
// Authenticate request (except for login)
if (!path.includes('/auth/login')) {
ctx.actor = await this.authenticateRequest(request);
}
try {
const result = await route.handler(ctx);
return this.buildResponse(result);
} catch (error) {
console.error('[ApiRouter] Handler error:', error);
return this.buildResponse({
status: 500,
body: { error: 'Internal server error' },
});
}
}
return this.buildResponse({
status: 404,
body: { error: 'Not found' },
});
}
/**
* Authenticate request from headers
*/
private async authenticateRequest(request: Request): Promise<IStackGalleryActor | undefined> {
const authHeader = request.headers.get('authorization');
// Try Bearer token (JWT)
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
// Check if it's a JWT (for UI) or API token
if (token.startsWith('srg_')) {
// API token
const result = await this.tokenService.validateToken(token, this.getClientIp(request));
if (result.valid && result.token && result.user) {
return {
type: 'api_token',
userId: result.user.id,
user: result.user,
tokenId: result.token.id,
ip: this.getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
protocols: result.token.protocols,
permissions: {
canRead: true,
canWrite: true,
canDelete: true,
},
};
}
} else {
// JWT token
const result = await this.authService.validateAccessToken(token);
if (result) {
return {
type: 'user',
userId: result.user.id,
user: result.user,
ip: this.getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
protocols: ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'],
permissions: {
canRead: true,
canWrite: true,
canDelete: true,
},
};
}
}
}
return undefined;
}
/**
* Get client IP from request
*/
private getClientIp(request: Request): string {
return (
request.headers.get('x-forwarded-for')?.split(',')[0].trim() ||
request.headers.get('x-real-ip') ||
'unknown'
);
}
/**
* Build HTTP response from API response
*/
private buildResponse(result: IApiResponse): Response {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...result.headers,
};
return new Response(result.body ? JSON.stringify(result.body) : null, {
status: result.status,
headers,
});
}
}