This commit is contained in:
bipproduction
2025-11-25 14:31:10 +08:00
parent 8d48aa8765
commit 4583897684
12 changed files with 377 additions and 72 deletions

View File

@@ -59,6 +59,11 @@ const Home = {
preload: () => import("./pages/Home"),
};
const Register = {
Component: React.lazy(() => import("./pages/Register")),
preload: () => import("./pages/Register"),
};
const ApikeyPage = {
Component: React.lazy(() => 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 index element={<DashboardPage.Component />} />

View File

@@ -330,10 +330,20 @@ export function LandingPage() {
<div className="nav-content">
<div className="logo">NexaFlow</div>
<ul className="nav-links">
<li><a href="#features">Features</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>
<li>
<a href="#features">Features</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>
</div>
</div>
@@ -342,10 +352,17 @@ export function LandingPage() {
<section className="hero">
<div className="container">
<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">
<a href="#" className="btn btn-primary">Start Free Trial</a>
<a href="#" className="btn btn-secondary">Watch Demo</a>
<a href="#" className="btn btn-primary">
Start Free Trial
</a>
<a href="#" className="btn btn-secondary">
Watch Demo
</a>
</div>
</div>
</section>
@@ -357,17 +374,26 @@ export function LandingPage() {
<div className="feature-card">
<div className="feature-icon"></div>
<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 className="feature-card">
<div className="feature-icon">🔒</div>
<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 className="feature-card">
<div className="feature-icon">🎯</div>
<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>
@@ -405,10 +431,12 @@ export function LandingPage() {
<a href="#">in</a>
<a href="#">f</a>
</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>
</footer>
</body>
</html>
);
}
}

View File

@@ -2,6 +2,7 @@
const clientRoutes = {
"/login": "/login",
"/": "/",
"/register": "/register",
"/dashboard": "/dashboard",
"/dashboard/apikey/apikey": "/dashboard/apikey/apikey",
"/dashboard/dashboard": "/dashboard/dashboard",

View File

@@ -8,57 +8,109 @@ import type { User } from "generated/prisma";
import { LandingPage } from "./Landing";
import { renderToReadableStream } from "react-dom/server";
import { cors } from "@elysiajs/cors";
import packageJson from "./../package.json";
const PORT = process.env.PORT || 3000;
const Docs = new Elysia().use(
Swagger({
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({
prefix: "/user",
}).get("/find", (ctx) => {
const { user } = ctx as any;
return {
user: user as User,
};
});
}).get(
"/find",
(ctx) => {
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({
prefix: "/api",
})
.use(apiAuth)
.use(ApiKeyRoute)
.use(ApiUser);
const app = new Elysia()
.use(
cors({
origin: "*",
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["Content-Type"],
}),
)
.use(cors())
.use(Api)
.use(Docs)
.use(Auth)
.get("/", async () => {
const stream = await renderToReadableStream(<LandingPage />);
return new Response(stream, {
headers: { "Content-Type": "text/html" },
});
})
.get("/assets/:name", (ctx) => {
try {
const file = Bun.file(`public/${encodeURIComponent(ctx.params.name)}`);
return new Response(file);
} catch (error) {
return new Response("File not found", { status: 404 });
}
})
.get(
"/assets/:name",
(ctx) => {
try {
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)
.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);

View File

@@ -1,3 +1,4 @@
import clientRoutes from "@/clientRoutes";
import {
Button,
Card,
@@ -10,9 +11,8 @@ import {
Title,
} from "@mantine/core";
import { useEffect, useState } from "react";
import apiFetch from "../lib/apiFetch";
import clientRoutes from "@/clientRoutes";
import { Navigate } from "react-router-dom";
import apiFetch from "../lib/apiFetch";
export default function Login() {
const [email, setEmail] = useState("");
@@ -58,7 +58,8 @@ export default function Login() {
}, []);
if (isAuthenticated === null) return null;
if (isAuthenticated) return <Navigate to={clientRoutes["/dashboard"]} replace />;
if (isAuthenticated)
return <Navigate to={clientRoutes["/dashboard"]} replace />;
return (
<Container size={420} py={80}>
@@ -89,6 +90,9 @@ export default function Login() {
Login
</Button>
</Group>
<Text ta="center" size="sm">
Don't have an account? <a href="/register">Register</a>
</Text>
</Stack>
</Card>
</Container>

View File

@@ -1,12 +1,18 @@
import { Container, Text, Anchor } from "@mantine/core";
import { Anchor, Container, Text } from "@mantine/core";
export default function NotFound() {
return (
<Container>
<Text size="xl" ta="center" mb="md">404 Not Found</Text>
<Text ta="center" mb="lg">The page you are looking for does not exist.</Text>
<Text size="xl" ta="center" mb="md">
404 Not Found
</Text>
<Text ta="center" mb="lg">
The page you are looking for does not exist.
</Text>
<Text ta="center">
<Anchor href="/" c="blue" underline="hover">Go back home</Anchor>
<Anchor href="/" c="blue" underline="hover">
Go back home
</Anchor>
</Text>
</Container>
);

108
src/pages/Register.tsx Normal file
View 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>
);
}

View File

@@ -1,21 +1,21 @@
import apiFetch from "@/lib/apiFetch";
import {
Button,
Card,
Container,
Divider,
Group,
Loader,
Stack,
Table,
Text,
TextInput,
Title,
Divider,
Loader,
} 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 { showNotification } from "@mantine/notifications";
import { useEffect, useState } from "react";
import useSwr from "swr";
export default function ApiKeyPage() {
return (

View File

@@ -24,14 +24,15 @@ import {
IconDashboard,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import {
default as clientRoute,
default as clientRoutes,
} from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
import ProtectedRoute from "@/components/ProtectedRoute";
import apiFetch from "@/lib/apiFetch";
import { modals } from "@mantine/modals";
/* ----------------------- Logout ----------------------- */
function Logout() {
@@ -42,9 +43,19 @@ function Logout() {
color="red"
size="xs"
onClick={async () => {
await apiFetch.auth.logout.delete();
localStorage.removeItem("token");
window.location.href = "/login";
modals.openConfirmModal({
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

View File

@@ -1,16 +1,14 @@
import {
AppShell,
Group,
Text,
Button,
Card,
SimpleGrid,
Table,
Stack,
Title,
Avatar,
Divider,
Container,
Divider,
Group,
SimpleGrid,
Stack,
Table,
Text,
Title
} from "@mantine/core";
export default function Dashboard() {

View File

@@ -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>) {
if (!params) return path;

View File

@@ -51,7 +51,7 @@ async function issueToken({
cookie.token?.set({
value: token,
httpOnly: true,
secure: isProd, // aktifkan hanya di production (HTTPS)
secure: isProd,
sameSite: 'strict',
maxAge: NINETY_YEARS,
path: '/',
@@ -60,6 +60,58 @@ async function issueToken({
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({
body,
cookie,
@@ -106,6 +158,9 @@ async function login({
}
}
/* -----------------------
AUTH ROUTES
-------------------------*/
const Auth = new Elysia({
prefix: '/auth',
detail: { description: 'Auth API', summary: 'Auth API', tags: ['auth'] },
@@ -116,6 +171,32 @@ const Auth = new Elysia({
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(
'/login',
async ({ jwt, body, cookie, set }) => {
@@ -132,11 +213,13 @@ const Auth = new Elysia({
password: t.String(),
}),
detail: {
description: 'Login with phone; auto-register if not found',
description: 'Login with email + password',
summary: 'login',
},
}
)
/* LOGOUT */
.delete(
'/logout',
({ cookie }) => {
@@ -151,4 +234,4 @@ const Auth = new Elysia({
}
)
export default Auth
export default Auth