64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
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<boolean> {
|
|
if (
|
|
this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_ENDPOINT, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
])
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const request = context
|
|
.switchToHttp()
|
|
.getRequest<AuthenticatedRequest>();
|
|
// 当前一期每位用户只有一个个人工作空间;后续多人协作时此处将按显式选择的工作空间校验 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<Workspace | null> {
|
|
// 排序规则是首期“唯一有效工作空间”的临时选择策略,不是未来的工作空间切换协议。
|
|
const result = await this.database.query<Workspace>(
|
|
`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;
|
|
}
|
|
}
|