DevOS v1 + Phase 2: unified resource model, infra ops, website deployment, AI chat ops

- Monorepo: NestJS API (Fastify) + React web (antd/ProComponents) + shared packages
- PostgreSQL/Prisma (unified resources, relations graph, audit, events, jobs)
- Auth/RBAC/workspace isolation, credential vault (AES-256-GCM)
- Servers/SSH, Docker discovery & lifecycle, Job Center
- Websites & deployments (git provider credential clone, env, health verification)
- Domains/DNS (Cloudflare adapter), certificates (TLS probe), monitoring
- Automation (cron/event/webhook HMAC + notifications), AI Gateway, MCP (29 tools)
- AI chat window (LLM function-calling + rules fallback, SSE streaming, approvals)
- Notes: Vditor WYSIWYG markdown editor, non-blocking autosave
- Tests: 35 unit/security tests; UI audit clean (desktop+mobile, deep links)
This commit is contained in:
phptits
2026-09-10 06:35:55 +00:00
commit 0d6cd37579
209 changed files with 22783 additions and 0 deletions
@@ -0,0 +1,180 @@
/**
* # 身份认证服务(Task 005)
* 注册/登录/当前用户。令牌策略:HS256 JWT(12h),载荷携带激活工作区。
* 登出为无状态操作(客户端丢弃令牌);服务端记录审计。
*/
import { Injectable } from "@nestjs/common";
import { z } from "zod";
import { signJwt, hashPassword, verifyPassword } from "@devos/auth";
import { PrismaService } from "../../prisma/prisma.service";
import { ConfigService } from "../../common/config/config.service";
import { AuditService } from "../audit/audit.service";
import { ApiError } from "../../common/errors/api-error";
import { getRequestContext } from "../../common/request-context";
import { SecurityPolicyService } from "../../common/security/security-policy.service";
/** # 注册 DTO **/
export const RegisterSchema = z.object({
email: z.string().email().max(200),
password: z.string().min(8).max(128),
name: z.string().min(1).max(80).optional(),
});
export type RegisterDto = z.infer<typeof RegisterSchema>;
/** # 登录 DTO(账号支持用户名或邮箱) **/
export const LoginSchema = z.object({
email: z.string().min(1).max(200),
password: z.string().min(1).max(128),
});
export type LoginDto = z.infer<typeof LoginSchema>;
/** # JWT 有效期(秒) **/
const TOKEN_TTL_SEC = 12 * 3600;
@Injectable()
export class IdentityService {
constructor(
private readonly prisma: PrismaService,
private readonly config: ConfigService,
private readonly audit: AuditService,
private readonly security: SecurityPolicyService,
) {}
/** # 注册:创建用户 + 个人默认工作区(OWNER)+ 自动登录(受注册开关与限流约束) */
async register(dto: RegisterDto) {
const policy = this.security.checkRegistration();
if (!policy.allowed) {
throw ApiError.forbidden(policy.reason!);
}
const ip = getRequestContext()?.ip ?? "unknown";
if (!this.security.checkRate(`register:${ip}`)) {
throw new ApiError(429, "RATE_LIMITED", "Too many attempts, slow down");
}
const existing = await this.prisma.user.findUnique({ where: { email: dto.email } });
if (existing) throw ApiError.conflict("Email already registered");
const user = await this.prisma.$transaction(async (tx) => {
const created = await tx.user.create({
data: {
email: dto.email,
name: dto.name ?? dto.email.split("@")[0],
passwordHash: await hashPassword(dto.password),
},
});
const count = await tx.workspace.count();
const workspace = await tx.workspace.create({
data: {
name: `${created.name ?? created.email}'s Workspace`,
slug: count === 0 ? "main" : `ws-${created.id.slice(0, 8)}`,
},
});
await tx.workspaceMember.create({
data: { workspaceId: workspace.id, userId: created.id, role: "OWNER" },
});
return created;
});
await this.audit.record({
workspaceId: (await this.firstWorkspaceId(user.id)) ?? "",
actorType: "USER",
actorId: user.id,
action: "auth.register",
resourceType: "User",
resourceId: user.id,
});
return this.issueToken(user.id, user.email);
}
/** # 登录(按 IP 限流,防暴力破解) */
async login(dto: LoginDto) {
const ip = getRequestContext()?.ip ?? "unknown";
if (!this.security.checkRate(`login:${ip}`)) {
throw new ApiError(429, "RATE_LIMITED", "Too many attempts, slow down");
}
const user = await this.prisma.user.findUnique({ where: { email: dto.email } });
// 统一错误信息,避免枚举探测
if (!user || !(await verifyPassword(dto.password, user.passwordHash))) {
await this.audit.record({
workspaceId: (await this.firstWorkspaceId(user?.id ?? "")) ?? "",
actorType: "USER",
actorId: user?.id,
action: "auth.login",
status: "FAILURE",
metadata: { reason: "invalid credentials" },
});
throw ApiError.unauthorized("Invalid email or password");
}
const wid = await this.firstWorkspaceId(user.id);
await this.audit.record({
workspaceId: wid ?? "",
actorType: "USER",
actorId: user.id,
action: "auth.login",
resourceType: "User",
resourceId: user.id,
});
return this.issueToken(user.id, user.email, wid);
}
/** # 当前用户:身份 + 工作区列表 */
async me(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: {
memberships: {
include: { workspace: { select: { id: true, name: true, slug: true } } },
orderBy: { createdAt: "asc" },
},
},
});
if (!user) throw ApiError.unauthorized();
return {
id: user.id,
email: user.email,
name: user.name,
activeWorkspaceId: user.memberships[0]?.workspace.id ?? null,
workspaces: user.memberships.map((m) => ({
id: m.workspace.id,
name: m.workspace.name,
slug: m.workspace.slug,
role: m.role,
})),
};
}
/** # 登出(无状态;记录审计便于追溯) */
async logout(userId: string, workspaceId: string | null) {
await this.audit.record({
workspaceId: workspaceId ?? "",
actorType: "USER",
actorId: userId,
action: "auth.logout",
resourceType: "User",
resourceId: userId,
});
return { ok: true };
}
/** # 签发令牌并返回用户视图 */
private async issueToken(userId: string, email: string, wid?: string | null) {
const activeWorkspaceId = wid ?? (await this.firstWorkspaceId(userId));
const token = signJwt(
{ sub: userId, email, wid: activeWorkspaceId ?? null },
this.config.appSecret,
{ expiresInSec: TOKEN_TTL_SEC },
);
const me = await this.me(userId);
return { token, expiresIn: TOKEN_TTL_SEC, user: me, requestId: getRequestContext()?.requestId };
}
/** # 查询用户第一个工作区 ID */
private async firstWorkspaceId(userId: string): Promise<string | null> {
if (!userId) return null;
const m = await this.prisma.workspaceMember.findFirst({
where: { userId },
orderBy: { createdAt: "asc" },
select: { workspaceId: true },
});
return m?.workspaceId ?? null;
}
}