From c1eb759436c5c15522c44275b4d15e452b91a1f9 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 28 Mar 2026 12:31:02 +0800 Subject: [PATCH] Feat: Add Change Password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加修改密码功能 --- packages/client/src/App.tsx | 2 + packages/client/src/components/Layout.tsx | 27 +++- packages/client/src/pages/ProjectDetail.tsx | 5 +- packages/client/src/pages/Settings.tsx | 159 ++++++++++++++++++++ packages/client/tsconfig.tsbuildinfo | 2 +- packages/server/src/routes/auth.js | 30 ++++ 6 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 packages/client/src/pages/Settings.tsx diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index b75af04..f16143a 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -4,6 +4,7 @@ import Projects from "./pages/Projects"; import ProjectDetail from "./pages/ProjectDetail"; import BuildDetail from "./pages/BuildDetail"; import Login from "./pages/Login"; +import Settings from "./pages/Settings"; import Layout from "./components/Layout"; function AuthGuard() { @@ -33,6 +34,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/packages/client/src/components/Layout.tsx b/packages/client/src/components/Layout.tsx index 5e007a2..722fa30 100644 --- a/packages/client/src/components/Layout.tsx +++ b/packages/client/src/components/Layout.tsx @@ -1,5 +1,5 @@ import { Outlet, NavLink, useNavigate } from "react-router-dom"; -import { GitBranch, LogOut } from "lucide-react"; +import { GitBranch, LogOut, Settings } from "lucide-react"; export default function Layout() { const navigate = useNavigate(); @@ -18,13 +18,24 @@ export default function Layout() { Paul CI/CD - +
+ + `flex items-center gap-1.5 text-xs transition-colors ${isActive ? "text-gray-200" : "text-gray-400 hover:text-gray-200"}` + } + > + + Settings + + +
diff --git a/packages/client/src/pages/ProjectDetail.tsx b/packages/client/src/pages/ProjectDetail.tsx index 19e3a79..f608dbb 100644 --- a/packages/client/src/pages/ProjectDetail.tsx +++ b/packages/client/src/pages/ProjectDetail.tsx @@ -97,8 +97,9 @@ export default function ProjectDetail() { const res = await fetch(`/api/projects/${id}/trigger`, { method: "POST" }); const data = await res.json(); setTriggering(false); - if (data.buildId) navigate(`/builds/${data.buildId}`); - load(); + if (data.buildId) { + navigate(`/builds/${data.buildId}`); + } }; const webhookUrl = `${window.location.origin}/api/webhook/${id}`; diff --git a/packages/client/src/pages/Settings.tsx b/packages/client/src/pages/Settings.tsx new file mode 100644 index 0000000..dd0f3cc --- /dev/null +++ b/packages/client/src/pages/Settings.tsx @@ -0,0 +1,159 @@ +import { useState, useEffect } from "react"; +import { KeyRound, UserPen } from "lucide-react"; + +export default function Settings() { + const [username, setUsername] = useState(""); + const [newUsername, setNewUsername] = useState(""); + const [usernameError, setUsernameError] = useState(""); + const [usernameSuccess, setUsernameSuccess] = useState(false); + const [usernameLoading, setUsernameLoading] = useState(false); + + const [pwForm, setPwForm] = useState({ + current_password: "", + new_password: "", + confirm_password: "", + }); + const [pwError, setPwError] = useState(""); + const [pwSuccess, setPwSuccess] = useState(false); + const [pwLoading, setPwLoading] = useState(false); + + useEffect(() => { + fetch("/api/auth/me") + .then((r) => r.json()) + .then((d) => { + setUsername(d.username); + setNewUsername(d.username); + }); + }, []); + + const saveUsername = async (e: React.FormEvent) => { + e.preventDefault(); + setUsernameError(""); + setUsernameSuccess(false); + if (newUsername.trim() === username) return; + setUsernameLoading(true); + const res = await fetch("/api/auth/change-username", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ new_username: newUsername.trim() }), + }); + setUsernameLoading(false); + if (res.ok) { + setUsername(newUsername.trim()); + setUsernameSuccess(true); + } else { + const data = await res.json(); + setUsernameError(data.error || "Failed to update username"); + } + }; + + const savePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPwError(""); + setPwSuccess(false); + if (pwForm.new_password !== pwForm.confirm_password) { + setPwError("New passwords do not match"); + return; + } + setPwLoading(true); + const res = await fetch("/api/auth/change-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + current_password: pwForm.current_password, + new_password: pwForm.new_password, + }), + }); + setPwLoading(false); + if (res.ok) { + setPwSuccess(true); + setPwForm({ current_password: "", new_password: "", confirm_password: "" }); + } else { + const data = await res.json(); + setPwError(data.error || "Failed to change password"); + } + }; + + return ( +
+

Settings

+ + {/* Username */} +
+
+ +

+ Change Username +

+
+
+
+ + setNewUsername(e.target.value)} + className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500" + /> +
+ {usernameError &&

{usernameError}

} + {usernameSuccess &&

Username updated successfully.

} + +
+
+ + {/* Password */} +
+
+ +

