This commit is contained in:
Benjamin Toby 2026-03-11 06:50:02 +00:00
parent 36f4af7065
commit 42dd622ad2
5 changed files with 183 additions and 101 deletions

View File

@ -1,12 +1,14 @@
import { AppContext } from "@/src/pages/_app";
import { TurboCISignupFormObject } from "@/src/types"; import { TurboCISignupFormObject } from "@/src/types";
import useStatus from "@/twui/components/hooks/useStatus"; import useStatus from "@/twui/components/hooks/useStatus";
import { useState } from "react"; import { useContext, useState } from "react";
type Params = { type Params = {
new_deployment_user?: boolean; new_deployment_user?: boolean;
}; };
export default function useSignupForm({ new_deployment_user }: Params) { export default function useSignupForm({ new_deployment_user }: Params) {
const { pageProps } = useContext(AppContext);
const [newUser, setNewUser] = useState<TurboCISignupFormObject>({}); const [newUser, setNewUser] = useState<TurboCISignupFormObject>({});
const { loading, setLoading } = useStatus(); const { loading, setLoading } = useStatus();
const [isPasswordConfirmed, setIsPasswordConfirmed] = useState(false); const [isPasswordConfirmed, setIsPasswordConfirmed] = useState(false);
@ -18,5 +20,6 @@ export default function useSignupForm({ new_deployment_user }: Params) {
setLoading, setLoading,
isPasswordConfirmed, isPasswordConfirmed,
setIsPasswordConfirmed, setIsPasswordConfirmed,
pageProps,
}; };
} }

View File

@ -19,6 +19,7 @@ export default function SignupForm({ new_deployment_user }: Props) {
setLoading, setLoading,
isPasswordConfirmed, isPasswordConfirmed,
setIsPasswordConfirmed, setIsPasswordConfirmed,
pageProps,
} = useSignupForm({ new_deployment_user }); } = useSignupForm({ new_deployment_user });
const is_password_valid = Boolean( const is_password_valid = Boolean(
@ -28,110 +29,146 @@ export default function SignupForm({ new_deployment_user }: Props) {
); );
return ( return (
<Stack className="w-full items-stretch"> <form
<Input onSubmit={(e) => {
placeholder="Eg. John" e.preventDefault();
title="First Name" }}
changeHandler={(v) => { >
setNewUser((prev) => ({ <Stack className="w-full items-stretch gap-6">
...prev, <Input
first_name: v, placeholder="Eg. John"
})); title="First Name"
}} changeHandler={(v) => {
showLabel setNewUser((prev) => ({
/> ...prev,
<Input first_name: v,
placeholder="Eg. Doe" }));
title="Last Name" }}
changeHandler={(v) => { required
setNewUser((prev) => ({ showLabel
...prev, />
last_name: v, <Input
})); placeholder="Eg. Doe"
}} title="Last Name"
showLabel changeHandler={(v) => {
/> setNewUser((prev) => ({
<Input ...prev,
placeholder="Email Address or Username" last_name: v,
title="Email/Username" }));
type="email" }}
changeHandler={(v) => { showLabel
setNewUser((prev) => ({ />
...prev, <Input
email: v, placeholder="Email Address"
})); title="Email"
}} type="email"
showLabel changeHandler={(v) => {
/> setNewUser((prev) => ({
<Input ...prev,
placeholder="Password" email: v,
title="Password" }));
type="password" }}
changeHandler={(v) => { required
setNewUser((prev) => ({ showLabel
...prev, />
password: v, {pageProps.user.id ? (
})); <Input
}} placeholder="Username"
validity={{ title="Username"
isValid: changeHandler={(v) => {
!Boolean(newUser.password?.match(/./)) || setNewUser((prev) => ({
!Boolean(newUser.confirmed_password?.match(/./)) ...prev,
? true username: v,
: is_password_valid, }));
msg: `Passwords don't match`, }}
}} validationRegex={/^[a-z0-9\-]{3,}$/}
showLabel info={
/> <>
<Input Allowed characters:{" "}
placeholder="Confirm Password" <code>
title="Confirm Password" <b>a-z, 0-9, -</b>
type="password" </code>
changeHandler={(v) => { .
setNewUser((prev) => ({ </>
...prev, }
confirmed_password: v, wrapperWrapperProps={{
})); className: "items-start!",
}}
required
showLabel
/>
) : null}
<Input
placeholder="Password"
title="Password"
type="password"
changeHandler={(v) => {
setNewUser((prev) => ({
...prev,
password: v,
}));
}}
validity={{
isValid:
!Boolean(newUser.password?.match(/./)) ||
!Boolean(newUser.confirmed_password?.match(/./))
? true
: is_password_valid,
msg: `Passwords don't match`,
}}
required
showLabel
/>
<Input
placeholder="Confirm Password"
title="Confirm Password"
type="password"
changeHandler={(v) => {
setNewUser((prev) => ({
...prev,
confirmed_password: v,
}));
setIsPasswordConfirmed(v == newUser.password); setIsPasswordConfirmed(v == newUser.password);
}} }}
showLabel showLabel
/> />
<Button <Button
title="Login" title="Login"
onClick={() => { onClick={() => {
if (!is_password_valid) { if (!is_password_valid) {
return; return;
} }
if (!window.confirm(`Create Super Admin Account?`)) { if (!window.confirm(`Create Super Admin Account?`)) {
return; return;
} }
setLoading(true); setLoading(true);
fetchApi<APIReqObject, APIResponseObject>( fetchApi<APIReqObject, APIResponseObject>(
`/api/auth/signup`, `/api/auth/signup`,
{ {
method: "POST", method: "POST",
body: { body: {
new_user: newUser, new_user: newUser,
},
}, },
}, )
) .then((res) => {
.then((res) => { console.log("res", res);
console.log("res", res);
if (res.success) { if (res.success) {
window.location.reload(); window.location.reload();
} }
}) })
.finally(() => {}); .finally(() => {});
}} }}
loading={loading} loading={loading}
> >
Signup Signup
</Button> </Button>
</Stack> </Stack>
</form>
); );
} }

