feat: add full-stack API persistence and RBAC

This commit is contained in:
2026-07-01 06:28:18 -07:00
parent aafd9caaac
commit 914f74c5cc
18 changed files with 1148 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
import { jsonSuccess } from "@/modules/core/server/api";
import { requirePermission } from "@/modules/core/server/auth";
import { readData } from "@/modules/core/server/store";
export async function GET(): Promise<Response> {
const auth = await requirePermission("audit:read", {
action: "audit.list",
targetType: "auditLog",
});
if (!auth.success) {
return auth.response;
}
const data = await readData();
return jsonSuccess("审计日志已加载", {
auditLogs: data.auditLogs.slice(0, 100),
});
}

View File

@@ -0,0 +1,8 @@
import { getBootstrapState } from "@/modules/core/server/auth";
import { jsonSuccess } from "@/modules/core/server/api";
export async function GET(): Promise<Response> {
const state = await getBootstrapState();
return jsonSuccess("Bootstrap state loaded", state);
}

View File

@@ -0,0 +1,62 @@
import { createSessionRecord, normalizeEmail, setSessionCookie, toPublicAccount, verifyPassword } from "@/modules/core/server/auth";
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
import { updateData } from "@/modules/core/server/store";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readString(source: Record<string, unknown>, key: string): string {
const value = source[key];
return typeof value === "string" ? value.trim() : "";
}
export async function POST(request: Request): Promise<Response> {
const body = await readJsonBody(request);
if (!isRecord(body)) {
return jsonFailure("请求数据格式无效");
}
const email = normalizeEmail(readString(body, "email"));
const password = readString(body, "password");
if (!email || !password) {
return jsonFailure("邮箱和密码不能为空");
}
const result = await updateData((data) => {
const account = data.accounts.find((item) => item.email === email);
if (!account || account.status !== "active" || !verifyPassword(password, account)) {
return null;
}
const session = createSessionRecord(account.id);
data.sessions.push(session);
return { account, session };
});
if (!result) {
await recordAuditLog({
action: "auth.login",
targetType: "account",
result: "failure",
reason: `登录失败:${email}`,
});
return jsonFailure("账号或密码错误", 401);
}
await setSessionCookie(result.session.id);
const account = toPublicAccount(result.account);
await recordAuditLog({
actor: account,
action: "auth.login",
targetType: "account",
targetId: account.id,
result: "success",
reason: "登录成功",
});
return jsonSuccess("登录成功", { account });
}

View File

@@ -0,0 +1,20 @@
import { removeCurrentSession } from "@/modules/core/server/auth";
import { jsonSuccess } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
export async function POST(): Promise<Response> {
const account = await removeCurrentSession();
if (account) {
await recordAuditLog({
actor: account,
action: "auth.logout",
targetType: "session",
result: "success",
reason: "退出登录",
});
}
return jsonSuccess("已退出登录", {});
}

View File

@@ -0,0 +1,88 @@
import { createAccountRecord, createSessionRecord, normalizeEmail, setSessionCookie } from "@/modules/core/server/auth";
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
import { updateData } from "@/modules/core/server/store";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readString(source: Record<string, unknown>, key: string): string {
const value = source[key];
return typeof value === "string" ? value.trim() : "";
}
export async function POST(request: Request): Promise<Response> {
const body = await readJsonBody(request);
if (!isRecord(body)) {
return jsonFailure("请求数据格式无效");
}
const name = readString(body, "name");
const email = readString(body, "email");
const password = readString(body, "password");
if (!name || !email || !password) {
return jsonFailure("姓名、邮箱和密码不能为空");
}
if (password.length < 6) {
return jsonFailure("密码至少需要6位");
}
const normalizedEmail = normalizeEmail(email);
const created = await updateData((data) => {
const exists = data.accounts.some((account) => account.email === normalizedEmail);
if (exists) {
return null;
}
const account = createAccountRecord({
name,
email,
password,
role: "caregiver",
});
const session = createSessionRecord(account.id);
data.accounts.push(account);
data.sessions.push(session);
return { account, session };
});
if (!created) {
return jsonFailure("账号已存在", 409);
}
await setSessionCookie(created.session.id);
await recordAuditLog({
actor: {
id: created.account.id,
name: created.account.name,
email: created.account.email,
role: created.account.role,
status: created.account.status,
createdAt: created.account.createdAt,
updatedAt: created.account.updatedAt,
},
action: "account.register",
targetType: "account",
targetId: created.account.id,
result: "success",
reason: "创建照护人员账号",
});
return jsonSuccess("账号已创建", {
account: {
id: created.account.id,
name: created.account.name,
email: created.account.email,
role: created.account.role,
status: created.account.status,
createdAt: created.account.createdAt,
updatedAt: created.account.updatedAt,
},
});
}

