import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import type { Database } from '@atlas/database'; import { AuthService } from '../auth/auth.service.js'; import { ApiError } from '../common/api-error.js'; import { DATABASE } from '../database/database.module.js'; export type ExtensionAuthenticatedRequest = { header: (name: string) => string | undefined; extensionAuth: { userId: string; workspaceId: string; sessionId: string; clientId: string }; }; @Injectable() export class ExtensionAccessGuard implements CanActivate { public constructor(@Inject(AuthService) private readonly auth: AuthService, @Inject(DATABASE) private readonly database: Database) {} public async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); // 毛子 ERP 风格:插件携带从 Atlas Web 同步而来的短期 Web access token。 // AuthService 同时核对 JWT 和未撤销的 Web session,不能仅信任 token 声明。 let authenticated: { userId: string; sessionId: string }; try { authenticated = await this.auth.authenticate(request.header('authorization')); } catch (error) { // AuthService 的通用认证守卫会抛 Nest UnauthorizedException;扩展接口 // 应返回稳定、可操作的错误信息,不能在 UI 中退化成“请求无法处理”。 if (error instanceof UnauthorizedException) throw new ApiError(401, 'SESSION_EXPIRED', 'Atlas ERP 登录已过期,请重新登录'); throw error; } const workspace = await this.database.query<{ id: string }>(`SELECT w.id FROM atlas.workspace_memberships m JOIN atlas.workspaces w ON w.id = m.workspace_id WHERE m.user_id = $1 AND w.status = 'active' ORDER BY m.joined_at LIMIT 1`, [authenticated.userId]); if (!workspace.rows[0]) throw new ApiError(403, 'WORKSPACE_MEMBERSHIP_REQUIRED', '当前账户没有可用工作空间'); request.extensionAuth = { userId: authenticated.userId, workspaceId: workspace.rows[0].id, sessionId: authenticated.sessionId, clientId: 'atlas-web' }; return true; } }