This commit is contained in:
Benjamin Toby 2026-03-12 05:27:31 +00:00
parent 355ae63651
commit a34fd3aa20
9 changed files with 327 additions and 67 deletions

View File

@ -0,0 +1,69 @@
import Avatar from "@/src/components/general/avatar";
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import { AppContext } from "@/src/pages/_app";
import LucideIcon from "@/twui/components/elements/lucide-icon";
import Tag from "@/twui/components/elements/Tag";
import Button from "@/twui/components/layout/Button";
import Row from "@/twui/components/layout/Row";
import Span from "@/twui/components/layout/Span";
import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react";
import DeleteDeplotmentUserButton from "../deployment-user/(partials)/delete-deployment-user-button";
type Props = {
dep_usr: NSQLITE_TURBOCI_ADMIN_USERS;
};
export default function UsersListCard({ dep_usr }: Props) {
const { pageProps } = useContext(AppContext);
const is_super_admin = Boolean(dep_usr.is_super_admin);
return (
<a href={`/admin/users/${dep_usr.id}`}>
<Row className="w-full justify-between">
<Row>
<Avatar
image_url={dep_usr.image}
title={`${dep_usr.first_name} Image`}
image_size={40}
/>
<Span>
{dep_usr.first_name} {dep_usr.last_name}
</Span>
</Row>
<Row>
<Button
title="Edit User"
size="smaller"
variant="ghost"
beforeIcon={<LucideIcon name="Edit3" size={17} />}
/>
{is_super_admin ? (
<Tag>Super Admin</Tag>
) : (
<>
<DeleteDeplotmentUserButton
dep_user={dep_usr}
target={
<Button
title="Delete User"
size="smaller"
variant="ghost"
beforeIcon={
<LucideIcon
name="Trash"
size={17}
/>
}
/>
}
/>
</>
)}
</Row>
</Row>
</a>
);
}

View File

@ -1,10 +1,7 @@
import Avatar from "@/src/components/general/avatar";
import { AppContext } from "@/src/pages/_app"; import { AppContext } from "@/src/pages/_app";
import Tag from "@/twui/components/elements/Tag";
import Row from "@/twui/components/layout/Row";
import Span from "@/twui/components/layout/Span";
import Stack from "@/twui/components/layout/Stack"; import Stack from "@/twui/components/layout/Stack";
import { useContext } from "react"; import { useContext } from "react";
import UsersListCard from "../(partials)/users-list-card";
export default function UsersList() { export default function UsersList() {
const { pageProps } = useContext(AppContext); const { pageProps } = useContext(AppContext);
@ -13,27 +10,7 @@ export default function UsersList() {
return ( return (
<Stack className="grid-cell-content"> <Stack className="grid-cell-content">
{deployment_users?.map((dep_usr, index) => { {deployment_users?.map((dep_usr, index) => {
const is_super_admin = Boolean(dep_usr.is_super_admin); return <UsersListCard dep_usr={dep_usr} key={index} />;
return (
<a href={`/admin/users/${dep_usr.id}`} key={index}>
<Row className="w-full justify-between">
<Row>
<Avatar
image_url={dep_usr.image}
title={`${dep_usr.first_name} Image`}
image_size={40}
/>
<Span>
{dep_usr.first_name} {dep_usr.last_name}
</Span>
</Row>
<Row>
{is_super_admin ? <Tag>Super Admin</Tag> : null}
</Row>
</Row>
</a>
);
})} })}
</Stack> </Stack>
); );

View File

@ -0,0 +1,54 @@
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import { APIReqObject } from "@/src/types";
import Loading from "@/twui/components/elements/Loading";
import useStatus from "@/twui/components/hooks/useStatus";
import Button from "@/twui/components/layout/Button";
import fetchApi from "@moduletrace/datasquirel/dist/client/fetch";
import { APIResponseObject } from "@moduletrace/nsqlite/dist/types";
import { ComponentProps, ReactNode } from "react";
type Props = {
dep_user: NSQLITE_TURBOCI_ADMIN_USERS;
target?: ReactNode;
button_props?: Omit<ComponentProps<typeof Button>, "title">;
};
export default function DeleteDeplotmentUserButton({
dep_user,
target: passed_target,
button_props,
}: Props) {
const { loading, setLoading } = useStatus();
const target = (
<Button title="Delete Deployment User" {...button_props}>
Delete User
</Button>
);
return (
<div
onClick={() => {
if (!window.confirm(`Delete User?`)) {
return;
}
setLoading(true);
fetchApi<APIReqObject, APIResponseObject>(
`/api/admin/delete-user`,
{
method: "POST",
body: {
user_id: dep_user.id,
},
},
).then((res) => {
console.log(res);
});
}}
>
{loading ? <Loading /> : passed_target || target}
</div>
);
}

View File

@ -4,6 +4,9 @@ import Divider from "@/twui/components/layout/Divider";
import AdminHero from "@/src/components/general/admin/hero"; import AdminHero from "@/src/components/general/admin/hero";
import Tag from "@/twui/components/elements/Tag"; import Tag from "@/twui/components/elements/Tag";
import Row from "@/twui/components/layout/Row"; import Row from "@/twui/components/layout/Row";
import SignupForm from "../../../auth/signup/(partials)/signup-form";
import Stack from "@/twui/components/layout/Stack";
import DeleteDeplotmentUserButton from "./(partials)/delete-deployment-user-button";
export default function Main() { export default function Main() {
const { pageProps } = useContext(AppContext); const { pageProps } = useContext(AppContext);
@ -13,8 +16,6 @@ export default function Main() {
return null; return null;
} }
console.log("deployment_user", deployment_user);
const is_super_admin = Boolean(deployment_user.is_super_admin); const is_super_admin = Boolean(deployment_user.is_super_admin);
return ( return (
@ -29,8 +30,19 @@ export default function Main() {
) : null} ) : null}
</Row> </Row>
} }
ctas={
<>
<DeleteDeplotmentUserButton
dep_user={deployment_user}
button_props={{ color: "secondary" }}
/>
</>
}
/> />
<Divider /> <Divider />
<Stack className="grid-cell-content max-w-[600px]">
<SignupForm existing_user={deployment_user} />
</Stack>
</Fragment> </Fragment>
); );
} }