View File

@@ -0,0 +1,12 @@
import { getCurrentAuthContext } from "@/modules/core/server/auth";
import { jsonSuccess } from "@/modules/core/server/api";
export async function GET(): Promise<Response> {
const context = await getCurrentAuthContext();
return jsonSuccess("Session loaded", {
account: context?.account ?? null,
permissions: context?.permissions ?? [],
});
}

View File

@@ -0,0 +1,90 @@
import { createAccountRecord, createSessionRecord, setSessionCookie } from "@/modules/core/server/auth";
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
import { updateData } from "@/modules/core/server/store";
function readString(source: Record<string, unknown>, key: string): string {
const value = source[key];
return typeof value === "string" ? value.trim() : "";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export async function POST(request: Request): Promise<Response> {
const body = await readJsonBody(request);
if (!isRecord(body)) {
return jsonFailure("请求数据格式无效");
}
const name = readString(body, "name");
const email = readString(body, "email");
const password = readString(body, "password");
const organization = readString(body, "organization");
if (!name || !email || !password || !organization) {
return jsonFailure("机构、姓名、邮箱和密码不能为空");
}
if (password.length < 6) {
return jsonFailure("密码至少需要6位");
}
const created = await updateData((data) => {
if (data.accounts.length > 0) {
return null;
}
const account = createAccountRecord({
name,
email,
password,
role: "admin",
organization,
});
const session = createSessionRecord(account.id);
data.accounts.push(account);
data.sessions.push(session);
return { account, session };
});
if (!created) {
return jsonFailure("系统已完成初始化", 409);
}
await setSessionCookie(created.session.id);
await recordAuditLog({
actor: {
id: created.account.id,
name: created.account.name,
email: created.account.email,
role: created.account.role,
organization: created.account.organization,
status: created.account.status,
createdAt: created.account.createdAt,
updatedAt: created.account.updatedAt,
},
action: "account.setup",
targetType: "account",
targetId: created.account.id,
result: "success",
reason: "初始化管理员账号",
});
return jsonSuccess("初始化完成", {
account: {
id: created.account.id,
name: created.account.name,
email: created.account.email,
role: created.account.role,
organization: created.account.organization,
status: created.account.status,
createdAt: created.account.createdAt,
updatedAt: created.account.updatedAt,
},
});
}

View File

@@ -0,0 +1,99 @@
import { validateElderInput } from "@/modules/elders/types";
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
import { requirePermission } from "@/modules/core/server/auth";
import { updateData } from "@/modules/core/server/store";
type RouteContext = {
params: Promise<{
id: string;
}>;
};
export async function PATCH(request: Request, context: RouteContext): Promise<Response> {
const { id } = await context.params;
const auth = await requirePermission("elder:update", {
action: "elder.update",
targetType: "elder",
targetId: id,
});
if (!auth.success) {
return auth.response;
}
const input = validateElderInput(await readJsonBody(request));
if (!input.success) {
return jsonFailure(input.reason);
}
const updated = await updateData((data) => {
const elder = data.elders.find((item) => item.id === id);
if (!elder) {
return null;
}
const nextElder = {
...elder,
...input.data,
updatedAt: new Date().toISOString(),
};
data.elders = data.elders.map((item) => (item.id === id ? nextElder : item));
return nextElder;
});
if (!updated) {
return jsonFailure("老人档案不存在", 404);
}
await recordAuditLog({
actor: auth.context.account,
action: "elder.update",
targetType: "elder",
targetId: updated.id,
result: "success",
reason: `更新老人档案:${updated.name}`,
});
return jsonSuccess("老人档案已更新", { elder: updated });
}
export async function DELETE(_request: Request, context: RouteContext): Promise<Response> {
const { id } = await context.params;
const auth = await requirePermission("elder:delete", {
action: "elder.delete",
targetType: "elder",
targetId: id,
});
if (!auth.success) {
return auth.response;
}
const deleted = await updateData((data) => {
const elder = data.elders.find((item) => item.id === id);
if (!elder) {
return null;
}
data.elders = data.elders.filter((item) => item.id !== id);
return elder;
});
if (!deleted) {
return jsonFailure("老人档案不存在", 404);
}
await recordAuditLog({
actor: auth.context.account,
action: "elder.delete",
targetType: "elder",
targetId: deleted.id,
result: "success",
reason: `删除老人档案:${deleted.name}`,
});
return jsonSuccess("老人档案已删除", { elder: deleted });
}

61
app/api/elders/route.ts Normal file
View File

@@ -0,0 +1,61 @@
import { randomUUID } from "node:crypto";
import type { Elder } from "@/modules/elders/types";
import { validateElderInput } from "@/modules/elders/types";
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
import { requirePermission } from "@/modules/core/server/auth";
import { readData, updateData } from "@/modules/core/server/store";
export async function GET(): Promise<Response> {
const auth = await requirePermission("elder:read", {
action: "elder.list",
targetType: "elder",
});
if (!auth.success) {
return auth.response;
}
const data = await readData();
return jsonSuccess("老人档案已加载", { elders: data.elders });
}
export async function POST(request: Request): Promise<Response> {
const auth = await requirePermission("elder:create", {
action: "elder.create",
targetType: "elder",
});
if (!auth.success) {
return auth.response;
}
const input = validateElderInput(await readJsonBody(request));
if (!input.success) {
return jsonFailure(input.reason);
}
const now = new Date().toISOString();
const elder: Elder = {
...input.data,
id: randomUUID(),
createdAt: now,
updatedAt: now,
};
await updateData((data) => {
data.elders = [elder, ...data.elders];
});
await recordAuditLog({
actor: auth.context.account,
action: "elder.create",
targetType: "elder",
targetId: elder.id,
result: "success",
reason: `新增老人档案:${elder.name}`,
});
return jsonSuccess("老人档案已创建", { elder }, 201);
}

View File

@@ -0,0 +1,20 @@
import { jsonSuccess } from "@/modules/core/server/api";
import { requirePermission, toPublicAccount } from "@/modules/core/server/auth";
import { readData } from "@/modules/core/server/store";
export async function GET(): Promise<Response> {
const auth = await requirePermission("account:read", {
action: "account.list",
targetType: "account",
});
if (!auth.success) {
return auth.response;
}
const data = await readData();
return jsonSuccess("账号列表已加载", {
accounts: data.accounts.map((account) => toPublicAccount(account)),
});
}

View File

@@ -0,0 +1,17 @@
import { jsonSuccess } from "@/modules/core/server/api";
import { requirePermission } from "@/modules/core/server/auth";
import { ROLE_DEFINITIONS } from "@/modules/core/server/permissions";
export async function GET(): Promise<Response> {
const auth = await requirePermission("account:read", {
action: "role.list",
targetType: "role",
});
if (!auth.success) {
return auth.response;
}
return jsonSuccess("角色列表已加载", { roles: ROLE_DEFINITIONS });
}