功能:完善用户与项目管理框架
This commit is contained in:
+179
-3
@@ -1,9 +1,10 @@
|
||||
import { db } from "./db.mjs";
|
||||
import { json, readBody } from "./http.mjs";
|
||||
import { clearSessionCookie, createSession, deleteSession, getSession, parseCookies, sessionCookie } from "./session.mjs";
|
||||
import { verifyPassword } from "./security.mjs";
|
||||
import { hashPassword, verifyPassword } from "./security.mjs";
|
||||
import { hasPermission, publicPermissions } from "./permissions.mjs";
|
||||
|
||||
function publicUser(user) {
|
||||
export function publicUser(user) {
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
@@ -12,6 +13,12 @@ function publicUser(user) {
|
||||
name: user.name,
|
||||
avatarUrl: user.avatar_url,
|
||||
role: user.role,
|
||||
status: user.status || "active",
|
||||
emailVerified: Boolean(user.email_verified),
|
||||
createdAt: user.created_at,
|
||||
updatedAt: user.updated_at,
|
||||
lastLoginAt: user.last_login_at,
|
||||
permissions: publicPermissions(user),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,12 +40,49 @@ export function requireCmsUser(request, response) {
|
||||
return session.user;
|
||||
}
|
||||
|
||||
export function requirePermission(request, response, permission) {
|
||||
const user = requireCmsUser(request, response);
|
||||
|
||||
if (!user) return undefined;
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
json(response, 403, { error: "Permission denied" });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
function settingEnabled(key, fallback = false) {
|
||||
const row = db.prepare("SELECT value FROM system_settings WHERE key = ?").get(key);
|
||||
return (row?.value ?? String(fallback)) === "true";
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
async function handleMe(request, response) {
|
||||
const { session } = getRequestSession(request);
|
||||
json(response, 200, { user: session?.user ?? null });
|
||||
}
|
||||
|
||||
async function handleOptions(request, response) {
|
||||
json(response, 200, {
|
||||
options: {
|
||||
allowEmailLogin: settingEnabled("auth.allow_email_login", true),
|
||||
allowGitHubLogin: settingEnabled("auth.allow_github_login", true),
|
||||
allowRegistration: settingEnabled("auth.allow_registration", false),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLogin(request, response) {
|
||||
if (!settingEnabled("auth.allow_email_login", true)) {
|
||||
json(response, 403, { error: "Email login is disabled" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = JSON.parse(await readBody(request) || "{}");
|
||||
const email = String(body.email || "").trim().toLowerCase();
|
||||
const password = String(body.password || "");
|
||||
@@ -55,7 +99,15 @@ async function handleLogin(request, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionUser = publicUser(user);
|
||||
if ((user.status || "active") !== "active") {
|
||||
json(response, 403, { error: "This account is disabled" });
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare("UPDATE users SET last_login_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(user.id);
|
||||
const nextUser = db.prepare("SELECT * FROM users WHERE id = ?").get(user.id);
|
||||
|
||||
const sessionUser = publicUser(nextUser);
|
||||
const sessionId = createSession({
|
||||
user: sessionUser,
|
||||
});
|
||||
@@ -63,12 +115,116 @@ async function handleLogin(request, response) {
|
||||
json(response, 200, { user: sessionUser }, { "Set-Cookie": sessionCookie(sessionId) });
|
||||
}
|
||||
|
||||
async function handleRegister(request, response) {
|
||||
if (!settingEnabled("auth.allow_registration", false)) {
|
||||
json(response, 403, { error: "Registration is disabled" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
const email = String(body.email || "").trim().toLowerCase();
|
||||
const name = String(body.name || "").trim();
|
||||
const password = String(body.password || "");
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
json(response, 400, { error: "A valid email is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
json(response, 400, { error: "Name is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
json(response, 400, { error: "Password must be at least 8 characters" });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
|
||||
if (existing) {
|
||||
json(response, 409, { error: "Email already exists" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = db
|
||||
.prepare("INSERT INTO users (email, password_hash, name, role, email_verified) VALUES (?, ?, ?, 'user', 0)")
|
||||
.run(email, hashPassword(password), name);
|
||||
|
||||
const user = publicUser(db.prepare("SELECT * FROM users WHERE id = ?").get(result.lastInsertRowid));
|
||||
const sessionId = createSession({ user });
|
||||
|
||||
json(response, 201, { user }, { "Set-Cookie": sessionCookie(sessionId) });
|
||||
}
|
||||
|
||||
async function handleLogout(request, response) {
|
||||
const { sessionId } = getRequestSession(request);
|
||||
deleteSession(sessionId);
|
||||
json(response, 200, { ok: true }, { "Set-Cookie": clearSessionCookie() });
|
||||
}
|
||||
|
||||
async function handleUpdateProfile(request, response) {
|
||||
const { sessionId, session } = getRequestSession(request);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
json(response, 401, { error: "Not signed in" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
const name = String(body.name || "").trim();
|
||||
|
||||
if (!name) {
|
||||
json(response, 400, { error: "Name is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare("UPDATE users SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(name, session.user.id);
|
||||
const user = db.prepare("SELECT * FROM users WHERE id = ?").get(session.user.id);
|
||||
const sessionUser = publicUser(user);
|
||||
|
||||
delete session.user;
|
||||
const nextSession = {
|
||||
...session,
|
||||
user: sessionUser,
|
||||
};
|
||||
deleteSession(sessionId);
|
||||
const nextSessionId = createSession(nextSession);
|
||||
|
||||
json(response, 200, { user: sessionUser }, { "Set-Cookie": sessionCookie(nextSessionId) });
|
||||
}
|
||||
|
||||
async function handleChangePassword(request, response) {
|
||||
const { session } = getRequestSession(request);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
json(response, 401, { error: "Not signed in" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = JSON.parse((await readBody(request)) || "{}");
|
||||
const currentPassword = String(body.currentPassword || "");
|
||||
const newPassword = String(body.newPassword || "");
|
||||
const user = db.prepare("SELECT * FROM users WHERE id = ?").get(session.user.id);
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
json(response, 400, { error: "New password must be at least 8 characters" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.password_hash && !verifyPassword(currentPassword, user.password_hash)) {
|
||||
json(response, 403, { error: "Current password is incorrect" });
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(
|
||||
hashPassword(newPassword),
|
||||
session.user.id,
|
||||
);
|
||||
|
||||
json(response, 200, { ok: true });
|
||||
}
|
||||
|
||||
export async function handleAuthApi(request, response, url) {
|
||||
try {
|
||||
if (url.pathname === "/api/auth/me" && request.method === "GET") {
|
||||
@@ -76,16 +232,36 @@ export async function handleAuthApi(request, response, url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/auth/options" && request.method === "GET") {
|
||||
await handleOptions(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/auth/login" && request.method === "POST") {
|
||||
await handleLogin(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/auth/register" && request.method === "POST") {
|
||||
await handleRegister(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/auth/logout" && request.method === "POST") {
|
||||
await handleLogout(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/auth/profile" && request.method === "PATCH") {
|
||||
await handleUpdateProfile(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/auth/password" && request.method === "POST") {
|
||||
await handleChangePassword(request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
json(response, 500, {
|
||||
|
||||
Reference in New Issue
Block a user