import { CanActivate, ExecutionContext, Inject, Injectable, } from '@nestjs/common'; import type { Workspace } from '@atlas/contracts'; import type { Database } from '@atlas/database'; import { Reflector } from '@nestjs/core'; import { ApiError } from '../common/api-error.js'; import { IS_PUBLIC_ENDPOINT } from '../common/public.decorator.js'; import { DATABASE } from '../database/database.module.js'; import type { AuthenticatedRequest } from '../auth/auth.guard.js'; export type WorkspaceAuthenticatedRequest = AuthenticatedRequest & { auth: AuthenticatedRequest['auth'] & { workspace: Workspace }; }; @Injectable() export class WorkspaceContextGuard implements CanActivate { public constructor( @Inject(Reflector) private readonly reflector: Reflector, @Inject(DATABASE) private readonly database: Database, ) {} public async canActivate(context: ExecutionContext): Promise { if ( this.reflector.getAllAndOverride(IS_PUBLIC_ENDPOINT, [ context.getHandler(), context.getClass(), ]) ) { return true; } const request = context .switchToHttp() .getRequest(); // 当前一期每位用户只有一个个人工作空间;后续多人协作时此处将按显式选择的工作空间校验 membership。 const workspace = await this.findCurrentWorkspace(request.auth.userId); if (!workspace) { throw new ApiError( 403, 'WORKSPACE_MEMBERSHIP_REQUIRED', '当前账户没有可用工作空间', ); } // 后续控制器和服务只使用此处验证过的上下文,绝不相信客户端传入 workspace_id。 request.auth.workspace = workspace; return true; } private async findCurrentWorkspace( userId: string, ): Promise { // 排序规则是首期“唯一有效工作空间”的临时选择策略,不是未来的工作空间切换协议。 const result = await this.database.query( `SELECT w.id, w.name, m.role 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`, [userId], ); return result.rows[0] ?? null; } }