This commit is contained in:
Benjamin Toby 2026-03-12 12:50:30 +00:00
parent a34fd3aa20
commit ee62ac8f54
18 changed files with 258 additions and 93 deletions

2
.gitignore vendored
View File

@ -43,3 +43,5 @@ next-env.d.ts
.backups
/secrets
.tmp
/test

View File

@ -19,6 +19,7 @@
"next": "14^",
"next-mdx-remote": "^6.0.0",
"openai": "^6.25.0",
"prism-themes": "^1.9.0",
"react": "19.2.3",
"react-code-blocks": "^0.1.6",
"react-dom": "19.2.3",
@ -808,6 +809,8 @@
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"prism-themes": ["prism-themes@1.9.0", "", {}, "sha512-tX2AYsehKDw1EORwBps+WhBFKc2kxfoFpQAjxBndbZKr4fRmMkv47XN0BghC/K1qwodB1otbe4oF23vUTFDokw=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],

View File

@ -34,6 +34,7 @@
"next": "14^",
"next-mdx-remote": "^6.0.0",
"openai": "^6.25.0",
"prism-themes": "^1.9.0",
"react": "19.2.3",
"react-code-blocks": "^0.1.6",
"react-dom": "19.2.3",

View File

@ -0,0 +1,10 @@
import RemoteCodeBlock from "@/twui/components/elements/RemoteCodeBlock";
type Props = {
content: string;
mode: "javascript" | "typescript" | "sh";
};
export default function TurboCICodeBlock({ mode, content }: Props) {
return <RemoteCodeBlock content={`\`\`\`${mode}\n${content}\n\`\`\``} />;
}

View File

@ -13,10 +13,12 @@ import useSettingsForm from "../(hooks)/use-settings-form";
import { APIReqObject } from "@/src/types";
export default function SettingsForm() {
const { setToast } = useContext(AppContext);
const { setToast, pageProps } = useContext(AppContext);
const { formData, setFormData, loading, setLoading, user } =
useSettingsForm();
const deployment_user = pageProps.deployment_user;
async function handleSubmit() {
if (!formData.first_name?.match(/./)) return;
@ -54,6 +56,10 @@ export default function SettingsForm() {
}
}
if (!deployment_user?.id) {
return null;
}
return (
<Stack className="w-full max-w-[600px] items-stretch gap-6">
<Stack className="gap-1">
@ -62,7 +68,9 @@ export default function SettingsForm() {
</Span>
<Span>
<strong>
{user.super_admin ? "root" : user?.username || "—"}
{user.super_admin
? "root"
: deployment_user?.username || "—"}
</strong>
</Span>
</Stack>

View File

@ -26,6 +26,10 @@ export default function DeleteDeplotmentUserButton({
</Button>
);
if (dep_user.is_super_admin) {
return null;
}
return (
<div
onClick={() => {

View File

@ -0,0 +1,51 @@
import { useContext } from "react";
import { AppContext } from "@/src/pages/_app";
import Stack from "@/twui/components/layout/Stack";
import H2 from "@/twui/components/layout/H2";
import Span from "@/twui/components/layout/Span";
import RemoteCodeBlock from "@/twui/components/elements/RemoteCodeBlock";
import SingleLineCodeBlock from "@/twui/components/elements/SingleLineCodeBlock";
import TurboCICodeBlock from "@/src/components/general/code-block";
import H3 from "@/twui/components/layout/H3";
export default function SSHConnection() {
const { pageProps } = useContext(AppContext);
const { deployment_user, deployment } = pageProps;
if (!deployment_user?.id) {
return null;
}
const is_super_admin = Boolean(deployment_user.is_super_admin);
return (
<>
<Stack className="grid-cell-content">
<H2>Connection</H2>
<Span>
Use these information to connect this user to the Relay
server
</Span>
</Stack>
<hr />
<Stack className="nested-grid-frame grid-cols-2">
<Stack className="grid-cell">
<Stack className="grid-cell-content">
<H3>Admin Port Forwarding</H3>
<Span>
Use these information to connect this user to the
Relay server
</Span>
<TurboCICodeBlock
content={`ssh -N -L ${deployment_user.username}@${deployment?.relay_server_ip}`}
mode="sh"
/>
</Stack>
</Stack>
<Stack className="grid-cell"></Stack>
</Stack>
<hr />
</>
);
}

View File

@ -7,6 +7,7 @@ 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";
import SSHConnection from "./(sections)/ssh-connection";
export default function Main() {
const { pageProps } = useContext(AppContext);
@ -43,6 +44,8 @@ export default function Main() {
<Stack className="grid-cell-content max-w-[600px]">
<SignupForm existing_user={deployment_user} />
</Stack>
<Divider />
<SSHConnection/>
</Fragment>
);
}

View File

@ -0,0 +1,57 @@
import Input from "@/twui/components/form/Input";
import useSignupForm from "../(hooks)/use-signup-form";
type Props = ReturnType<typeof useSignupForm>;
export default function SignupFormPassword({
isPasswordConfirmed,
newUser,
setNewUser,
setIsPasswordConfirmed,
}: Props) {
const is_password_valid = Boolean(
isPasswordConfirmed &&
Boolean(newUser.password?.match(/./)) &&
Boolean(newUser.confirmed_password?.match(/./)),
);
return (
<>
<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);
}}
showLabel
/>
</>
);
}

View File

@ -5,9 +5,9 @@ import useSignupForm from "../(hooks)/use-signup-form";
import fetchApi from "@/twui/components/utils/fetch/fetchApi";
import { APIReqObject } from "@/src/types";
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
import { useEffect } from "react";
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import { twMerge } from "tailwind-merge";
import SignupFormPassword from "./signup-form-password";
type Props = {
new_deployment_user?: boolean;
@ -18,6 +18,7 @@ export default function SignupForm({
new_deployment_user,
existing_user,
}: Props) {
const init = useSignupForm({ new_deployment_user, existing_user });
const {
newUser,
setNewUser,
@ -26,7 +27,7 @@ export default function SignupForm({
isPasswordConfirmed,
setIsPasswordConfirmed,
pageProps,
} = useSignupForm({ new_deployment_user, existing_user });
} = init;
const is_password_valid = Boolean(
isPasswordConfirmed &&
@ -80,7 +81,7 @@ export default function SignupForm({
required
showLabel
/>
{pageProps.user.id ? (
{pageProps.user?.id && !existing_user?.is_super_admin ? (
<Input
placeholder="Username"
title="Username"
@ -115,47 +116,7 @@ export default function SignupForm({
/>
) : null}
{existing_user?.id ? 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);
}}
showLabel
/>
</>
)}
{existing_user?.id ? null : <SignupFormPassword {...init} />}
<Button
title="Login"
onClick={() => {
@ -163,7 +124,7 @@ export default function SignupForm({
return;
}
const confirm_msg = pageProps.user.id
const confirm_msg = pageProps.user?.id
? `Add New User?`
: `Create Super Admin Account?`;
@ -187,20 +148,31 @@ export default function SignupForm({
)
.then((res) => {
if (res.success) {
if (pageProps.user.id) {
if (pageProps.user?.id) {
window.location.pathname = `/admin/users`;
} else {
window.location.reload();
}
} else {
window.alert(
res.msg ||
res.error ||
`New User Creation Failed!`,
);
console.log("res", res);
}
})
.finally(() => {});
.finally(() => {
setTimeout(() => {
setLoading(false);
}, 5000);
});
}}
loading={loading}
>
{existing_user?.id
? `Edit User`
: pageProps.user.super_admin
: pageProps.user?.super_admin
? "Add User"
: "Signup"}
</Button>

View File

@ -17,8 +17,6 @@ export default async function deleteDeploymentUser({ user_id }: Params) {
targetId: _n(user_id),
});
console.log("target_user_res", target_user_res);
const target_user = target_user_res.singleRes;
if (
@ -31,6 +29,8 @@ export default async function deleteDeploymentUser({ user_id }: Params) {
const { username } = target_user;
console.log("username", username);
const { force_command_file, sshd_config_file } = grabDeploymentUserDirNames(
{ user: target_user },
);
@ -38,12 +38,13 @@ export default async function deleteDeploymentUser({ user_id }: Params) {
let cmd = `/bin/bash << 'TURBOCIHEREDOC'\n`;
// cmd += `userdel -r ${username}\n`;
cmd += `killall -u ${username}\n`;
cmd += `deluser --remove-all-files ${username}\n`;
// cmd += `pkill -u ${username} || echo "User Deleted!"\n`;
cmd += `deluser --remove-all-files ${username} || echo "User Deleted!"\n`;
cmd += `rm -f ${force_command_file}\n`;
cmd += `rm -f ${sshd_config_file}\n`;
cmd += `Match User ${username}\n`;
cmd += `TURBOCIHEREDOC\n`;
execSync(cmd);

View File

@ -18,11 +18,13 @@ export default async function setupDeploymentUser({ user_id }: Params) {
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) {
if (
!target_user?.id ||
!target_user.username ||
target_user.is_super_admin
) {
return;
}
@ -42,7 +44,7 @@ export default async function setupDeploymentUser({ user_id }: Params) {
cmd += `useradd --create-home --shell /bin/bash --comment "TurboCI Deployment user ${username}" ${username}\n`;
cmd += `passwd --lock "${username}"\n`;
cmd += `mkdir -p "${ssh_dir}"\n`;
cmd += `ssh-keygen -t ed25519 -f "${ssh_key_file}" -N ""\n`;
cmd += `ssh-keygen -t ed25519 -f "${ssh_key_file}" -N "" -C "${username}@$(hostname)"\n`;
cmd += `cp "${ssh_key_file}.pub" "${ssh_dir}/authorized_keys"\n`;
cmd += `chown -R "${username}:${username}" "${ssh_dir}"\n`;

View File

@ -1,25 +1,34 @@
import { User } from "@/src/types";
import { TWUI_LINK_LIST_LINK_OBJECT } from "@/twui/components/elements/LinkList";
export const AdminAsideLinks: (
| TWUI_LINK_LIST_LINK_OBJECT
| TWUI_LINK_LIST_LINK_OBJECT[]
| undefined
)[] = [
{
title: "Dashboard",
url: "/admin",
strict: true,
},
{
title: "Services",
url: "/admin/services",
},
{
title: "Users",
url: "/admin/users",
},
{
title: "Settings",
url: "/admin/settings",
},
];
type Params = { user?: User };
export function AdminAsideLinks({ user }: Params) {
const links: (
| TWUI_LINK_LIST_LINK_OBJECT
| TWUI_LINK_LIST_LINK_OBJECT[]
| undefined
)[] = [
{
title: "Dashboard",
url: "/admin",
strict: true,
},
{
title: "Services",
url: "/admin/services",
},
user?.super_admin
? {
title: "Users",
url: "/admin/users",
}
: undefined,
{
title: "Settings",
url: "/admin/settings",
},
];
return links;
}

View File

@ -2,16 +2,18 @@ import LinkList from "@/twui/components/elements/LinkList";
import Main from "@/twui/components/layout/Main";
import Row from "@/twui/components/layout/Row";
import Stack from "@/twui/components/layout/Stack";
import { Fragment, PropsWithChildren } from "react";
import { Fragment, PropsWithChildren, useContext } from "react";
import { AdminAsideLinks } from "./(data)/links";
import Header from "./header";
import { twMerge } from "tailwind-merge";
import Spacer from "@/twui/components/layout/Spacer";
import Head from "next/head";
import { AppContext } from "@/src/pages/_app";
type Props = PropsWithChildren & {};
export default function Layout({ children }: Props) {
const { pageProps } = useContext(AppContext);
return (
<Fragment>
<Head>
@ -26,7 +28,7 @@ export default function Layout({ children }: Props) {
<Header />
<Stack className="grid-cell col-span-6 xl:col-span-1 gap-0">
<LinkList
links={AdminAsideLinks}
links={AdminAsideLinks({ user: pageProps.user })}
className="w-full xl:flex-col"
linkProps={{
className: "turboci-admin-aside-link",

View File

@ -1,4 +1,5 @@
import "@/src/styles/globals.css";
import "prism-themes/themes/prism-dracula.css";
import type { AppProps } from "next/app";
import { createContext } from "react";
import { PagePropsType, TurboCIAdminAppContextType } from "../types";

View File

@ -1,6 +1,8 @@
import Main from "@/src/components/pages/admin/settings";
import { NSQLITE_TURBOCI_ADMIN_USERS } from "@/src/db/types";
import defaultAdminProps from "@/src/functions/pages/admin/default-admin-props";
import Layout from "@/src/layouts/admin";
import NSQLite from "@moduletrace/nsqlite";
import { GetServerSideProps } from "next";
export default function AdminSettings() {
@ -12,5 +14,27 @@ export default function AdminSettings() {
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return await defaultAdminProps({ ctx });
return await defaultAdminProps({
ctx,
async propsFn({ user }) {
if (!user.id) {
return `/admin`;
}
const users_res = await NSQLite.select<NSQLITE_TURBOCI_ADMIN_USERS>(
{
table: "users",
targetId: user.id,
},
);
if (!users_res.singleRes?.id) {
return `/admin/users`;
}
return {
deployment_user: users_res.singleRes,
};
},
});
};

View File

@ -29,6 +29,10 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
},
);
if (!users_res.singleRes?.id) {
return `/admin/users`;
}
return {
deployment_user: users_res.singleRes,
};

View File

@ -73,7 +73,7 @@ export default async function handler(
email,
password: new_user_password,
is_super_admin: user?.id ? 0 : 1,
username: slugify(username),
username: username ? slugify(username) : undefined,
},
],
table: "users",
@ -90,20 +90,31 @@ export default async function handler(
(typeof NSQLiteTables)[number]
>({
table: "users",
query: {
query: {
is_super_admin: { value: "1" },
},
},
targetId: new_user_id,
});
const newly_inserted_user = newly_inserted_user_res.singleRes;
if (!newly_inserted_user?.id) {
console.log("newly_inserted_user_res", newly_inserted_user_res);
throw new Error(`Couldn't Find Newly inserted user.`);
}
if (user?.id && !newly_inserted_user.username) {
const newly_inserted_user_res = await NSQLite.delete<
NSQLITE_TURBOCI_ADMIN_USERS,
(typeof NSQLiteTables)[number]
>({
table: "users",
targetId: newly_inserted_user?.id,
});
throw new Error(`Couldn't set Newly inserted user username.`);
}
if (user?.id) {
console.log("newly_inserted_user", newly_inserted_user);
await setupDeploymentUser({ user_id: newly_inserted_user.id });
return res.json({