- 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.
278 lines
9.5 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|