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.
This commit is contained in:
277
ts/api/router.ts
Normal file
277
ts/api/router.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user