功能:完善用户与项目管理框架
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import { db } from "./db.mjs";
|
||||
import { requireCmsUser } from "./auth.mjs";
|
||||
import { json, readBody } from "./http.mjs";
|
||||
|
||||
const projectRoles = ["owner", "admin", "editor", "viewer"];
|
||||
const projectManagers = ["owner", "admin"];
|
||||
|
||||
function publicProject(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
githubOwner: row.github_owner,
|
||||
githubRepo: row.github_repo,
|
||||
githubBranch: row.github_branch,
|
||||
siteRoot: row.site_root,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
ownerUserId: row.owner_user_id,
|
||||
};
|
||||
}
|
||||
|
||||
function publicMember(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
userId: row.user_id,
|
||||
role: row.role,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
avatarUrl: row.avatar_url,
|
||||
status: row.status || "active",
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
function getProject(projectId) {
|
||||
return db.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
|
||||
}
|
||||
|
||||
function hasGlobalProjectAccess(user) {
|
||||
return user.role === "system_admin" || user.role === "admin";
|
||||
}
|
||||
|
||||
function getProjectMember(projectId, userId) {
|
||||
return db.prepare("SELECT * FROM project_members WHERE project_id = ? AND user_id = ?").get(projectId, userId);
|
||||
}
|
||||
|
||||
function canViewProject(user, projectId) {
|
||||
return hasGlobalProjectAccess(user) || Boolean(getProjectMember(projectId, user.id));
|
||||
}
|
||||
|
||||
function canManageProject(user, projectId) {
|
||||
if (hasGlobalProjectAccess(user)) return true;
|
||||
const member = getProjectMember(projectId, user.id);
|
||||
return Boolean(member && projectManagers.includes(member.role));
|
||||
}
|
||||
|
||||
function requireProjectView(request, response, projectId) {
|
||||
const user = requireCmsUser(request, response);
|
||||
if (!user) return undefined;
|
||||
|
||||
if (!canViewProject(user, projectId)) {
|
||||
json(response, 403, { error: "Permission denied" });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
function requireProjectManage(request, response, projectId) {
|
||||
const user = requireCmsUser(request, response);
|
||||
if (!user) return undefined;
|
||||
|
||||
if (!canManageProject(user, projectId)) {
|
||||
json(response, 403, { error: "Only project owners and admins can manage this project" });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
function listMembers(projectId) {
|
||||
return db
|
||||
.prepare(`
|
||||
SELECT project_members.*, users.name, users.email, users.avatar_url, users.status
|
||||
FROM project_members
|
||||
INNER JOIN users ON users.id = project_members.user_id
|
||||
WHERE project_members.project_id = ?
|
||||
ORDER BY
|
||||
CASE project_members.role
|
||||
WHEN 'owner' THEN 1
|
||||
WHEN 'admin' THEN 2
|
||||
WHEN 'editor' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
project_members.created_at ASC
|
||||
`)
|
||||
.all(projectId)
|
||||
.map(publicMember);
|
||||
}
|
||||
|
||||
function listProjects(user) {
|
||||
if (user.role === "system_admin" || user.role === "admin") {
|
||||
return db.prepare("SELECT * FROM projects ORDER BY updated_at DESC, id DESC").all().map(publicProject);
|
||||
}
|
||||
|
||||
return db
|
||||
.prepare(`
|
||||
SELECT projects.*
|
||||
FROM projects
|
||||
INNER JOIN project_members ON project_members.project_id = projects.id
|
||||
WHERE project_members.user_id = ?
|
||||
ORDER BY projects.updated_at DESC, projects.id DESC
|
||||
`)
|
||||
.all(user.id)
|
||||
.map(publicProject);
|
||||
}
|
||||
|
||||
async function handleListProjects(request, response) {
|
||||
const user = requireCmsUser(request, response);
|
||||
if (!user) return;
|
||||
|
||||
json(response, 200, { projects: listProjects(user) });
|
||||
}
|
||||
|
||||
async function handleCreateProject(request, response) {
|
||||
const user = requireCmsUser(request, response);
|
||||
if (!user) return;
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
const name = String(body.name || "").trim();
|
||||
|
||||
if (!name) {
|
||||
json(response, 400, { error: "Project name is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = db
|
||||
.prepare(`
|
||||
INSERT INTO projects (owner_user_id, name, github_owner, github_repo, github_branch, site_root)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
.run(
|
||||
user.id,
|
||||
name,
|
||||
String(body.githubOwner || "").trim() || null,
|
||||
String(body.githubRepo || "").trim() || null,
|
||||
String(body.githubBranch || "main").trim() || "main",
|
||||
String(body.siteRoot || "docs").trim().replace(/^\/+|\/+$/g, "") || "docs",
|
||||
);
|
||||
|
||||
db.prepare("INSERT OR IGNORE INTO project_members (project_id, user_id, role) VALUES (?, ?, 'owner')").run(
|
||||
result.lastInsertRowid,
|
||||
user.id,
|
||||
);
|
||||
|
||||
const project = db.prepare("SELECT * FROM projects WHERE id = ?").get(result.lastInsertRowid);
|
||||
json(response, 201, { project: publicProject(project) });
|
||||
}
|
||||
|
||||
async function handleUpdateProject(request, response, projectId) {
|
||||
const project = db.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
|
||||
if (!project) {
|
||||
json(response, 404, { error: "Project not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = requireProjectManage(request, response, projectId);
|
||||
if (!user) return;
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
db.prepare(`
|
||||
UPDATE projects
|
||||
SET name = ?,
|
||||
github_owner = ?,
|
||||
github_repo = ?,
|
||||
github_branch = ?,
|
||||
site_root = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
String(body.name || project.name).trim(),
|
||||
String(body.githubOwner ?? project.github_owner ?? "").trim() || null,
|
||||
String(body.githubRepo ?? project.github_repo ?? "").trim() || null,
|
||||
String(body.githubBranch || project.github_branch || "main").trim() || "main",
|
||||
String(body.siteRoot || project.site_root || "docs").trim().replace(/^\/+|\/+$/g, "") || "docs",
|
||||
projectId,
|
||||
);
|
||||
|
||||
json(response, 200, { project: publicProject(db.prepare("SELECT * FROM projects WHERE id = ?").get(projectId)) });
|
||||
}
|
||||
|
||||
async function handleListMembers(request, response, projectId) {
|
||||
if (!getProject(projectId)) {
|
||||
json(response, 404, { error: "Project not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = requireProjectView(request, response, projectId);
|
||||
if (!user) return;
|
||||
|
||||
json(response, 200, { members: listMembers(projectId) });
|
||||
}
|
||||
|
||||
async function handleAddMember(request, response, projectId) {
|
||||
if (!getProject(projectId)) {
|
||||
json(response, 404, { error: "Project not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = requireProjectManage(request, response, projectId);
|
||||
if (!user) return;
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
const email = String(body.email || "").trim().toLowerCase();
|
||||
const role = String(body.role || "editor");
|
||||
|
||||
if (!projectRoles.includes(role)) {
|
||||
json(response, 400, { error: "Invalid member role" });
|
||||
return;
|
||||
}
|
||||
|
||||
const target = db.prepare("SELECT * FROM users WHERE email = ?").get(email);
|
||||
if (!target) {
|
||||
json(response, 404, { error: "User not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO project_members (project_id, user_id, role)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(project_id, user_id) DO UPDATE SET role = excluded.role
|
||||
`).run(projectId, target.id, role);
|
||||
|
||||
json(response, 200, { members: listMembers(projectId) });
|
||||
}
|
||||
|
||||
async function handleUpdateMember(request, response, projectId, memberId) {
|
||||
const member = db.prepare("SELECT * FROM project_members WHERE project_id = ? AND id = ?").get(projectId, memberId);
|
||||
if (!member) {
|
||||
json(response, 404, { error: "Member not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = requireProjectManage(request, response, projectId);
|
||||
if (!user) return;
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
const role = String(body.role || member.role);
|
||||
|
||||
if (!projectRoles.includes(role)) {
|
||||
json(response, 400, { error: "Invalid member role" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.role === "owner" && role !== "owner") {
|
||||
const ownerCount = db
|
||||
.prepare("SELECT COUNT(*) AS count FROM project_members WHERE project_id = ? AND role = 'owner'")
|
||||
.get(projectId).count;
|
||||
|
||||
if (ownerCount <= 1) {
|
||||
json(response, 400, { error: "At least one project owner is required" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare("UPDATE project_members SET role = ? WHERE id = ?").run(role, memberId);
|
||||
json(response, 200, { members: listMembers(projectId) });
|
||||
}
|
||||
|
||||
async function handleRemoveMember(request, response, projectId, memberId) {
|
||||
const member = db.prepare("SELECT * FROM project_members WHERE project_id = ? AND id = ?").get(projectId, memberId);
|
||||
if (!member) {
|
||||
json(response, 404, { error: "Member not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = requireProjectManage(request, response, projectId);
|
||||
if (!user) return;
|
||||
|
||||
if (member.role === "owner") {
|
||||
const ownerCount = db
|
||||
.prepare("SELECT COUNT(*) AS count FROM project_members WHERE project_id = ? AND role = 'owner'")
|
||||
.get(projectId).count;
|
||||
|
||||
if (ownerCount <= 1) {
|
||||
json(response, 400, { error: "At least one project owner is required" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare("DELETE FROM project_members WHERE id = ?").run(memberId);
|
||||
json(response, 200, { members: listMembers(projectId) });
|
||||
}
|
||||
|
||||
export async function handleProjectsApi(request, response, url) {
|
||||
try {
|
||||
if (url.pathname === "/api/projects" && request.method === "GET") {
|
||||
await handleListProjects(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/projects" && request.method === "POST") {
|
||||
await handleCreateProject(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
const match = url.pathname.match(/^\/api\/projects\/(\d+)$/);
|
||||
if (match && request.method === "PATCH") {
|
||||
await handleUpdateProject(request, response, Number(match[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
const membersMatch = url.pathname.match(/^\/api\/projects\/(\d+)\/members$/);
|
||||
if (membersMatch && request.method === "GET") {
|
||||
await handleListMembers(request, response, Number(membersMatch[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (membersMatch && request.method === "POST") {
|
||||
await handleAddMember(request, response, Number(membersMatch[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
const memberMatch = url.pathname.match(/^\/api\/projects\/(\d+)\/members\/(\d+)$/);
|
||||
if (memberMatch && request.method === "PATCH") {
|
||||
await handleUpdateMember(request, response, Number(memberMatch[1]), Number(memberMatch[2]));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memberMatch && request.method === "DELETE") {
|
||||
await handleRemoveMember(request, response, Number(memberMatch[1]), Number(memberMatch[2]));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
json(response, 500, {
|
||||
error: error instanceof Error ? error.message : "Unknown projects API error",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user