First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
|
||||
import grabUserPasswordHash from "@/src/functions/backend/auth/grab-user-password-hash";
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
type Params = Parameters<BunextPageServerFn<PagePropsType>>[0];
|
||||
|
||||
/**
|
||||
* Shared server props for the `/admin/settings` sub-pages.
|
||||
*/
|
||||
export default async function grabSettingsServerProps({ req }: Params) {
|
||||
const { user } = await userAuth({ req });
|
||||
|
||||
if (!user?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/login",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const user_res = await grabUsers({ user_id: user.id });
|
||||
|
||||
if (!user_res.singleRes) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/admin",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const has_password = Boolean(
|
||||
await grabUserPasswordHash({ user_id: user.id }),
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
person: sanitizeUserBio({ user: user_res.singleRes }),
|
||||
has_password,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
type Params = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default function settingsPageMeta({
|
||||
label,
|
||||
}: Params): BunextPageModuleMetaFn {
|
||||
return async (params) => {
|
||||
const person = (params.serverRes?.props as PagePropsType | undefined)
|
||||
?.person;
|
||||
|
||||
const person_name = person
|
||||
? `${person.first_name + (person.last_name ? ` ${person.last_name}` : "")}`
|
||||
: "";
|
||||
|
||||
return {
|
||||
title: `${label} Settings${person_name ? ` - ${person_name}` : ""} | ${SiteData["SiteName"]}`,
|
||||
description: `Manage your ${label.toLowerCase()} settings on ${SiteData["SiteName"]}.`,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
|
||||
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
||||
import clientDeleteMediaHandler from "@/src/functions/frontend/client-delete-media-handler";
|
||||
import clientUploadMediaHandler from "@/src/functions/frontend/client-upload-media-handler";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function submitAccountForm({
|
||||
setLoading,
|
||||
setStatus,
|
||||
form,
|
||||
existing_full,
|
||||
image,
|
||||
}: ReturnType<typeof useFormInit<UserFormObject>>) {
|
||||
try {
|
||||
const user_id = existing_full?.id;
|
||||
|
||||
if (!user_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm("Update your account details?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const new_user_data: BUN_SQLITE_BSQLITE_USERS = _.pick<
|
||||
UserFormObject,
|
||||
keyof UserFormObject
|
||||
>(form, ["first_name", "last_name", "email", "username", "bio"]);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const update_user_res = await adminCrudHandler({
|
||||
action: "update",
|
||||
table: "users",
|
||||
update_data: new_user_data,
|
||||
id: user_id,
|
||||
});
|
||||
|
||||
if (!update_user_res.success) {
|
||||
throw new Error(update_user_res.msg || "Account Update Failed");
|
||||
}
|
||||
|
||||
if (image?.imageBase64Full) {
|
||||
const delete_media_res = await clientDeleteMediaHandler({
|
||||
user_id: user_id,
|
||||
media_paradigm: "user-profile-image",
|
||||
});
|
||||
|
||||
if (!delete_media_res.success) {
|
||||
throw new Error(
|
||||
delete_media_res.msg ||
|
||||
delete_media_res.error ||
|
||||
`Couldn't delete old media`,
|
||||
);
|
||||
}
|
||||
|
||||
const upload_image_res = await clientUploadMediaHandler({
|
||||
user_id: user_id,
|
||||
media_base_64_data_url: image.imageBase64Full,
|
||||
media_paradigms: ["user-profile-image"],
|
||||
media_type: "image",
|
||||
is_media_primary: true,
|
||||
update_on_duplicate: true,
|
||||
reauth: true,
|
||||
});
|
||||
|
||||
if (!upload_image_res.success) {
|
||||
throw new Error(
|
||||
upload_image_res.msg || "Couldn't upload Image",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
} catch (error: any) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: error.message || "Account Form Error",
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
|
||||
export default function AccountFormAction(
|
||||
_params: ReturnType<typeof useFormInit<BUN_SQLITE_BSQLITE_USERS>>,
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Button title="Save Changes">Save Changes</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import LoadingOverlay from "@moduletrace/twui/elements/LoadingOverlay";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS } from "@/db/types/db";
|
||||
import submitAccountForm from "../(functions)/submit-account-form";
|
||||
import AccountFormAction from "./account-form-action";
|
||||
import Tag from "@moduletrace/twui/elements/Tag";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import UserFormName from "@/src/pages/admin/people/create/(partials)/user-form/user-form-name";
|
||||
import UserFormEmail from "@/src/pages/admin/people/create/(partials)/user-form/user-form-email";
|
||||
import UserFormUsername from "@/src/pages/admin/people/create/(partials)/user-form/user-form-username";
|
||||
import UserFormBio from "@/src/pages/admin/people/create/(partials)/user-form/user-form-bio";
|
||||
import UserFormImage from "@/src/pages/admin/people/create/(partials)/user-form/user-form-image";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
existing_user?: UserFormObject;
|
||||
existing_user_full?: BUN_SQLITE_BSQLITE_USERS;
|
||||
};
|
||||
|
||||
export default function AccountForm({ existing_user, existing_user_full }: Props) {
|
||||
const init = useFormInit<UserFormObject>({
|
||||
existing: existing_user,
|
||||
existing_full: existing_user_full,
|
||||
});
|
||||
|
||||
const { loading, status } = init;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
className="w-full gap-6"
|
||||
onSubmit={() => {
|
||||
submitAccountForm(init);
|
||||
}}
|
||||
>
|
||||
{status?.error && <Tag color="error">{status.msg}</Tag>}
|
||||
{loading && <LoadingOverlay />}
|
||||
|
||||
<UserFormName {...init} />
|
||||
<Row className="w-full grid xl:grid-cols-2">
|
||||
<UserFormEmail {...init} />
|
||||
<UserFormUsername {...init} />
|
||||
</Row>
|
||||
<Row className="w-full grid xl:grid-cols-2 items-stretch">
|
||||
<UserFormBio {...init} />
|
||||
<UserFormImage {...init} />
|
||||
</Row>
|
||||
<AccountFormAction {...init} />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Section from "@moduletrace/twui/layout/Section";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import { useContext } from "react";
|
||||
import AccountForm from "../(partials)/account-form";
|
||||
import _ from "lodash";
|
||||
import type { UserFormObject } from "@/src/types";
|
||||
import type { BUN_SQLITE_BSQLITE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
|
||||
export default function AccountFormSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const person = pageProps?.person;
|
||||
|
||||
if (!person) return null;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<AccountForm
|
||||
existing_user={
|
||||
_.pick<
|
||||
BUN_SQLITE_BSQLITE_USERS_JOIN,
|
||||
keyof BUN_SQLITE_BSQLITE_USERS_JOIN
|
||||
>(person, [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"username",
|
||||
"bio",
|
||||
]) as UserFormObject
|
||||
}
|
||||
existing_user_full={person}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import AdminHero from "@/src/components/general/admin-hero";
|
||||
import Tag from "@moduletrace/twui/elements/Tag";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import { useContext } from "react";
|
||||
|
||||
type Props = {
|
||||
section_title: string;
|
||||
};
|
||||
|
||||
export default function Hero({ section_title }: Props) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const person = pageProps?.person;
|
||||
|
||||
if (!person) return null;
|
||||
|
||||
return (
|
||||
<AdminHero
|
||||
title={section_title}
|
||||
description={
|
||||
<Stack className="gap-4">
|
||||
{person.user_types?.[0] ? (
|
||||
<Row className="flex-nowrap">
|
||||
{person.user_types?.split(",").map((ut, i) => (
|
||||
<Tag key={i} color="gray" variant="outlined">
|
||||
{ut}
|
||||
</Tag>
|
||||
))}
|
||||
</Row>
|
||||
) : null}
|
||||
{person.bio ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: person.bio }}
|
||||
className="bio-subtitle"
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabSettingsServerProps from "./(functions)/grab-settings-server-props";
|
||||
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
return await grabSettingsServerProps(params);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import Hero from "./(sections)/hero";
|
||||
import AccountFormSection from "./(sections)/account-form-section";
|
||||
import settingsPageMeta from "./(functions)/settings-page-meta";
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero section_title="Profile" />
|
||||
<AccountFormSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta = settingsPageMeta({ label: "Profile" });
|
||||
@@ -0,0 +1,63 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { ApiReqParams, PasswordFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import validatePassword from "@/src/utils/validate-password";
|
||||
|
||||
type Params = ReturnType<typeof useFormInit<PasswordFormObject>> & {
|
||||
has_password: boolean;
|
||||
onDone: (params: { has_password: boolean }) => void;
|
||||
};
|
||||
|
||||
export default async function submitPasswordForm({
|
||||
form,
|
||||
setLoading,
|
||||
setStatus,
|
||||
has_password,
|
||||
onDone,
|
||||
}: Params) {
|
||||
if (has_password && !form.current_password) {
|
||||
setStatus({ error: true, msg: "Enter your current password." });
|
||||
return;
|
||||
}
|
||||
|
||||
const password_error = validatePassword({ password: form.new_password });
|
||||
|
||||
if (password_error) {
|
||||
setStatus({ error: true, msg: password_error });
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.new_password != form.confirm_password) {
|
||||
setStatus({ error: true, msg: "The passwords don't match." });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/admin/settings/update-password`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
password_update: {
|
||||
current_password: form.current_password,
|
||||
new_password: form.new_password,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res.success) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Couldn't update your password.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({ success: true, msg: res.msg || "Password saved." });
|
||||
onDone({ has_password: true });
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState } from "react";
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import LoadingOverlay from "@moduletrace/twui/elements/LoadingOverlay";
|
||||
import Tag from "@moduletrace/twui/elements/Tag";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { PasswordFormObject } from "@/src/types";
|
||||
import PasswordFormField from "./password-form-field";
|
||||
import PasswordFormActions from "./password-form-actions";
|
||||
import submitPasswordForm from "../../(functions)/submit-password-form";
|
||||
|
||||
type Props = {
|
||||
has_password: boolean;
|
||||
};
|
||||
|
||||
export default function PasswordForm({ has_password: initial }: Props) {
|
||||
const init = useFormInit<PasswordFormObject>();
|
||||
const { loading, status } = init;
|
||||
|
||||
const [has_password, setHasPassword] = useState(initial);
|
||||
const [form_key, setFormKey] = useState(0);
|
||||
|
||||
const onDone = ({ has_password }: { has_password: boolean }) => {
|
||||
setHasPassword(has_password);
|
||||
init.setForm({});
|
||||
setFormKey((prev) => prev + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
key={form_key}
|
||||
className="w-full gap-6"
|
||||
onSubmit={() => {
|
||||
submitPasswordForm({ ...init, has_password, onDone });
|
||||
}}
|
||||
>
|
||||
{status?.error && <Tag color="error">{status.msg}</Tag>}
|
||||
{status?.success && <Tag color="success">{status.msg}</Tag>}
|
||||
{loading && <LoadingOverlay />}
|
||||
|
||||
{has_password ? (
|
||||
<PasswordFormField
|
||||
{...init}
|
||||
field="current_password"
|
||||
label="Current password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
) : null}
|
||||
<Row className="w-full grid xl:grid-cols-2">
|
||||
<PasswordFormField
|
||||
{...init}
|
||||
field="new_password"
|
||||
label={has_password ? "New password" : "Password"}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<PasswordFormField
|
||||
{...init}
|
||||
field="confirm_password"
|
||||
label="Confirm password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Row>
|
||||
<PasswordFormActions has_password={has_password} />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
|
||||
type Props = {
|
||||
has_password: boolean;
|
||||
};
|
||||
|
||||
export default function PasswordFormActions({ has_password }: Props) {
|
||||
const label = has_password ? "Change password" : "Set password";
|
||||
|
||||
return (
|
||||
<Button title={label} type="submit">
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AutocompleteOptions } from "@moduletrace/twui/types";
|
||||
import PasswordInput from "@/src/components/general/password-input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { PasswordFormObject } from "@/src/types";
|
||||
|
||||
type Props = ReturnType<typeof useFormInit<PasswordFormObject>> & {
|
||||
field: keyof PasswordFormObject;
|
||||
label: string;
|
||||
autoComplete: (typeof AutocompleteOptions)[number];
|
||||
};
|
||||
|
||||
export default function PasswordFormField({
|
||||
setForm,
|
||||
field,
|
||||
label,
|
||||
autoComplete,
|
||||
}: Props) {
|
||||
return (
|
||||
<PasswordInput
|
||||
id={field}
|
||||
label={label}
|
||||
autoComplete={autoComplete}
|
||||
onValueChange={(value) => {
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Section from "@moduletrace/twui/layout/Section";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import H3 from "@moduletrace/twui/layout/H3";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import { useContext } from "react";
|
||||
import useLoginMethod from "@/src/hooks/use-login-method";
|
||||
import PasswordForm from "../(partials)/password-form";
|
||||
|
||||
export default function PasswordFormSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const method = useLoginMethod();
|
||||
|
||||
if (!pageProps?.person || method != "password") return null;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Stack className="w-full gap-6">
|
||||
<Stack className="gap-1">
|
||||
<H3 className="mb-0!">Change Password</H3>
|
||||
<Span variant="faded">
|
||||
You log in with your password, then a code we email you.
|
||||
</Span>
|
||||
</Stack>
|
||||
<PasswordForm has_password={Boolean(pageProps.has_password)} />
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import grabSettingsServerProps from "../(functions)/grab-settings-server-props";
|
||||
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
if (grabLoginMethod() != "password") {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/admin/settings",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return await grabSettingsServerProps(params);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import Hero from "../(sections)/hero";
|
||||
import PasswordFormSection from "./(sections)/password-form-section";
|
||||
import settingsPageMeta from "../(functions)/settings-page-meta";
|
||||
|
||||
export default function AdminSettingsPasswordPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero section_title="Password" />
|
||||
<PasswordFormSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta = settingsPageMeta({ label: "Password" });
|
||||
Reference in New Issue
Block a user