View File

@ -0,0 +1,35 @@
import { NSQLITE_TURBOCI_ADMIN_USERS, NSQLiteTables } from "@/src/db/types";
import NSQLite from "@moduletrace/nsqlite";
import { existsSync } from "fs";
type Params = {
user_id: string | number;
};
export default async function setupDeploymentUser({ user_id }: Params) {
const target_user_res = await NSQLite.select<
NSQLITE_TURBOCI_ADMIN_USERS,
(typeof NSQLiteTables)[number]
>({
table: "users",
});
const target_user = target_user_res.singleRes;
if (!target_user?.id) {
return;
}
const { username } = target_user;
const user_dir = `/home/${username}`;
if (!existsSync(user_dir)) {
let cmd = ``;
cmd += `useradd --create-home --shell /bin/bash --comment "TurboCI Deployment user ${username}" ${username}\n`;
cmd += `passwd --lock "${username}"\n`;
}
return;
}

View File

@ -6,6 +6,7 @@ import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/
import NSQLite from "@moduletrace/nsqlite"; import NSQLite from "@moduletrace/nsqlite";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import userAuth from "@/src/utils/user-auth"; import userAuth from "@/src/utils/user-auth";
import { slugify } from "@/src/exports/client-exports";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -23,6 +24,10 @@ export default async function handler(
const { new_user } = req.body as APIReqObject; const { new_user } = req.body as APIReqObject;
if (user?.id && !new_user?.username?.match(/^[a-z0-9\-]{3,}$/)) {
throw new Error(`Please pass a valid username`);
}
if (!new_user) { if (!new_user) {
throw new Error(`No new User Object Passed!`); throw new Error(`No new User Object Passed!`);
} }
@ -52,7 +57,7 @@ export default async function handler(
}); });
} }
const { first_name, email, last_name, password } = new_user; const { first_name, email, last_name, password, username } = new_user;
const new_user_password = hashPassword({ password }); const new_user_password = hashPassword({ password });
@ -67,6 +72,7 @@ export default async function handler(
email, email,
password: new_user_password, password: new_user_password,
is_super_admin: user?.id ? 0 : 1, is_super_admin: user?.id ? 0 : 1,
username: slugify(username),
}, },
], ],
table: "users", table: "users",

View File

@ -230,6 +230,7 @@ export type TurboCISignupFormObject = {
email?: string; email?: string;
password?: string; password?: string;
confirmed_password?: string; confirmed_password?: string;
username?: string;
}; };
export type TurboCIAdminAppContextType = ReturnType<typeof useAppInit>; export type TurboCIAdminAppContextType = ReturnType<typeof useAppInit>;