chore: prepare for migration to OpenAPI
This commit is contained in:
@@ -18,6 +18,14 @@
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["src/routeTree.gen.ts", "dist/**", "node_modules/**"],
|
||||
"linter": { "enabled": false },
|
||||
"formatter": { "enabled": false },
|
||||
"assist": { "enabled": false }
|
||||
}
|
||||
],
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
|
||||
36
src/api/index.tsx
Normal file
36
src/api/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { swagger } from "@elysiajs/swagger";
|
||||
import Elysia from "elysia";
|
||||
import { apiMiddleware } from "../middleware/apiMiddleware";
|
||||
import { auth } from "../utils/auth";
|
||||
import { apikey } from "./apikey";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const api = new Elysia({
|
||||
prefix: "/api",
|
||||
})
|
||||
.use(cors())
|
||||
.all("/auth/*", ({ request }) => auth.handler(request))
|
||||
.get("/session", async ({ request }) => {
|
||||
const data = await auth.api.getSession({ headers: request.headers });
|
||||
return { data };
|
||||
})
|
||||
.use(apiMiddleware)
|
||||
.use(apikey);
|
||||
|
||||
if (!isProduction) {
|
||||
api.use(
|
||||
swagger({
|
||||
path: "/docs",
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Bun + React API",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export default api;
|
||||
@@ -8,12 +8,12 @@
|
||||
/** biome-ignore-all lint/suspicious/noAssignInExpressions: <explanation */
|
||||
|
||||
import { createTheme, MantineProvider } from "@mantine/core";
|
||||
import { ModalsProvider } from "@mantine/modals";
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { Inspector } from "react-dev-inspector";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import { ModalsProvider } from "@mantine/modals";
|
||||
import { VITE_PUBLIC_URL, IS_DEV } from "./utils/env";
|
||||
import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env";
|
||||
|
||||
// Create a new router instance
|
||||
export const router = createRouter({
|
||||
@@ -40,12 +40,11 @@ const elem = document.getElementById("root")!;
|
||||
const app = (
|
||||
<InspectorWrapper
|
||||
keys={["shift", "a"]}
|
||||
onClickElement={(e) => {
|
||||
if (!e.codeInfo) return;
|
||||
|
||||
const url = VITE_PUBLIC_URL;
|
||||
fetch(`${url}/__open-in-editor`, {
|
||||
|
||||
onClickElement={(e) => {
|
||||
if (!e.codeInfo) return;
|
||||
|
||||
const url = VITE_PUBLIC_URL;
|
||||
fetch(`${url}/__open-in-editor`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -182,6 +182,6 @@ code {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation: none !important;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
42
src/index.ts
42
src/index.ts
@@ -1,42 +1,13 @@
|
||||
/** biome-ignore-all lint/suspicious/noExplicitAny: penjelasannya */
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { swagger } from "@elysiajs/swagger";
|
||||
import { Elysia } from "elysia";
|
||||
import { apikey } from "./api/apikey";
|
||||
import { apiMiddleware } from "./middleware/apiMiddleware";
|
||||
import { auth } from "./utils/auth";
|
||||
import api from "./api";
|
||||
import { openInEditor } from "./utils/open-in-editor";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const api = new Elysia({
|
||||
prefix: "/api",
|
||||
})
|
||||
.use(cors())
|
||||
.all("/auth/*", ({ request }) => auth.handler(request))
|
||||
.get("/session", async ({ request }) => {
|
||||
const data = await auth.api.getSession({ headers: request.headers });
|
||||
return { data };
|
||||
})
|
||||
.use(apiMiddleware)
|
||||
.use(apikey);
|
||||
|
||||
if (!isProduction) {
|
||||
api.use(
|
||||
swagger({
|
||||
path: "/docs",
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Bun + React API",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const app = new Elysia().use(api);
|
||||
|
||||
if (!isProduction) {
|
||||
@@ -158,8 +129,11 @@ if (!isProduction) {
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 1. Try exact match in dist
|
||||
let filePath = path.join("dist", pathname === "/" ? "index.html" : pathname);
|
||||
|
||||
let filePath = path.join(
|
||||
"dist",
|
||||
pathname === "/" ? "index.html" : pathname,
|
||||
);
|
||||
|
||||
// 2. If not found and looks like an asset (has extension), try root of dist
|
||||
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
||||
@@ -191,4 +165,4 @@ console.log(
|
||||
`🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`,
|
||||
);
|
||||
|
||||
export type ApiApp = typeof app;
|
||||
export type ApiApp = typeof app;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -31,8 +30,8 @@ import {
|
||||
useNavigate,
|
||||
} from "@tanstack/react-router";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { authStore } from "../../store/auth";
|
||||
import { authClient } from "@/utils/auth-client";
|
||||
import { authStore } from "../../store/auth";
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
component: DashboardLayout,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
|
||||
import { authClient } from "@/utils/auth-client";
|
||||
import {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
@@ -13,11 +11,11 @@ import {
|
||||
Grid,
|
||||
Group,
|
||||
Paper,
|
||||
rem,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
@@ -34,6 +32,8 @@ import {
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
|
||||
import { authClient } from "@/utils/auth-client";
|
||||
import { authStore } from "../store/auth";
|
||||
|
||||
export const Route = createFileRoute("/profile")({
|
||||
@@ -62,7 +62,8 @@ function Profile() {
|
||||
size: "sm",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Apakah Anda yakin ingin keluar dari akun Anda? Anda harus masuk kembali untuk mengakses data Anda.
|
||||
Apakah Anda yakin ingin keluar dari akun Anda? Anda harus masuk
|
||||
kembali untuk mengakses data Anda.
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Keluar", cancel: "Batal" },
|
||||
@@ -78,8 +79,20 @@ function Profile() {
|
||||
}
|
||||
};
|
||||
|
||||
const InfoField = ({ icon: Icon, label, value, copyable = false, id = "" }: any) => (
|
||||
<Paper withBorder p="md" radius="md" bg="rgba(251, 240, 223, 0.03)" style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}>
|
||||
const InfoField = ({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
copyable = false,
|
||||
id = "",
|
||||
}: any) => (
|
||||
<Paper
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
bg="rgba(251, 240, 223, 0.03)"
|
||||
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
|
||||
>
|
||||
<Group wrap="nowrap" align="flex-start">
|
||||
<Box mt={3}>
|
||||
<Icon size={20} stroke={1.5} color="#f3d5a3" />
|
||||
@@ -89,18 +102,32 @@ function Profile() {
|
||||
{label}
|
||||
</Text>
|
||||
<Group gap="xs" mt={4} wrap="nowrap">
|
||||
<Text fw={500} size="sm" c="#fbf0df" truncate="end" style={{ flex: 1 }}>
|
||||
<Text
|
||||
fw={500}
|
||||
size="sm"
|
||||
c="#fbf0df"
|
||||
truncate="end"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{value || "N/A"}
|
||||
</Text>
|
||||
{copyable && value && (
|
||||
<Tooltip label={copied === id ? "Copied!" : "Salin ke papan klip"} position="top" withArrow>
|
||||
<Tooltip
|
||||
label={copied === id ? "Copied!" : "Salin ke papan klip"}
|
||||
position="top"
|
||||
withArrow
|
||||
>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color={copied === id ? "green" : "gray"}
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(value, id)}
|
||||
>
|
||||
{copied === id ? <IconCheck size={14} /> : <IconCopy size={14} />}
|
||||
{copied === id ? (
|
||||
<IconCheck size={14} />
|
||||
) : (
|
||||
<IconCopy size={14} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -116,8 +143,12 @@ function Profile() {
|
||||
{/* Header Section */}
|
||||
<Group justify="space-between" align="center">
|
||||
<Box>
|
||||
<Title order={1} c="#f3d5a3">Profil Saya</Title>
|
||||
<Text c="dimmed" size="sm">Kelola informasi akun dan pengaturan keamanan Anda</Text>
|
||||
<Title order={1} c="#f3d5a3">
|
||||
Profil Saya
|
||||
</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Kelola informasi akun dan pengaturan keamanan Anda
|
||||
</Text>
|
||||
</Box>
|
||||
<Group>
|
||||
{snap.user?.role === "admin" && (
|
||||
@@ -144,24 +175,47 @@ function Profile() {
|
||||
<Divider color="rgba(251, 240, 223, 0.1)" />
|
||||
|
||||
{/* Profile Overview Card */}
|
||||
<Card withBorder radius="lg" p={0} bg="rgba(26, 26, 26, 0.5)" style={{ overflow: "hidden" }}>
|
||||
<Box h={120} bg="linear-gradient(45deg, #2c2c2c 0%, #1a1a1a 100%)" style={{ borderBottom: "1px solid rgba(251, 240, 223, 0.1)" }} />
|
||||
<Card
|
||||
withBorder
|
||||
radius="lg"
|
||||
p={0}
|
||||
bg="rgba(26, 26, 26, 0.5)"
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
<Box
|
||||
h={120}
|
||||
bg="linear-gradient(45deg, #2c2c2c 0%, #1a1a1a 100%)"
|
||||
style={{ borderBottom: "1px solid rgba(251, 240, 223, 0.1)" }}
|
||||
/>
|
||||
<Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}>
|
||||
<Group align="flex-end" gap="xl" mb="md">
|
||||
<Avatar
|
||||
src={snap.user?.image}
|
||||
size={120}
|
||||
radius={120}
|
||||
style={{ border: "4px solid #1a1a1a", boxShadow: "0 4px 10px rgba(0,0,0,0.3)" }}
|
||||
style={{
|
||||
border: "4px solid #1a1a1a",
|
||||
boxShadow: "0 4px 10px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
{snap.user?.name?.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Stack gap={0} pb="md">
|
||||
<Title order={2} c="#fbf0df">{snap.user?.name}</Title>
|
||||
<Title order={2} c="#fbf0df">
|
||||
{snap.user?.name}
|
||||
</Title>
|
||||
<Group gap="xs">
|
||||
<Text c="dimmed" size="sm">{snap.user?.email}</Text>
|
||||
<Text c="dimmed" size="xs">•</Text>
|
||||
<Badge variant="dot" color={snap.user?.role === "admin" ? "orange" : "blue"} size="sm">
|
||||
<Text c="dimmed" size="sm">
|
||||
{snap.user?.email}
|
||||
</Text>
|
||||
<Text c="dimmed" size="xs">
|
||||
•
|
||||
</Text>
|
||||
<Badge
|
||||
variant="dot"
|
||||
color={snap.user?.role === "admin" ? "orange" : "blue"}
|
||||
size="sm"
|
||||
>
|
||||
{snap.user?.role || "user"}
|
||||
</Badge>
|
||||
</Group>
|
||||
@@ -173,19 +227,41 @@ function Profile() {
|
||||
<Grid gutter="lg">
|
||||
<Grid.Col span={{ base: 12, md: 7 }}>
|
||||
<Stack gap="md">
|
||||
<Title order={4} c="#f3d5a3">Informasi Identitas</Title>
|
||||
<Title order={4} c="#f3d5a3">
|
||||
Informasi Identitas
|
||||
</Title>
|
||||
<Grid gutter="sm">
|
||||
<Grid.Col span={6}>
|
||||
<InfoField icon={IconUser} label="Nama Lengkap" value={snap.user?.name} />
|
||||
<InfoField
|
||||
icon={IconUser}
|
||||
label="Nama Lengkap"
|
||||
value={snap.user?.name}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<InfoField icon={IconShield} label="Peran" value={snap.user?.role || "User"} />
|
||||
<InfoField
|
||||
icon={IconShield}
|
||||
label="Peran"
|
||||
value={snap.user?.role || "User"}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InfoField icon={IconAt} label="Alamat Email" value={snap.user?.email} copyable id="email" />
|
||||
<InfoField
|
||||
icon={IconAt}
|
||||
label="Alamat Email"
|
||||
value={snap.user?.email}
|
||||
copyable
|
||||
id="email"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InfoField icon={IconId} label="Unique User ID" value={snap.user?.id} copyable id="userid" />
|
||||
<InfoField
|
||||
icon={IconId}
|
||||
label="Unique User ID"
|
||||
value={snap.user?.id}
|
||||
copyable
|
||||
id="userid"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -193,40 +269,73 @@ function Profile() {
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||
<Stack gap="md">
|
||||
<Title order={4} c="#f3d5a3">Keamanan & Sesi</Title>
|
||||
<Card withBorder radius="md" p="lg" bg="rgba(251, 240, 223, 0.03)" style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}>
|
||||
<Title order={4} c="#f3d5a3">
|
||||
Keamanan & Sesi
|
||||
</Title>
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
p="lg"
|
||||
bg="rgba(251, 240, 223, 0.03)"
|
||||
style={{ border: "1px solid rgba(251, 240, 223, 0.1)" }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>Sesi Saat Ini</Text>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
|
||||
Sesi Saat Ini
|
||||
</Text>
|
||||
<Group justify="space-between" align="center">
|
||||
<Badge color="green" variant="light">Aktif Sekarang</Badge>
|
||||
<Text size="xs" c="dimmed">ID: {snap.session?.id?.substring(0, 8)}...</Text>
|
||||
<Badge color="green" variant="light">
|
||||
Aktif Sekarang
|
||||
</Badge>
|
||||
<Text size="xs" c="dimmed">
|
||||
ID: {snap.session?.id?.substring(0, 8)}...
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>Session Token</Text>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
|
||||
Session Token
|
||||
</Text>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Code block style={{
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
color: "#f3d5a3",
|
||||
border: "1px solid rgba(251, 240, 223, 0.1)",
|
||||
fontSize: rem(11),
|
||||
flex: 1
|
||||
}}>
|
||||
{snap.session?.token ? `${snap.session.token.substring(0, 32)}...` : "N/A"}
|
||||
</Code>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={() => snap.session?.token && copyToClipboard(snap.session.token, "token")}
|
||||
<Code
|
||||
block
|
||||
style={{
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
color: "#f3d5a3",
|
||||
border: "1px solid rgba(251, 240, 223, 0.1)",
|
||||
fontSize: rem(11),
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{copied === "token" ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
||||
{snap.session?.token
|
||||
? `${snap.session.token.substring(0, 32)}...`
|
||||
: "N/A"}
|
||||
</Code>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={() =>
|
||||
snap.session?.token &&
|
||||
copyToClipboard(snap.session.token, "token")
|
||||
}
|
||||
>
|
||||
{copied === "token" ? (
|
||||
<IconCheck size={16} />
|
||||
) : (
|
||||
<IconCopy size={16} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Button variant="light" color="gray" fullWidth leftSection={<IconExternalLink size={16} />}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
fullWidth
|
||||
leftSection={<IconExternalLink size={16} />}
|
||||
>
|
||||
Riwayat Sesi
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
export const getEnv = (key: string, defaultValue = ""): string => {
|
||||
// 1. Try Vite's import.meta.env
|
||||
try {
|
||||
if (typeof import.meta.env !== "undefined" && import.meta.env[key]) {
|
||||
if (import.meta.env?.[key]) {
|
||||
return import.meta.env[key];
|
||||
}
|
||||
} catch {}
|
||||
@@ -20,11 +20,14 @@ export const getEnv = (key: string, defaultValue = ""): string => {
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export const VITE_PUBLIC_URL = getEnv("VITE_PUBLIC_URL", "http://localhost:3000");
|
||||
export const VITE_PUBLIC_URL = getEnv(
|
||||
"VITE_PUBLIC_URL",
|
||||
"http://localhost:3000",
|
||||
);
|
||||
|
||||
export const IS_DEV = (() => {
|
||||
try {
|
||||
return typeof import.meta.env !== "undefined" && import.meta.env.DEV;
|
||||
return import.meta.env?.DEV;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// open-in-editor.ts
|
||||
// DEV utility: open source file in local editor
|
||||
|
||||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* Types
|
||||
|
||||
Reference in New Issue
Block a user