feat(api): 开发平台、门店相关代码
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Workspace } from '@atlas/contracts';
|
||||
import type { WorkspaceAuthenticatedRequest } from './workspace-context.guard.js';
|
||||
|
||||
// 受保护接口经全局工作空间守卫验证后,只能从这里读取当前工作空间。
|
||||
export const CurrentWorkspace = createParamDecorator(
|
||||
(_data: unknown, context: ExecutionContext): Workspace => {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<WorkspaceAuthenticatedRequest>();
|
||||
return request.auth.workspace;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user