Add dedicated bot, workspace and user settings pages
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import UserForm from "@/src/pages/admin/people/create/(partials)/user-form";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: (params: { person: BUN_SQLITE_COBBLE_USERS_JOIN; msg: string }) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a user in a modal. Editing one happens on its settings page.
|
||||
*/
|
||||
export default function NewUserModal({ open, onClose, onSaved }: Props) {
|
||||
return (
|
||||
<AddModalShell
|
||||
open={open}
|
||||
setOpen={(next) => !next && onClose()}
|
||||
title="Add a user"
|
||||
description="They get their own coordinator and main computer, and sign in with this password."
|
||||
>
|
||||
{open ? <UserForm onCancel={onClose} onSaved={onSaved} /> : null}
|
||||
</AddModalShell>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import AddModalShell from "@/src/layouts/bots/(partials)/add-modal-shell";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import UserForm from "@/src/pages/admin/people/create/(partials)/user-form";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Edits this user; adds one when not given.
|
||||
*/
|
||||
person?: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: (params: { person: BUN_SQLITE_COBBLE_USERS_JOIN; msg: string }) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds or edits a user in a modal.
|
||||
*/
|
||||
export default function UserEditModal({ person, open, onClose, onSaved }: Props) {
|
||||
return (
|
||||
<AddModalShell
|
||||
open={open}
|
||||
setOpen={(next) => !next && onClose()}
|
||||
title={person ? `Edit ${person.first_name || "the user"}` : "Add a user"}
|
||||
description={
|
||||
person
|
||||
? "Leave the password empty to keep theirs."
|
||||
: "They get their own coordinator and main computer, and sign in with this password."
|
||||
}
|
||||
>
|
||||
{open ? <UserForm key={person?.id || "new"} person={person} onCancel={onClose} onSaved={onSaved} /> : null}
|
||||
</AddModalShell>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import canRevokeUser from "@/src/utils/rbac/can-revoke-user";
|
||||
import canUpdateUser from "@/src/utils/rbac/can-update-user";
|
||||
import SettingsListRow from "../../(partials)/settings-list-row";
|
||||
import SettingsRowAction from "../../(partials)/settings-row-action";
|
||||
|
||||
@@ -13,22 +12,20 @@ type Props = {
|
||||
actor_user_id?: number | string;
|
||||
actor_user_types?: BUN_SQLITE_COBBLE_USER_TYPES[] | null;
|
||||
busy?: boolean;
|
||||
onEdit: () => void;
|
||||
onRevoke: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One user: their photo, name, user types and email, then Edit, Revoke
|
||||
* (not once revoked) and Delete where the actor may (`canUpdateUser`,
|
||||
* `canRevokeUser`; never revoking or deleting yourself).
|
||||
* One user: their photo, name, user types and email. The title opens
|
||||
* their settings page; Revoke (not once revoked) and Delete sit at the
|
||||
* end where the actor may (`canRevokeUser`; never yourself).
|
||||
*/
|
||||
export default function UserSettingsRow({
|
||||
person,
|
||||
actor_user_id,
|
||||
actor_user_types,
|
||||
busy,
|
||||
onEdit,
|
||||
onRevoke,
|
||||
onDelete,
|
||||
}: Props) {
|
||||
@@ -42,25 +39,17 @@ export default function UserSettingsRow({
|
||||
.join(", ");
|
||||
const is_self = person.id == actor_user_id;
|
||||
const target_user_types = user_types.map((user_type) => ({ user_type }));
|
||||
const can_edit = canUpdateUser({ actor_user_types, actor_user_id, target_user_id: person.id, target_user_types });
|
||||
const can_delete = !is_self && canRevokeUser({ user_types: actor_user_types, target_user_types });
|
||||
const can_revoke = can_delete && !user_types.includes("revoked");
|
||||
|
||||
return (
|
||||
<SettingsListRow
|
||||
avatar={<UserAvatar person={person} size="md" />}
|
||||
href={`/admin/settings/users/${person.id}`}
|
||||
title={name}
|
||||
badge={is_self ? "You" : undefined}
|
||||
subtitle={[types_title, person.email].filter(Boolean).join(" · ")}
|
||||
>
|
||||
{can_edit ? (
|
||||
<SettingsRowAction
|
||||
title={`Edit ${name}`}
|
||||
label="Edit"
|
||||
disabled={busy}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
) : null}
|
||||
{can_revoke ? (
|
||||
<SettingsRowAction
|
||||
title={`Revoke ${name}`}
|
||||
|
||||
@@ -8,26 +8,21 @@ import useSettingsList from "../../(hooks)/use-settings-list";
|
||||
import SettingsGroup from "../../(partials)/settings-group";
|
||||
import SettingsList from "../../(partials)/settings-list";
|
||||
import SettingsStatus from "../../(partials)/settings-status";
|
||||
import UserEditModal from "../(partials)/user-edit-modal";
|
||||
import NewUserModal from "../(partials)/new-user-modal";
|
||||
import UserSettingsRow from "../(partials)/user-settings-row";
|
||||
import revokeUser from "../(functions)/revoke-user";
|
||||
|
||||
/**
|
||||
* Everyone who can sign in (for super admins and admins): add a user
|
||||
* with a password, edit one (photo, name, email, username, a new
|
||||
* password, user types), revoke one, or delete one with everything
|
||||
* that's theirs. What each person
|
||||
* may do here is the RBAC helpers' call.
|
||||
* Everyone who can sign in (for super admins and admins). A row opens
|
||||
* that user's settings page; add one with a password, revoke one, or
|
||||
* delete one with everything that's theirs.
|
||||
*/
|
||||
export default function UsersSettingsSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const { items, setItems, busy, setBusy, status, setStatus, remove } = useSettingsList({
|
||||
items: pageProps?.persons || [],
|
||||
});
|
||||
/**
|
||||
* The user being edited, or `true` while adding one.
|
||||
*/
|
||||
const [editing, setEditing] = useState<BUN_SQLITE_COBBLE_USERS_JOIN | true>();
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const savePerson = (person: BUN_SQLITE_COBBLE_USERS_JOIN) =>
|
||||
setItems((prev) =>
|
||||
@@ -66,7 +61,6 @@ export default function UsersSettingsSection() {
|
||||
actor_user_id={pageProps?.user?.id}
|
||||
actor_user_types={pageProps?.user_types}
|
||||
busy={busy}
|
||||
onEdit={() => setEditing(person)}
|
||||
onRevoke={() => revoke(person)}
|
||||
onDelete={() =>
|
||||
remove({
|
||||
@@ -80,27 +74,26 @@ export default function UsersSettingsSection() {
|
||||
))}
|
||||
</SettingsList>
|
||||
{canCreateUser({ user_types: pageProps?.user_types }) ? (
|
||||
<Button
|
||||
title="Add a user"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={() => setEditing(true)}
|
||||
className="self-start gap-1!"
|
||||
>
|
||||
<Plus size={14} aria-hidden />
|
||||
New user
|
||||
</Button>
|
||||
<Button
|
||||
title="Add a user"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={() => setCreating(true)}
|
||||
className="self-start gap-1!"
|
||||
>
|
||||
<Plus size={14} aria-hidden />
|
||||
New user
|
||||
</Button>
|
||||
) : null}
|
||||
<SettingsStatus status={status} />
|
||||
<UserEditModal
|
||||
person={editing === true ? undefined : editing}
|
||||
open={Boolean(editing)}
|
||||
onClose={() => setEditing(undefined)}
|
||||
<NewUserModal
|
||||
open={creating}
|
||||
onClose={() => setCreating(false)}
|
||||
onSaved={({ person, msg }) => {
|
||||
savePerson(person);
|
||||
setEditing(undefined);
|
||||
setCreating(false);
|
||||
setStatus({ success: true, msg });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useState } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import canRevokeUser from "@/src/utils/rbac/can-revoke-user";
|
||||
import revokeUser from "../../(functions)/revoke-user";
|
||||
import SettingsGroup from "../../../(partials)/settings-group";
|
||||
import SettingsStatus from "../../../(partials)/settings-status";
|
||||
|
||||
type Props = {
|
||||
person: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
actor_user_id?: number | string;
|
||||
actor_user_types?: BUN_SQLITE_COBBLE_USER_TYPES[] | null;
|
||||
onUpdated: (person: BUN_SQLITE_COBBLE_USERS_JOIN) => void;
|
||||
};
|
||||
|
||||
type Status = { error?: boolean; success?: boolean; msg?: string };
|
||||
|
||||
/**
|
||||
* Revoking or deleting a user, where the actor may. Never yourself, and
|
||||
* never the super admin.
|
||||
*/
|
||||
export default function UserDangerGroup({
|
||||
person,
|
||||
actor_user_id,
|
||||
actor_user_types,
|
||||
onUpdated,
|
||||
}: Props) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [status, setStatus] = useState<Status>();
|
||||
const name = person.first_name || "this user";
|
||||
const user_types = parseUserTypes({ user_types: person.user_types });
|
||||
const is_self = person.id == actor_user_id;
|
||||
const target_user_types = user_types.map((user_type) => ({ user_type }));
|
||||
const can_delete =
|
||||
!is_self &&
|
||||
canRevokeUser({ user_types: actor_user_types, target_user_types });
|
||||
const can_revoke = can_delete && !user_types.includes("revoked");
|
||||
|
||||
const revoke = async () => {
|
||||
if (
|
||||
busy ||
|
||||
!window.confirm(
|
||||
`Revoke ${name}? They can't sign in until Revoked is removed from their user types. Their crew and files are kept.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
setBusy(true);
|
||||
setStatus(undefined);
|
||||
|
||||
const res = await revokeUser({ person });
|
||||
|
||||
setBusy(false);
|
||||
|
||||
if (!res.success || !res.singleRes?.id) {
|
||||
setStatus({ error: true, msg: res.msg || "Couldn't revoke them." });
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdated(res.singleRes);
|
||||
setStatus({ success: true, msg: `${name} can't sign in now.` });
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
if (
|
||||
busy ||
|
||||
!window.confirm(
|
||||
`Delete ${name} with their bots, workspaces and computers? Their computers' files stay on disk.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
setBusy(true);
|
||||
setStatus(undefined);
|
||||
|
||||
const res = await postAdminApi({
|
||||
path: "users/delete-user",
|
||||
body: { user_id: person.id },
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
window.location.href = "/admin/settings/users";
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(false);
|
||||
setStatus({ error: true, msg: res.msg || "Couldn't delete them." });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
id="danger"
|
||||
title="Danger zone"
|
||||
description="Revoking keeps everything they had; deleting takes it with them."
|
||||
>
|
||||
{is_self ? (
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
This is you. You can't revoke or delete yourself here.
|
||||
</Span>
|
||||
) : !can_delete ? (
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
Nothing can be done here.
|
||||
</Span>
|
||||
) : (
|
||||
<Row className="w-full gap-2! flex-wrap items-center">
|
||||
{can_revoke ? (
|
||||
<Button
|
||||
title={`Revoke ${name}`}
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
loading={busy}
|
||||
disabled={busy}
|
||||
onClick={revoke}
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
title={`Delete ${name}`}
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
loading={busy}
|
||||
disabled={busy}
|
||||
onClick={remove}
|
||||
>
|
||||
Delete user
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
<SettingsStatus status={status} />
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import UserForm from "@/src/pages/admin/people/create/(partials)/user-form";
|
||||
import SettingsGroup from "../../../(partials)/settings-group";
|
||||
import SettingsStatus from "../../../(partials)/settings-status";
|
||||
|
||||
type Props = {
|
||||
person: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
onSaved: (person: BUN_SQLITE_COBBLE_USERS_JOIN) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The user's profile, photo, user types, access and password, saved
|
||||
* together. The same form as adding one, embedded on the page.
|
||||
*/
|
||||
export default function UserEditGroup({ person, onSaved }: Props) {
|
||||
const [status, setStatus] = useState<{ success?: boolean; msg?: string }>();
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
id="edit"
|
||||
title="Edit"
|
||||
description="Their photo, name, contact details, user types, what they reach and a new password."
|
||||
>
|
||||
<UserForm
|
||||
key={`${person.id}-${person.updated_at || ""}`}
|
||||
person={person}
|
||||
embedded
|
||||
onSaved={({ person: saved, msg }) => {
|
||||
onSaved(saved);
|
||||
setStatus({ success: true, msg });
|
||||
}}
|
||||
/>
|
||||
<SettingsStatus status={status} />
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import UserAvatar from "@/src/components/general/user-avatar";
|
||||
import { UserTypes } from "@/src/dict/user-types-dict";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import parseIdList from "@/src/utils/parse-id-list";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import SettingsGroup from "../../../(partials)/settings-group";
|
||||
|
||||
type Props = {
|
||||
person: BUN_SQLITE_COBBLE_USERS_JOIN;
|
||||
/**
|
||||
* The session user, to say "You".
|
||||
*/
|
||||
actor_user_id?: number | string;
|
||||
/**
|
||||
* Workers and workspaces a member reaches.
|
||||
*/
|
||||
crew_bots: BUN_SQLITE_COBBLE_BOTS[];
|
||||
crew_workspaces: BUN_SQLITE_COBBLE_WORKSPACES[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A user at a glance: their photo, name, contact details, user types and
|
||||
* what they reach.
|
||||
*/
|
||||
export default function UserOverviewGroup({
|
||||
person,
|
||||
actor_user_id,
|
||||
crew_bots,
|
||||
crew_workspaces,
|
||||
}: Props) {
|
||||
const name =
|
||||
[person.first_name, person.last_name].filter(Boolean).join(" ") ||
|
||||
person.email ||
|
||||
`User ${person.id}`;
|
||||
const user_types = parseUserTypes({ user_types: person.user_types });
|
||||
const is_self = person.id == actor_user_id;
|
||||
const reaches_everything =
|
||||
user_types.includes("super_admin") || user_types.includes("admin");
|
||||
const bot_ids = parseIdList({ ids: person.bot_ids });
|
||||
const workspace_ids = parseIdList({ ids: person.workspace_ids });
|
||||
const bot_names = crew_bots
|
||||
.filter((bot) => bot_ids.includes(Number(bot.id)))
|
||||
.map((bot) => bot.name || `Bot ${bot.id}`);
|
||||
const workspace_names = crew_workspaces
|
||||
.filter((workspace) => workspace_ids.includes(Number(workspace.id)))
|
||||
.map((workspace) => workspace.name || `Workspace ${workspace.id}`);
|
||||
const access = reaches_everything
|
||||
? "Every worker and workspace."
|
||||
: [
|
||||
bot_names.length
|
||||
? `Bots: ${bot_names.join(", ")}`
|
||||
: "No bots.",
|
||||
workspace_names.length
|
||||
? `Workspaces: ${workspace_names.join(", ")}`
|
||||
: "No workspaces.",
|
||||
].join(" ");
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
id="overview"
|
||||
title="Overview"
|
||||
description="Who this is, and what they can reach."
|
||||
>
|
||||
<Row className="w-full gap-4! flex-nowrap items-start">
|
||||
<UserAvatar person={person} size="lg" />
|
||||
<Stack className="grow min-w-0 gap-1!">
|
||||
<Row className="gap-2! flex-nowrap items-center">
|
||||
<Span className="text-sm font-bold leading-5 text-ink truncate">
|
||||
{name}
|
||||
</Span>
|
||||
{is_self ? (
|
||||
<Span className="shrink-0 text-[11px] font-bold leading-4 text-ink-secondary">
|
||||
You
|
||||
</Span>
|
||||
) : null}
|
||||
</Row>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary truncate w-full">
|
||||
{[person.email, person.username].filter(Boolean).join(" · ")}
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{UserTypes.filter((type) =>
|
||||
user_types.includes(type.value),
|
||||
)
|
||||
.map((type) => type.title)
|
||||
.join(", ") || "No user types"}
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{access}
|
||||
</Span>
|
||||
</Stack>
|
||||
</Row>
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import { UserTypes } from "@/src/dict/user-types-dict";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import SettingsShell from "../../../(partials)/settings-shell";
|
||||
import UserDangerGroup from "../(partials)/user-danger-group";
|
||||
import UserEditGroup from "../(partials)/user-edit-group";
|
||||
import UserOverviewGroup from "../(partials)/user-overview-group";
|
||||
|
||||
/**
|
||||
* One user's settings: overview, the form that edits them, and revoking
|
||||
* or deleting them where the actor may.
|
||||
*/
|
||||
export default function UserSettingsSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const [person, setPerson] = useState<
|
||||
BUN_SQLITE_COBBLE_USERS_JOIN | undefined
|
||||
>(pageProps?.target_person || undefined);
|
||||
|
||||
if (!person?.id) return null;
|
||||
|
||||
const name =
|
||||
[person.first_name, person.last_name].filter(Boolean).join(" ") ||
|
||||
person.email ||
|
||||
`User ${person.id}`;
|
||||
const user_types = parseUserTypes({ user_types: person.user_types });
|
||||
const types_title = UserTypes.filter((type) =>
|
||||
user_types.includes(type.value),
|
||||
)
|
||||
.map((type) => type.title)
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<SettingsShell
|
||||
title={name}
|
||||
description={
|
||||
types_title || person.email || "A user on this install."
|
||||
}
|
||||
breadcrumb_title={name}
|
||||
>
|
||||
<UserOverviewGroup
|
||||
person={person}
|
||||
actor_user_id={pageProps?.user?.id}
|
||||
crew_bots={pageProps?.crew_bots || []}
|
||||
crew_workspaces={pageProps?.crew_workspaces || []}
|
||||
/>
|
||||
<UserEditGroup person={person} onSaved={setPerson} />
|
||||
<UserDangerGroup
|
||||
person={person}
|
||||
actor_user_id={pageProps?.user?.id}
|
||||
actor_user_types={pageProps?.user_types}
|
||||
onUpdated={setPerson}
|
||||
/>
|
||||
</SettingsShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { PagePropsType, PageQueryObject } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import { SettingsDefaultPath } from "@/src/dict/settings-pages-dict";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabCrewOptions from "@/src/functions/backend/db/access/grab-crew-options";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
|
||||
import canListUsers from "@/src/utils/rbac/can-list-users";
|
||||
import grabSettingsServerProps from "../../(functions)/grab-settings-server-props";
|
||||
|
||||
/**
|
||||
* One user's settings, for those who may list users (`canListUsers`):
|
||||
* their profile, user types, access and danger zone.
|
||||
*/
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
const settings = await grabSettingsServerProps(params);
|
||||
|
||||
if (!settings.props) return settings;
|
||||
|
||||
const { user_types } = await userAuth({ req: params.req });
|
||||
|
||||
if (!canListUsers({ user_types })) {
|
||||
return { redirect: { destination: SettingsDefaultPath, permanent: false } };
|
||||
}
|
||||
|
||||
const query: PageQueryObject = params.query;
|
||||
const target_person = (
|
||||
await grabUsers({ user_id: String(query.user_id) })
|
||||
).singleRes;
|
||||
|
||||
if (!target_person?.id) {
|
||||
return { redirect: { destination: "/admin/settings/users", permanent: false } };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...settings.props,
|
||||
target_person: sanitizeUserBio({ user: target_person }),
|
||||
...(await grabCrewOptions()),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import UserSettingsSection from "./(sections)/user-settings-section";
|
||||
import settingsRecordPageMeta from "../../(functions)/settings-record-page-meta";
|
||||
|
||||
export default function AdminSettingsUserPage() {
|
||||
return <UserSettingsSection />;
|
||||
}
|
||||
|
||||
export const meta = settingsRecordPageMeta({ kind: "users" });
|
||||
Reference in New Issue
Block a user