This commit is contained in:
bipproduction
2025-11-30 08:08:48 +08:00
parent 4583897684
commit b15fd3acaa
12 changed files with 152 additions and 5 deletions

View File

@@ -29,3 +29,8 @@ model ApiKey {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Configs {
id String @id @default("1")
allowRegister Boolean @default(false)
}

View File

@@ -8,6 +8,10 @@ const user = [
} }
]; ];
const configs = {
allowRegister: false
}
; (async () => { ; (async () => {
for (const u of user) { for (const u of user) {
await prisma.user.upsert({ await prisma.user.upsert({
@@ -19,7 +23,13 @@ const user = [
console.log(`✅ User ${u.email} seeded successfully`) console.log(`✅ User ${u.email} seeded successfully`)
} }
await prisma.configs.upsert({
where: { id: "1" },
create: configs,
update: configs,
})
console.log(`✅ Configs seeded successfully`)
})().catch((e) => { })().catch((e) => {
console.error(e) console.error(e)
process.exit(1) process.exit(1)

View File

@@ -64,6 +64,16 @@ const Register = {
preload: () => import("./pages/Register"), preload: () => import("./pages/Register"),
}; };
const ConfigLayout = {
Component: React.lazy(() => import("./pages/dashboard/config/config_layout")),
preload: () => import("./pages/dashboard/config/config_layout"),
};
const ConfigPage = {
Component: React.lazy(() => import("./pages/dashboard/config/config_page")),
preload: () => import("./pages/dashboard/config/config_page"),
};
const ApikeyPage = { const ApikeyPage = {
Component: React.lazy(() => import("./pages/dashboard/apikey/apikey_page")), Component: React.lazy(() => import("./pages/dashboard/apikey/apikey_page")),
preload: () => import("./pages/dashboard/apikey/apikey_page"), preload: () => import("./pages/dashboard/apikey/apikey_page"),
@@ -118,6 +128,19 @@ export default function AppRoutes() {
<Route path="/dashboard" element={<DashboardLayout.Component />}> <Route path="/dashboard" element={<DashboardLayout.Component />}>
<Route index element={<DashboardPage.Component />} /> <Route index element={<DashboardPage.Component />} />
<Route path="/dashboard/config" element={<ConfigLayout.Component />}>
<Route index element={<ConfigPage.Component />} />
<Route
path="/dashboard/config/config"
element={
<React.Suspense fallback={<SkeletonLoading />}>
<ConfigPage.Component />
</React.Suspense>
}
/>
</Route>
<Route <Route
path="/dashboard/apikey/apikey" path="/dashboard/apikey/apikey"
element={ element={

View File

@@ -4,6 +4,8 @@ const clientRoutes = {
"/": "/", "/": "/",
"/register": "/register", "/register": "/register",
"/dashboard": "/dashboard", "/dashboard": "/dashboard",
"/dashboard/config": "/dashboard/config",
"/dashboard/config/config": "/dashboard/config/config",
"/dashboard/apikey/apikey": "/dashboard/apikey/apikey", "/dashboard/apikey/apikey": "/dashboard/apikey/apikey",
"/dashboard/dashboard": "/dashboard/dashboard", "/dashboard/dashboard": "/dashboard/dashboard",
"/*": "/*" "/*": "/*"

View File

@@ -9,6 +9,8 @@ import { LandingPage } from "./Landing";
import { renderToReadableStream } from "react-dom/server"; import { renderToReadableStream } from "react-dom/server";
import { cors } from "@elysiajs/cors"; import { cors } from "@elysiajs/cors";
import packageJson from "./../package.json"; import packageJson from "./../package.json";
import Configs from "./server/routes/configs_route";
import { prisma } from "./server/lib/prisma";
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const Docs = new Elysia().use( const Docs = new Elysia().use(
@@ -68,6 +70,7 @@ const ApiUser = new Elysia({
const Api = new Elysia({ const Api = new Elysia({
prefix: "/api", prefix: "/api",
}) })
.use(Configs)
.use(apiAuth) .use(apiAuth)
.use(ApiKeyRoute) .use(ApiKeyRoute)
.use(ApiUser); .use(ApiUser);
@@ -77,6 +80,15 @@ const app = new Elysia()
.use(Api) .use(Api)
.use(Docs) .use(Docs)
.use(Auth) .use(Auth)
.get("/get-allow-register", async () => {
const configs = await prisma.configs.findUnique({ where: { id: "1" } })
return { allowRegister: configs?.allowRegister }
}, {
detail: {
description: "Get allow register",
summary: "get allow register",
},
})
.get( .get(
"/assets/:name", "/assets/:name",
(ctx) => { (ctx) => {

View File

@@ -13,6 +13,8 @@ import {
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import apiFetch from "../lib/apiFetch"; import apiFetch from "../lib/apiFetch";
import useSWR from "swr";
import { useNavigate } from "react-router-dom";
export default function Register() { export default function Register() {
const [name, setName] = useState(""); const [name, setName] = useState("");
@@ -20,6 +22,10 @@ export default function Register() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null); const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const navigate = useNavigate();
const { data, error, isLoading } = useSWR("/", apiFetch["get-allow-register"].get);
const allowRegister = data?.data?.allowRegister ?? false;
const handleSubmit = async () => { const handleSubmit = async () => {
setLoading(true); setLoading(true);
@@ -57,10 +63,22 @@ export default function Register() {
checkSession(); checkSession();
}, []); }, []);
if (isLoading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error.message}</Text>;
if (isAuthenticated === null) return null; if (isAuthenticated === null) return null;
if (isAuthenticated) if (isAuthenticated)
return <Navigate to={clientRoutes["/dashboard"]} replace />; return <Navigate to={clientRoutes["/dashboard"]} replace />;
if (!allowRegister) return <Container size={"md"} w={"100%"}>
<Group justify="center">
<Stack>
<Text>Allow register is disabled</Text>
<Button onClick={() => navigate(clientRoutes["/login"])}>Back to login</Button>
</Stack>
</Group>
</Container>;
return ( return (
<Container size={420} py={80}> <Container size={420} py={80}>
<Card shadow="sm" radius="md" padding="xl"> <Card shadow="sm" radius="md" padding="xl">

View File

@@ -0,0 +1,5 @@
import { Outlet } from "react-router-dom";
export default function ConfigLayout() {
return <Outlet />;
}

View File

@@ -0,0 +1,37 @@
import apiFetch from "@/lib/apiFetch";
import { Container, Stack, Switch, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import useSWR from "swr";
export default function ConfigPage() {
const { data, error, isLoading } = useSWR("/", apiFetch["get-allow-register"].get);
const [allowRegister, setAllowRegister] = useState(false);
useShallowEffect(() => {
if (data) {
setAllowRegister(data.data?.allowRegister ?? false)
}
}, [data])
if (isLoading) return <Container size="lg" w={"100%"}><Text>Loading...</Text></Container>
if (error) return <Container size="lg" w={"100%"}><Text>Error: {error.message}</Text></Container>
async function updateAllowRegister({ allowRegister }: { allowRegister: boolean }) {
const res = await apiFetch.api.configs["update-allow-register"].post({ allowRegister: allowRegister })
console.log(res.data)
setAllowRegister(res.data?.allowRegister ?? false)
}
return <Container size="lg" w={"100%"}>
<Stack>
<Text>Config Page</Text>
<Switch label="Allow Register" checked={allowRegister} onChange={(e) => {
updateAllowRegister({ allowRegister: !allowRegister })
}} />
</Stack>
</Container>
}

View File

@@ -22,6 +22,8 @@ import {
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconDashboard, IconDashboard,
IconKey,
IconSettings,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import type { User } from "generated/prisma"; import type { User } from "generated/prisma";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
@@ -43,13 +45,12 @@ function Logout() {
color="red" color="red"
size="xs" size="xs"
onClick={async () => { onClick={async () => {
modals.openConfirmModal({ modals.openConfirmModal({
title: "Confirm Logout", title: "Confirm Logout",
children: "Are you sure you want to logout?", children: "Are you sure you want to logout?",
labels: { confirm: "Logout", cancel: "Cancel" }, labels: { confirm: "Logout", cancel: "Cancel" },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onCancel: () => { }, onCancel: () => {},
onConfirm: async () => { onConfirm: async () => {
await apiFetch.auth.logout.delete(); await apiFetch.auth.logout.delete();
localStorage.removeItem("token"); localStorage.removeItem("token");
@@ -205,11 +206,19 @@ function NavigationDashboard() {
<NavLink <NavLink
active={isActive("/dashboard/apikey/apikey")} active={isActive("/dashboard/apikey/apikey")}
leftSection={<IconDashboard size={18} />} leftSection={<IconKey size={18} />}
label="API Keys" label="API Keys"
description="Manage your API credentials" description="Manage your API credentials"
onClick={() => navigate(clientRoutes["/dashboard/apikey/apikey"])} onClick={() => navigate(clientRoutes["/dashboard/apikey/apikey"])}
/> />
<NavLink
active={isActive("/dashboard/config/config")}
leftSection={<IconSettings size={18} />}
label="Config"
description="Manage your app config"
onClick={() => navigate(clientRoutes["/dashboard/config/config"])}
/>
</Stack> </Stack>
); );
} }

View File

@@ -8,7 +8,7 @@ import {
Stack, Stack,
Table, Table,
Text, Text,
Title Title,
} from "@mantine/core"; } from "@mantine/core";
export default function Dashboard() { export default function Dashboard() {

View File

@@ -1,5 +1,5 @@
export type AppRoute = "/login" | "/" | "/register" | "/dashboard" | "/dashboard/apikey/apikey" | "/dashboard/dashboard"; export type AppRoute = "/login" | "/" | "/register" | "/dashboard" | "/dashboard/config" | "/dashboard/config/config" | "/dashboard/apikey/apikey" | "/dashboard/dashboard";
export function route(path: AppRoute, params?: Record<string,string|number>) { export function route(path: AppRoute, params?: Record<string,string|number>) {
if (!params) return path; if (!params) return path;

View File

@@ -0,0 +1,26 @@
import Elysia, { t } from "elysia";
import { prisma } from "@/server/lib/prisma";
const Configs = new Elysia({
prefix: "/configs",
detail: { description: "Configs API", summary: "Configs API", tags: ["configs"] },
})
.post("/update-allow-register", async ({ body }) => {
const { allowRegister } = body
await prisma.configs.update({
where: { id: "1" },
data: { allowRegister },
})
return { success: true, message: "Configs updated successfully", allowRegister }
}, {
body: t.Object({
allowRegister: t.Boolean(),
}),
detail: {
description: "Update configs",
summary: "update configs",
},
})
export default Configs