View File

@ -7,6 +7,7 @@ import { APIReqObject } from "@/src/types";
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types"; import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
import { useEffect } from "react"; import { useEffect } from "react";
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types"; import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import { twMerge } from "tailwind-merge";
type Props = { type Props = {
new_deployment_user?: boolean; new_deployment_user?: boolean;
@ -43,6 +44,7 @@ export default function SignupForm({
<Input <Input
placeholder="Eg. John" placeholder="Eg. John"
title="First Name" title="First Name"
defaultValue={existing_user?.first_name}
changeHandler={(v) => { changeHandler={(v) => {
setNewUser((prev) => ({ setNewUser((prev) => ({
...prev, ...prev,
@ -55,6 +57,7 @@ export default function SignupForm({
<Input <Input
placeholder="Eg. Doe" placeholder="Eg. Doe"
title="Last Name" title="Last Name"
defaultValue={existing_user?.last_name}
changeHandler={(v) => { changeHandler={(v) => {
setNewUser((prev) => ({ setNewUser((prev) => ({
...prev, ...prev,
@ -67,6 +70,7 @@ export default function SignupForm({
placeholder="Email Address" placeholder="Email Address"
title="Email" title="Email"
type="email" type="email"
defaultValue={existing_user?.email}
changeHandler={(v) => { changeHandler={(v) => {
setNewUser((prev) => ({ setNewUser((prev) => ({
...prev, ...prev,
@ -80,6 +84,7 @@ export default function SignupForm({
<Input <Input
placeholder="Username" placeholder="Username"
title="Username" title="Username"
defaultValue={existing_user?.username}
changeHandler={(v) => { changeHandler={(v) => {
setNewUser((prev) => ({ setNewUser((prev) => ({
...prev, ...prev,
@ -97,12 +102,21 @@ export default function SignupForm({
</> </>
} }
wrapperWrapperProps={{ wrapperWrapperProps={{
className: "items-start!", className: twMerge(
"items-start!",
Boolean(existing_user?.username)
? "opacity-70 pointer-events-none"
: "",
),
}} }}
disabled={Boolean(existing_user?.username)}
required required
showLabel showLabel
/> />
) : null} ) : null}
{existing_user?.id ? null : (
<>
<Input <Input
placeholder="Password" placeholder="Password"
title="Password" title="Password"
@ -116,7 +130,9 @@ export default function SignupForm({
validity={{ validity={{
isValid: isValid:
!Boolean(newUser.password?.match(/./)) || !Boolean(newUser.password?.match(/./)) ||
!Boolean(newUser.confirmed_password?.match(/./)) !Boolean(
newUser.confirmed_password?.match(/./),
)
? true ? true
: is_password_valid, : is_password_valid,
msg: `Passwords don't match`, msg: `Passwords don't match`,
@ -138,6 +154,8 @@ export default function SignupForm({
}} }}
showLabel showLabel
/> />
</>
)}
<Button <Button
title="Login" title="Login"
onClick={() => { onClick={() => {
@ -156,11 +174,14 @@ export default function SignupForm({
setLoading(true); setLoading(true);
fetchApi<APIReqObject, APIResponseObject>( fetchApi<APIReqObject, APIResponseObject>(
`/api/auth/signup`, existing_user?.id
? `/api/admin/edit-user`
: `/api/auth/signup`,
{ {
method: "POST", method: "POST",
body: { body: {
new_user: newUser, new_user: newUser,
user_id: existing_user?.id,
}, },
}, },
) )
@ -178,7 +199,7 @@ export default function SignupForm({
loading={loading} loading={loading}
> >
{existing_user?.id {existing_user?.id
? `` ? `Edit User`
: pageProps.user.super_admin : pageProps.user.super_admin
? "Add User" ? "Add User"
: "Signup"} : "Signup"}

View File

@ -0,0 +1,53 @@
import { NSQLITE_TURBOCI_ADMIN_USERS, NSQLiteTables } from "@/src/db/types";
import { _n } from "@/src/exports/client-exports";
import grabDeploymentUserDirNames from "@/src/utils/grab-deployment-user-dir-names";
import NSQLite from "@moduletrace/nsqlite";
import { execSync } from "child_process";
type Params = {
user_id: string | number;
};
export default async function deleteDeploymentUser({ user_id }: Params) {
const target_user_res = await NSQLite.select<
NSQLITE_TURBOCI_ADMIN_USERS,
(typeof NSQLiteTables)[number]
>({
table: "users",
targetId: _n(user_id),
});
console.log("target_user_res", target_user_res);
const target_user = target_user_res.singleRes;
if (
!target_user?.id ||
!target_user.username ||
target_user.is_super_admin
) {
return;
}
const { username } = target_user;
const { force_command_file, sshd_config_file } = grabDeploymentUserDirNames(
{ user: target_user },
);
let cmd = `/bin/bash << 'TURBOCIHEREDOC'\n`;
// cmd += `userdel -r ${username}\n`;
cmd += `killall -u ${username}\n`;
cmd += `deluser --remove-all-files ${username}\n`;
cmd += `rm -f ${force_command_file}\n`;
cmd += `rm -f ${sshd_config_file}\n`;
cmd += `Match User ${username}\n`;
execSync(cmd);
NSQLite.delete({ table: "users", targetId: target_user.id });
return;
}

View File

@ -1,10 +1,9 @@
import loginUser from "@/src/functions/auth/login-user";
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import userAuth from "@/src/utils/user-auth"; import userAuth from "@/src/utils/user-auth";
import NSQLite from "@moduletrace/nsqlite";
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types"; import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { APIReqObject } from "@/src/types"; import { APIReqObject } from "@/src/types";
import { _n } from "@/src/exports/client-exports";
import deleteDeploymentUser from "@/src/functions/deployment-users/delete-deployment-user";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -27,6 +26,14 @@ export default async function handler(
}); });
} }
const { user_id } = req.body as APIReqObject;
if (_n(user_id) == user.id) {
throw new Error(`Can't delete root user!`);
}
await deleteDeploymentUser({ user_id: _n(user_id) });
return res.json({ return res.json({
success: true, success: true,
}); });

View File

@ -0,0 +1,66 @@
import loginUser from "@/src/functions/auth/login-user";
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import userAuth from "@/src/utils/user-auth";
import NSQLite from "@moduletrace/nsqlite";
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
import type { NextApiRequest, NextApiResponse } from "next";
import { APIReqObject } from "@/src/types";
import { _n } from "@/src/exports/client-exports";
import setupDeploymentUser from "@/src/functions/deployment-users/setup-deployment-user";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<APIResponseObject>,
) {
try {
if (req.method !== "POST") {
return res.json({
success: false,
msg: "Wrong Method",
});
}
const { singleRes: user } = await userAuth({ req });
if (!user?.id || !user.super_admin) {
return res.json({
success: false,
msg: "Unauthorized",
});
}
const { new_user, user_id } = req.body as APIReqObject;
if (!new_user) {
throw new Error(`No User Form Sent.`);
}
const { first_name, last_name, email, image, username } = new_user;
if (!first_name?.match(/./)) {
return res.json({ success: false, msg: "First name is required" });
}
const update: NSQLITE_TURBOCI_ADMIN_USERS = {
first_name,
last_name,
email,
image,
username,
};
await NSQLite.update({
table: "users",
targetId: _n(user_id),
data: update,
});
await setupDeploymentUser({ user_id: _n(user_id) });
return res.json({
success: true,
});
} catch (error: any) {
return res.json({ success: false, msg: error.message });
}
}

View File

@ -207,6 +207,7 @@ export type APIReqObject = {
email?: string; email?: string;
password?: string; password?: string;
new_user?: TurboCISignupFormObject; new_user?: TurboCISignupFormObject;
user_id?: string | number;
}; };
export type LoginFormData = { export type LoginFormData = {