tambahan
This commit is contained in:
@@ -59,6 +59,11 @@ const Home = {
|
|||||||
preload: () => import("./pages/Home"),
|
preload: () => import("./pages/Home"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Register = {
|
||||||
|
Component: React.lazy(() => import("./pages/Register")),
|
||||||
|
preload: () => import("./pages/Register"),
|
||||||
|
};
|
||||||
|
|
||||||
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"),
|
||||||
@@ -101,6 +106,15 @@ export default function AppRoutes() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/register"
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={<SkeletonLoading />}>
|
||||||
|
<Register.Component />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route path="/dashboard" element={<DashboardLayout.Component />}>
|
<Route path="/dashboard" element={<DashboardLayout.Component />}>
|
||||||
<Route index element={<DashboardPage.Component />} />
|
<Route index element={<DashboardPage.Component />} />
|
||||||
|
|
||||||
|
|||||||
@@ -330,10 +330,20 @@ export function LandingPage() {
|
|||||||
<div className="nav-content">
|
<div className="nav-content">
|
||||||
<div className="logo">NexaFlow</div>
|
<div className="logo">NexaFlow</div>
|
||||||
<ul className="nav-links">
|
<ul className="nav-links">
|
||||||
<li><a href="#features">Features</a></li>
|
<li>
|
||||||
<li><a href="#about">About</a></li>
|
<a href="#features">Features</a>
|
||||||
<li><a href="#contact">Contact</a></li>
|
</li>
|
||||||
<li><a href={clientRoutes["/dashboard"]} className="cta-nav">Get Started</a></li>
|
<li>
|
||||||
|
<a href="#about">About</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={clientRoutes["/dashboard"]} className="cta-nav">
|
||||||
|
Get Started
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,10 +352,17 @@ export function LandingPage() {
|
|||||||
<section className="hero">
|
<section className="hero">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h1>Transform Your Workflow with AI</h1>
|
<h1>Transform Your Workflow with AI</h1>
|
||||||
<p>Powerful automation and intelligent insights to boost your productivity and streamline operations</p>
|
<p>
|
||||||
|
Powerful automation and intelligent insights to boost your
|
||||||
|
productivity and streamline operations
|
||||||
|
</p>
|
||||||
<div className="hero-buttons">
|
<div className="hero-buttons">
|
||||||
<a href="#" className="btn btn-primary">Start Free Trial</a>
|
<a href="#" className="btn btn-primary">
|
||||||
<a href="#" className="btn btn-secondary">Watch Demo</a>
|
Start Free Trial
|
||||||
|
</a>
|
||||||
|
<a href="#" className="btn btn-secondary">
|
||||||
|
Watch Demo
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -357,17 +374,26 @@ export function LandingPage() {
|
|||||||
<div className="feature-card">
|
<div className="feature-card">
|
||||||
<div className="feature-icon">⚡</div>
|
<div className="feature-icon">⚡</div>
|
||||||
<h3>Lightning Fast</h3>
|
<h3>Lightning Fast</h3>
|
||||||
<p>Experience blazing fast performance with our optimized infrastructure and cutting-edge technology</p>
|
<p>
|
||||||
|
Experience blazing fast performance with our optimized
|
||||||
|
infrastructure and cutting-edge technology
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="feature-card">
|
<div className="feature-card">
|
||||||
<div className="feature-icon">🔒</div>
|
<div className="feature-icon">🔒</div>
|
||||||
<h3>Secure & Reliable</h3>
|
<h3>Secure & Reliable</h3>
|
||||||
<p>Enterprise-grade security with 99.9% uptime guarantee to keep your data safe and accessible</p>
|
<p>
|
||||||
|
Enterprise-grade security with 99.9% uptime guarantee to keep
|
||||||
|
your data safe and accessible
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="feature-card">
|
<div className="feature-card">
|
||||||
<div className="feature-icon">🎯</div>
|
<div className="feature-icon">🎯</div>
|
||||||
<h3>Smart Analytics</h3>
|
<h3>Smart Analytics</h3>
|
||||||
<p>Gain actionable insights with AI-powered analytics and make data-driven decisions effortlessly</p>
|
<p>
|
||||||
|
Gain actionable insights with AI-powered analytics and make
|
||||||
|
data-driven decisions effortlessly
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +431,9 @@ export function LandingPage() {
|
|||||||
<a href="#">in</a>
|
<a href="#">in</a>
|
||||||
<a href="#">f</a>
|
<a href="#">f</a>
|
||||||
</div>
|
</div>
|
||||||
<p style={{marginTop: '30px', fontSize: '14px'}}>© 2025 NexaFlow. All rights reserved.</p>
|
<p style={{ marginTop: "30px", fontSize: "14px" }}>
|
||||||
|
© 2025 NexaFlow. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
const clientRoutes = {
|
const clientRoutes = {
|
||||||
"/login": "/login",
|
"/login": "/login",
|
||||||
"/": "/",
|
"/": "/",
|
||||||
|
"/register": "/register",
|
||||||
"/dashboard": "/dashboard",
|
"/dashboard": "/dashboard",
|
||||||
"/dashboard/apikey/apikey": "/dashboard/apikey/apikey",
|
"/dashboard/apikey/apikey": "/dashboard/apikey/apikey",
|
||||||
"/dashboard/dashboard": "/dashboard/dashboard",
|
"/dashboard/dashboard": "/dashboard/dashboard",
|
||||||
|
|||||||
110
src/index.tsx
110
src/index.tsx
@@ -8,57 +8,109 @@ import type { User } from "generated/prisma";
|
|||||||
import { LandingPage } from "./Landing";
|
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";
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
const Docs = new Elysia().use(
|
const Docs = new Elysia().use(
|
||||||
Swagger({
|
Swagger({
|
||||||
path: "/docs",
|
path: "/docs",
|
||||||
|
specPath: "/spec",
|
||||||
|
exclude: ["/docs", "/spec"],
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: packageJson.name,
|
||||||
|
version: packageJson.version,
|
||||||
|
description: "API documentation for " + packageJson.name,
|
||||||
|
contact: {
|
||||||
|
name: "Malik Kurosaki",
|
||||||
|
email: "kurosakiblackangel@gmail.com",
|
||||||
|
url: "https://github.com/malikkurosaki",
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
name: "MIT",
|
||||||
|
url:
|
||||||
|
"https://github.com/malikkurosaki/" +
|
||||||
|
packageJson.name +
|
||||||
|
"/blob/main/LICENSE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: process.env.BASE_URL || "http://localhost:3000",
|
||||||
|
description: process.env.BASE_URL
|
||||||
|
? "Production server"
|
||||||
|
: "Local development server",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ApiUser = new Elysia({
|
const ApiUser = new Elysia({
|
||||||
prefix: "/user",
|
prefix: "/user",
|
||||||
}).get("/find", (ctx) => {
|
}).get(
|
||||||
const { user } = ctx as any;
|
"/find",
|
||||||
return {
|
(ctx) => {
|
||||||
user: user as User,
|
const { user } = ctx as any;
|
||||||
};
|
return {
|
||||||
});
|
user: user as User,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
description: "Get the current user information",
|
||||||
|
summary: "Retrieve authenticated user details",
|
||||||
|
tags: ["User"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const Api = new Elysia({
|
const Api = new Elysia({
|
||||||
prefix: "/api",
|
prefix: "/api",
|
||||||
})
|
})
|
||||||
|
|
||||||
.use(apiAuth)
|
.use(apiAuth)
|
||||||
.use(ApiKeyRoute)
|
.use(ApiKeyRoute)
|
||||||
.use(ApiUser);
|
.use(ApiUser);
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(
|
.use(cors())
|
||||||
cors({
|
|
||||||
origin: "*",
|
|
||||||
methods: ["GET", "POST", "OPTIONS"],
|
|
||||||
allowedHeaders: ["Content-Type"],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.use(Api)
|
.use(Api)
|
||||||
.use(Docs)
|
.use(Docs)
|
||||||
.use(Auth)
|
.use(Auth)
|
||||||
.get("/", async () => {
|
.get(
|
||||||
const stream = await renderToReadableStream(<LandingPage />);
|
"/assets/:name",
|
||||||
return new Response(stream, {
|
(ctx) => {
|
||||||
headers: { "Content-Type": "text/html" },
|
try {
|
||||||
});
|
const file = Bun.file(`public/${encodeURIComponent(ctx.params.name)}`);
|
||||||
})
|
return new Response(file);
|
||||||
.get("/assets/:name", (ctx) => {
|
} catch (error) {
|
||||||
try {
|
return new Response("File not found", { status: 404 });
|
||||||
const file = Bun.file(`public/${encodeURIComponent(ctx.params.name)}`);
|
}
|
||||||
return new Response(file);
|
},
|
||||||
} catch (error) {
|
{
|
||||||
return new Response("File not found", { status: 404 });
|
detail: {
|
||||||
}
|
description: "Serve static asset files",
|
||||||
})
|
summary: "Get a static asset by name",
|
||||||
|
tags: ["Static Assets"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/",
|
||||||
|
async () => {
|
||||||
|
const stream = await renderToReadableStream(<LandingPage />);
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: { "Content-Type": "text/html" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
description: "Landing page for " + packageJson.name,
|
||||||
|
summary: "Get the main landing page",
|
||||||
|
tags: ["General"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
.get("/*", html)
|
.get("/*", html)
|
||||||
.listen(PORT, () => {
|
.listen(PORT, () => {
|
||||||
console.log(`Server running at http://localhost:${PORT}`);
|
console.log(`Server running at http://localhost:${PORT}`);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import clientRoutes from "@/clientRoutes";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -10,9 +11,8 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import apiFetch from "../lib/apiFetch";
|
|
||||||
import clientRoutes from "@/clientRoutes";
|
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
|
import apiFetch from "../lib/apiFetch";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
@@ -58,7 +58,8 @@ export default function Login() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isAuthenticated === null) return null;
|
if (isAuthenticated === null) return null;
|
||||||
if (isAuthenticated) return <Navigate to={clientRoutes["/dashboard"]} replace />;
|
if (isAuthenticated)
|
||||||
|
return <Navigate to={clientRoutes["/dashboard"]} replace />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size={420} py={80}>
|
<Container size={420} py={80}>
|
||||||
@@ -89,6 +90,9 @@ export default function Login() {
|
|||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" size="sm">
|
||||||
|
Don't have an account? <a href="/register">Register</a>
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { Container, Text, Anchor } from "@mantine/core";
|
import { Anchor, Container, Text } from "@mantine/core";
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Text size="xl" ta="center" mb="md">404 Not Found</Text>
|
<Text size="xl" ta="center" mb="md">
|
||||||
<Text ta="center" mb="lg">The page you are looking for does not exist.</Text>
|
404 Not Found
|
||||||
|
</Text>
|
||||||
|
<Text ta="center" mb="lg">
|
||||||
|
The page you are looking for does not exist.
|
||||||
|
</Text>
|
||||||
<Text ta="center">
|
<Text ta="center">
|
||||||
<Anchor href="/" c="blue" underline="hover">Go back home</Anchor>
|
<Anchor href="/" c="blue" underline="hover">
|
||||||
|
Go back home
|
||||||
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
108
src/pages/Register.tsx
Normal file
108
src/pages/Register.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import clientRoutes from "@/clientRoutes";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import apiFetch from "../lib/apiFetch";
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiFetch.auth.register.post({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
window.location.href = clientRoutes["/login"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
alert(JSON.stringify(response.error));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function checkSession() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch.api.user.find.get();
|
||||||
|
setIsAuthenticated(res.status === 200);
|
||||||
|
} catch {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isAuthenticated === null) return null;
|
||||||
|
if (isAuthenticated)
|
||||||
|
return <Navigate to={clientRoutes["/dashboard"]} replace />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={420} py={80}>
|
||||||
|
<Card shadow="sm" radius="md" padding="xl">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Title order={2} ta="center">
|
||||||
|
Register
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
placeholder="Your full name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="********"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="sm">
|
||||||
|
<Button onClick={handleSubmit} loading={loading} fullWidth>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" size="sm">
|
||||||
|
Already have an account? <a href="/login">Login</a>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Container,
|
Container,
|
||||||
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Divider,
|
|
||||||
Loader,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import apiFetch from "@/lib/apiFetch";
|
|
||||||
import { showNotification } from "@mantine/notifications";
|
|
||||||
import useSwr from "swr";
|
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSwr from "swr";
|
||||||
|
|
||||||
export default function ApiKeyPage() {
|
export default function ApiKeyPage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,14 +24,15 @@ import {
|
|||||||
IconDashboard,
|
IconDashboard,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { User } from "generated/prisma";
|
import type { User } from "generated/prisma";
|
||||||
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
default as clientRoute,
|
default as clientRoute,
|
||||||
default as clientRoutes,
|
default as clientRoutes,
|
||||||
} from "@/clientRoutes";
|
} from "@/clientRoutes";
|
||||||
import apiFetch from "@/lib/apiFetch";
|
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
|
||||||
/* ----------------------- Logout ----------------------- */
|
/* ----------------------- Logout ----------------------- */
|
||||||
function Logout() {
|
function Logout() {
|
||||||
@@ -42,9 +43,19 @@ function Logout() {
|
|||||||
color="red"
|
color="red"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await apiFetch.auth.logout.delete();
|
|
||||||
localStorage.removeItem("token");
|
modals.openConfirmModal({
|
||||||
window.location.href = "/login";
|
title: "Confirm Logout",
|
||||||
|
children: "Are you sure you want to logout?",
|
||||||
|
labels: { confirm: "Logout", cancel: "Cancel" },
|
||||||
|
confirmProps: { color: "red" },
|
||||||
|
onCancel: () => { },
|
||||||
|
onConfirm: async () => {
|
||||||
|
await apiFetch.auth.logout.delete();
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.href = "/login";
|
||||||
|
},
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
AppShell,
|
|
||||||
Group,
|
|
||||||
Text,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
SimpleGrid,
|
|
||||||
Table,
|
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
Avatar,
|
|
||||||
Divider,
|
|
||||||
Container,
|
Container,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
export type AppRoute = "/login" | "/" | "/dashboard" | "/dashboard/apikey/apikey" | "/dashboard/dashboard";
|
export type AppRoute = "/login" | "/" | "/register" | "/dashboard" | "/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;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function issueToken({
|
|||||||
cookie.token?.set({
|
cookie.token?.set({
|
||||||
value: token,
|
value: token,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: isProd, // aktifkan hanya di production (HTTPS)
|
secure: isProd,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
maxAge: NINETY_YEARS,
|
maxAge: NINETY_YEARS,
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -60,6 +60,58 @@ async function issueToken({
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------
|
||||||
|
REGISTER FUNCTION
|
||||||
|
-------------------------*/
|
||||||
|
async function register({
|
||||||
|
body,
|
||||||
|
cookie,
|
||||||
|
set,
|
||||||
|
jwt,
|
||||||
|
}: {
|
||||||
|
body: { name: string; email: string; password: string }
|
||||||
|
cookie: COOKIE
|
||||||
|
set: SET
|
||||||
|
jwt: JWT
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const { name, email, password } = body
|
||||||
|
|
||||||
|
// cek existing user
|
||||||
|
const existing = await prisma.user.findUnique({ where: { email } })
|
||||||
|
if (existing) {
|
||||||
|
set.status = 400
|
||||||
|
return { message: 'Email already registered' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// create user
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password, // plaintext – bisa ditambah hash
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "User registered successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error registering user:', err)
|
||||||
|
set.status = 500
|
||||||
|
return {
|
||||||
|
message: 'Register failed',
|
||||||
|
error:
|
||||||
|
err instanceof Error ? err.message : JSON.stringify(err ?? null),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------
|
||||||
|
LOGIN FUNCTION
|
||||||
|
-------------------------*/
|
||||||
async function login({
|
async function login({
|
||||||
body,
|
body,
|
||||||
cookie,
|
cookie,
|
||||||
@@ -106,6 +158,9 @@ async function login({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -----------------------
|
||||||
|
AUTH ROUTES
|
||||||
|
-------------------------*/
|
||||||
const Auth = new Elysia({
|
const Auth = new Elysia({
|
||||||
prefix: '/auth',
|
prefix: '/auth',
|
||||||
detail: { description: 'Auth API', summary: 'Auth API', tags: ['auth'] },
|
detail: { description: 'Auth API', summary: 'Auth API', tags: ['auth'] },
|
||||||
@@ -116,6 +171,32 @@ const Auth = new Elysia({
|
|||||||
secret,
|
secret,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* REGISTER */
|
||||||
|
.post(
|
||||||
|
'/register',
|
||||||
|
async ({ jwt, body, cookie, set }) => {
|
||||||
|
return await register({
|
||||||
|
jwt: jwt as JWT,
|
||||||
|
body,
|
||||||
|
cookie: cookie as any,
|
||||||
|
set: set as any,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
email: t.String(),
|
||||||
|
password: t.String(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
description: 'Register new account',
|
||||||
|
summary: 'register',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* LOGIN */
|
||||||
.post(
|
.post(
|
||||||
'/login',
|
'/login',
|
||||||
async ({ jwt, body, cookie, set }) => {
|
async ({ jwt, body, cookie, set }) => {
|
||||||
@@ -132,11 +213,13 @@ const Auth = new Elysia({
|
|||||||
password: t.String(),
|
password: t.String(),
|
||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
description: 'Login with phone; auto-register if not found',
|
description: 'Login with email + password',
|
||||||
summary: 'login',
|
summary: 'login',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* LOGOUT */
|
||||||
.delete(
|
.delete(
|
||||||
'/logout',
|
'/logout',
|
||||||
({ cookie }) => {
|
({ cookie }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user