+ Change Password +

+
+
+
+ + setPwForm((f) => ({ ...f, current_password: e.target.value }))} + className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500" + /> +
+
+ + setPwForm((f) => ({ ...f, new_password: e.target.value }))} + className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500" + /> +
+
+ + setPwForm((f) => ({ ...f, confirm_password: e.target.value }))} + className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500" + /> +
+ {pwError &&

{pwError}

} + {pwSuccess &&

Password changed successfully.

} + +
+
+
+ ); +} diff --git a/packages/client/tsconfig.tsbuildinfo b/packages/client/tsconfig.tsbuildinfo index b95fa06..2fef7ef 100644 --- a/packages/client/tsconfig.tsbuildinfo +++ b/packages/client/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/components/layout.tsx","./src/pages/builddetail.tsx","./src/pages/login.tsx","./src/pages/projectdetail.tsx","./src/pages/projects.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/components/layout.tsx","./src/pages/builddetail.tsx","./src/pages/login.tsx","./src/pages/projectdetail.tsx","./src/pages/projects.tsx","./src/pages/settings.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/packages/server/src/routes/auth.js b/packages/server/src/routes/auth.js index 53be1e7..ca8e6c1 100644 --- a/packages/server/src/routes/auth.js +++ b/packages/server/src/routes/auth.js @@ -28,4 +28,34 @@ export default async function authRoutes(fastify) { return reply.code(401).send({ error: "Not authenticated" }); return { id: request.session.userId, username: request.session.username }; }); + + fastify.post("/api/auth/change-username", async (request, reply) => { + const { new_username } = request.body ?? {}; + if (!new_username?.trim()) + return reply.code(400).send({ error: "new_username required" }); + + const existing = db.prepare("SELECT id FROM users WHERE username = ?").get(new_username.trim()); + if (existing) + return reply.code(409).send({ error: "Username already taken" }); + + db.prepare("UPDATE users SET username = ? WHERE id = ?").run(new_username.trim(), request.session.userId); + request.session.username = new_username.trim(); + return { ok: true }; + }); + + fastify.post("/api/auth/change-password", async (request, reply) => { + const { current_password, new_password } = request.body ?? {}; + if (!current_password || !new_password) + return reply.code(400).send({ error: "current_password and new_password required" }); + if (new_password.length < 6) + return reply.code(400).send({ error: "new_password must be at least 6 characters" }); + + const user = db.prepare("SELECT * FROM users WHERE id = ?").get(request.session.userId); + if (!user || !(await bcrypt.compare(current_password, user.password_hash))) + return reply.code(401).send({ error: "Current password is incorrect" }); + + const hash = await bcrypt.hash(new_password, 10); + db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(hash, user.id); + return { ok: true }; + }); }