From 4583897684ddb2102096509d45d9b1c08a05a8b6 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Tue, 25 Nov 2025 14:31:10 +0800 Subject: [PATCH] tambahan --- src/AppRoutes.tsx | 14 +++ src/Landing.tsx | 52 +++++++--- src/clientRoutes.ts | 1 + src/index.tsx | 110 +++++++++++++++------ src/pages/Login.tsx | 10 +- src/pages/NotFound.tsx | 14 ++- src/pages/Register.tsx | 108 ++++++++++++++++++++ src/pages/dashboard/apikey/apikey_page.tsx | 12 +-- src/pages/dashboard/dashboard_layout.tsx | 21 +++- src/pages/dashboard/dashboard_page.tsx | 16 ++- src/routeTypes.ts | 2 +- src/server/routes/auth_route.ts | 89 ++++++++++++++++- 12 files changed, 377 insertions(+), 72 deletions(-) create mode 100644 src/pages/Register.tsx diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index 19d5e38..acbc003 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -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() { } /> + }> + + + } + /> + }> } /> diff --git a/src/Landing.tsx b/src/Landing.tsx index 39014d3..91318ca 100644 --- a/src/Landing.tsx +++ b/src/Landing.tsx @@ -330,10 +330,20 @@ export function LandingPage() {
NexaFlow
@@ -342,10 +352,17 @@ export function LandingPage() {

Transform Your Workflow with AI

-

Powerful automation and intelligent insights to boost your productivity and streamline operations

+

+ Powerful automation and intelligent insights to boost your + productivity and streamline operations +

@@ -357,17 +374,26 @@ export function LandingPage() {

Lightning Fast

-

Experience blazing fast performance with our optimized infrastructure and cutting-edge technology

+

+ Experience blazing fast performance with our optimized + infrastructure and cutting-edge technology +

🔒

Secure & Reliable

-

Enterprise-grade security with 99.9% uptime guarantee to keep your data safe and accessible

+

+ Enterprise-grade security with 99.9% uptime guarantee to keep + your data safe and accessible +

🎯

Smart Analytics

-

Gain actionable insights with AI-powered analytics and make data-driven decisions effortlessly

+

+ Gain actionable insights with AI-powered analytics and make + data-driven decisions effortlessly +

@@ -405,10 +431,12 @@ export function LandingPage() { in f -

© 2025 NexaFlow. All rights reserved.

+

+ © 2025 NexaFlow. All rights reserved. +

); -} \ No newline at end of file +} diff --git a/src/clientRoutes.ts b/src/clientRoutes.ts index e678d42..f5145b0 100644 --- a/src/clientRoutes.ts +++ b/src/clientRoutes.ts @@ -2,6 +2,7 @@ const clientRoutes = { "/login": "/login", "/": "/", + "/register": "/register", "/dashboard": "/dashboard", "/dashboard/apikey/apikey": "/dashboard/apikey/apikey", "/dashboard/dashboard": "/dashboard/dashboard", diff --git a/src/index.tsx b/src/index.tsx index 6e003d2..04026f2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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(); - 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(); + 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}`); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 4bdbbda..2befba7 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -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 ; + if (isAuthenticated) + return ; return ( @@ -89,6 +90,9 @@ export default function Login() { Login + + Don't have an account? Register + diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 64799a4..8ee61a4 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,12 +1,18 @@ -import { Container, Text, Anchor } from "@mantine/core"; +import { Anchor, Container, Text } from "@mantine/core"; export default function NotFound() { return ( - 404 Not Found - The page you are looking for does not exist. + + 404 Not Found + + + The page you are looking for does not exist. + - Go back home + + Go back home + ); diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx new file mode 100644 index 0000000..f3acd80 --- /dev/null +++ b/src/pages/Register.tsx @@ -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(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 ; + + return ( + + + + + Register + + + setName(e.target.value)} + required + /> + + setEmail(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + /> + + + + + + Already have an account? Login + + + + + ); +} diff --git a/src/pages/dashboard/apikey/apikey_page.tsx b/src/pages/dashboard/apikey/apikey_page.tsx index e38e01a..5d4056f 100644 --- a/src/pages/dashboard/apikey/apikey_page.tsx +++ b/src/pages/dashboard/apikey/apikey_page.tsx @@ -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 ( diff --git a/src/pages/dashboard/dashboard_layout.tsx b/src/pages/dashboard/dashboard_layout.tsx index 0227547..a65efb8 100644 --- a/src/pages/dashboard/dashboard_layout.tsx +++ b/src/pages/dashboard/dashboard_layout.tsx @@ -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 diff --git a/src/pages/dashboard/dashboard_page.tsx b/src/pages/dashboard/dashboard_page.tsx index 6d46d10..202b7fe 100644 --- a/src/pages/dashboard/dashboard_page.tsx +++ b/src/pages/dashboard/dashboard_page.tsx @@ -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() { diff --git a/src/routeTypes.ts b/src/routeTypes.ts index cfe43c7..2c8934f 100644 --- a/src/routeTypes.ts +++ b/src/routeTypes.ts @@ -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) { if (!params) return path; diff --git a/src/server/routes/auth_route.ts b/src/server/routes/auth_route.ts index 7cb4ae1..e93a2bb 100644 --- a/src/server/routes/auth_route.ts +++ b/src/server/routes/auth_route.ts @@ -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 \ No newline at end of file +export default Auth