feat(extension): 采集插件功能开发
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Body, Controller, Get, Headers, Inject, Post, Query, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { z } from 'zod';
|
||||
import { collectedProductPayloadSchema } from '@atlas/contracts';
|
||||
import { ApiError } from '../common/api-error.js';
|
||||
import { Public } from '../common/public.decorator.js';
|
||||
import { ExtensionAccessGuard, type ExtensionAuthenticatedRequest } from './extension-auth.guard.js';
|
||||
import { ExtensionService } from './extension.service.js';
|
||||
|
||||
type RequestLike = { header: (name: string) => string | undefined };
|
||||
type ResponseLike = { redirect: (status: number, url: string) => void; status: (code: number) => ResponseLike };
|
||||
const authorizeSchema = z.object({ client_id: z.string().min(1).max(128), redirect_uri: z.url(), state: z.string().min(16).max(1_000), code_challenge: z.string().regex(/^[A-Za-z0-9_-]{43,128}$/), code_challenge_method: z.literal('S256') }).strict();
|
||||
const exchangeSchema = z.object({ client_id: z.string().min(1).max(128), code: z.string().min(32).max(1_000), redirect_uri: z.url(), code_verifier: z.string().min(43).max(128) }).strict();
|
||||
const refreshSchema = z.object({ refresh_token: z.string().min(32).max(1_000) }).strict();
|
||||
|
||||
@Controller('extension')
|
||||
export class ExtensionController {
|
||||
public constructor(@Inject(ExtensionService) private readonly extension: ExtensionService) {}
|
||||
@Get('authorize') @Public()
|
||||
public async authorize(@Query() query: unknown, @Req() request: RequestLike, @Res() response: ResponseLike) {
|
||||
const result = await this.extension.authorize(parse(authorizeSchema, query) as z.infer<typeof authorizeSchema>, cookie(request.header('cookie'), 'atlas_refresh'));
|
||||
const target = 'redirect_url' in result ? result.redirect_url : result.login_url;
|
||||
if (!target) throw new ApiError(500, 'EXTENSION_AUTHORIZATION_REDIRECT_MISSING', '扩展授权跳转地址缺失');
|
||||
response.redirect(302, target);
|
||||
}
|
||||
@Post('auth/exchange') @Public()
|
||||
public async exchange(@Body() body: unknown, @Res({ passthrough: true }) response: ResponseLike) { const result = await this.extension.exchange(parse(exchangeSchema, body) as z.infer<typeof exchangeSchema>); response.status(result.status); return result.body; }
|
||||
@Post('auth/refresh') @Public()
|
||||
public async refresh(@Body() body: unknown, @Res({ passthrough: true }) response: ResponseLike) { const input = parse(refreshSchema, body) as z.infer<typeof refreshSchema>; const result = await this.extension.refresh(input.refresh_token); response.status(result.status); return result.body; }
|
||||
@Post('auth/revoke') @Public() @UseGuards(ExtensionAccessGuard)
|
||||
public async revoke(@Req() request: ExtensionAuthenticatedRequest, @Res({ passthrough: true }) response: ResponseLike) { await this.extension.revoke(request.extensionAuth.workspaceId, request.extensionAuth.sessionId); response.status(204); }
|
||||
@Get('session') @Public() @UseGuards(ExtensionAccessGuard)
|
||||
public session(@Req() request: ExtensionAuthenticatedRequest) { return { data: { authenticated: true, workspace: { id: request.extensionAuth.workspaceId, available: true }, can_collect: true } }; }
|
||||
@Post('materials') @Public() @UseGuards(ExtensionAccessGuard)
|
||||
public async materials(@Req() request: ExtensionAuthenticatedRequest, @Headers('idempotency-key') key: string | undefined, @Body() body: unknown, @Res({ passthrough: true }) response: ResponseLike) {
|
||||
if (!key || !z.uuid().safeParse(key).success) throw new ApiError(400, 'IDEMPOTENCY_KEY_REQUIRED', '请求缺少或格式错误的 Idempotency-Key');
|
||||
const result = await this.extension.ingest(request.extensionAuth.workspaceId, request.extensionAuth.userId, key, parse(collectedProductPayloadSchema, body)); response.status(result.status); return result.body;
|
||||
}
|
||||
}
|
||||
function parse<T extends { safeParse: (value: unknown) => { success: boolean; data?: unknown; error?: { issues: Array<{ path: PropertyKey[]; message: string }> } } }>(schema: T, value: unknown) { const result = schema.safeParse(value); if (result.success) return result.data as never; throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', result.error?.issues.map((item) => ({ field: item.path.join('.'), code: 'INVALID_FIELD', message: item.message }))); }
|
||||
function cookie(header: string | undefined, name: string) { return header?.split(';').map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1); }
|
||||
Reference in New Issue
Block a user