From 25b64281f8f494f60096cfd0f0fe2c90b8922937 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 20 Sep 2026 11:48:02 +0100 Subject: [PATCH] Add user change password --- src/components/general/host-wg-ip-field.tsx | 2 +- .../user-form/user-form-user-types.tsx | 10 +- .../(sections)/change-password-section.tsx | 140 ++++++++++++++++++ src/pages/admin/settings/index.tsx | 2 + src/pages/api/admin/change-password.ts | 74 +++++++++ src/types/index.ts | 3 + 6 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 src/pages/admin/settings/(sections)/change-password-section.tsx create mode 100644 src/pages/api/admin/change-password.ts diff --git a/src/components/general/host-wg-ip-field.tsx b/src/components/general/host-wg-ip-field.tsx index 991c4d3..5af6111 100644 --- a/src/components/general/host-wg-ip-field.tsx +++ b/src/components/general/host-wg-ip-field.tsx @@ -83,7 +83,7 @@ export default function HostWgIpField({ return ( { onChange?.(v); }} diff --git a/src/components/general/user-form/user-form-user-types.tsx b/src/components/general/user-form/user-form-user-types.tsx index 53c7807..0c533ec 100644 --- a/src/components/general/user-form/user-form-user-types.tsx +++ b/src/components/general/user-form/user-form-user-types.tsx @@ -16,14 +16,20 @@ export default function UserFormUserTypes({ ).map((ut) => ({ value: ut.value, }))} - defaultValues={form.user_types || []} + defaultValues={ + (form.user_types as unknown as UserType[] | undefined) || + [] + } changeHandler={(value) => { const user_types = Array.isArray(value) ? (value as UserType[]) : value ? ([value] as UserType[]) : []; - setForm({ ...form, user_types }); + setForm({ + ...form, + user_types: user_types.join(","), + }); }} title="User Types" showLabel diff --git a/src/pages/admin/settings/(sections)/change-password-section.tsx b/src/pages/admin/settings/(sections)/change-password-section.tsx new file mode 100644 index 0000000..1ee2c56 --- /dev/null +++ b/src/pages/admin/settings/(sections)/change-password-section.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { CircleCheck, TriangleAlert } from "lucide-react"; +import AdminCard from "@/src/components/general/admin-card"; +import useStatus from "@/src/components/twui/hooks/useStatus"; +import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; +import Input from "@/src/components/twui/form/Input"; +import H3 from "@/src/components/twui/layout/H3"; +import P from "@/src/components/twui/layout/P"; +import Row from "@/src/components/twui/layout/Row"; +import Span from "@/src/components/twui/layout/Span"; +import Stack from "@/src/components/twui/layout/Stack"; +import Tag from "@/src/components/twui/elements/Tag"; +import Button from "@/src/components/twui/layout/Button"; +import type { ApiReqParams } from "@/src/types"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; + +export default function ChangePasswordSection() { + const { loading, setLoading, status, setStatus } = useStatus(); + + const [new_password, setNewPassword] = useState(""); + const [confirm_password, setConfirmPassword] = useState(""); + + function handleChangePassword() { + if (!window.confirm("Change your password?")) return; + + setStatus(undefined); + setLoading(true); + + fetchApi( + `/api/admin/change-password`, + { + method: "POST", + body: { + new_password, + confirm_password, + }, + }, + ) + .then((res) => { + if (res.success) { + setNewPassword(""); + setConfirmPassword(""); + setStatus({ + success: true, + msg: res.msg || "Password updated", + }); + return; + } + + setStatus({ + error: true, + success: false, + msg: res.msg || "Could not update the password", + }); + }) + .catch((error: any) => { + setStatus({ + error: true, + success: false, + msg: error.message || "Could not update the password", + }); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( + + +

+ Change password +

+

+ Choose a strong password you don't use anywhere else. +

+
+ + { + setNewPassword(e.target.value); + }} + value={new_password} + /> + { + setConfirmPassword(e.target.value); + }} + value={confirm_password} + /> + + {status?.error && status.msg ? ( + + + + {status.msg} + + + ) : null} + {status?.success && status.msg ? ( + + + + {status.msg} + + + ) : null} + + + + +
+ ); +} \ No newline at end of file diff --git a/src/pages/admin/settings/index.tsx b/src/pages/admin/settings/index.tsx index 5cfd60c..e09da10 100644 --- a/src/pages/admin/settings/index.tsx +++ b/src/pages/admin/settings/index.tsx @@ -4,6 +4,7 @@ import { SiteData } from "@/src/data/site-data"; import Divider from "@/src/components/twui/layout/Divider"; import Stack from "@/src/components/twui/layout/Stack"; import SettingsFormSection from "./(sections)/settings-form-section"; +import ChangePasswordSection from "./(sections)/change-password-section"; export default function AdminSettingsPage() { return ( @@ -15,6 +16,7 @@ export default function AdminSettingsPage() { + ); diff --git a/src/pages/api/admin/change-password.ts b/src/pages/api/admin/change-password.ts new file mode 100644 index 0000000..00eb12a --- /dev/null +++ b/src/pages/api/admin/change-password.ts @@ -0,0 +1,74 @@ +import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db"; +import { AppData } from "@/src/data/app-data"; +import userAuth from "@/src/functions/backend/auth/user-auth"; +import type { ApiReqParams, TableType } from "@/src/types"; +import BunSQLite from "@moduletrace/bun-sqlite"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; + +export const handler: BunextAPIRouteHandler = async ( + params, +) => { + const req = params.req; + const body = params.body as ApiReqParams; + + if (req.method !== "POST") { + return { + success: false, + }; + } + + const { user } = await userAuth({ req }); + + if (!user?.logged_in_status || !user.id) { + return { + success: false, + msg: `Unauthorized`, + logoutUser: true, + }; + } + + try { + const { new_password, confirm_password } = body; + + if (!new_password) { + throw new Error(`Please enter a new password`); + } + + if (new_password !== confirm_password) { + throw new Error(`New passwords don't match`); + } + + const new_password_hash = await Bun.password.hash(new_password, { + algorithm: "bcrypt", + cost: AppData["HashCost"], + }); + + const update_res = await BunSQLite.update< + BUN_SQLITE_WGUI_USERS, + TableType + >({ + table: "users", + data: { + password: new_password_hash, + }, + targetId: user.id, + }); + + if (!update_res.success) { + throw new Error(`Couldn't update password`); + } + + return { + success: true, + msg: `Password updated successfully`, + }; + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 157ec21..7e4b4cf 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -200,6 +200,9 @@ export type ApiReqParams< email_or_username?: string; password?: string; }; + current_password?: string; + new_password?: string; + confirm_password?: string; contact?: { name?: string; email?: string;