tambahan
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const clientRoutes = {
|
||||
"/login": "/login",
|
||||
"/": "/",
|
||||
"/register": "/register",
|
||||
"/dashboard": "/dashboard",
|
||||
"/dashboard/apikey/apikey": "/dashboard/apikey/apikey",
|
||||
"/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 { 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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
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 {
|
||||
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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user