tambahan
This commit is contained in:
176
xx.ts
176
xx.ts
@@ -1,127 +1,65 @@
|
|||||||
import { readdirSync, statSync, writeFileSync } from "fs";
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import _ from "lodash";
|
import { Elysia } from 'elysia'
|
||||||
import { basename, extname, join, relative } from "path";
|
import jwt, { type JWTPayloadSpec } from '@elysiajs/jwt'
|
||||||
|
import bearer from '@elysiajs/bearer'
|
||||||
|
import { prisma } from '../lib/prisma'
|
||||||
|
|
||||||
const PAGES_DIR = join(process.cwd(), "src/pages");
|
// =========================================================
|
||||||
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
|
// JWT Secret Validation
|
||||||
|
// =========================================================
|
||||||
|
const secret = process.env.JWT_SECRET
|
||||||
|
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||||
|
|
||||||
// 🧩 Ubah nama file ke nama komponen (PascalCase)
|
// =========================================================
|
||||||
const toComponentName = (fileName: string) =>
|
// Auth Middleware Plugin
|
||||||
fileName
|
// =========================================================
|
||||||
.replace(/_/g, " ")
|
export default function apiAuth(app: Elysia) {
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||||
.replace(/\s/g, "");
|
return app
|
||||||
|
// Register Bearer and JWT plugins
|
||||||
|
.use(bearer()) // ✅ Extracts Bearer token automatically (case-insensitive)
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'jwt',
|
||||||
|
secret,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// 🧩 Ubah nama file ke path route
|
// Derive user from JWT or cookie
|
||||||
function toRoutePath(name: string): string {
|
.derive(async ({ bearer, cookie, jwt }) => {
|
||||||
if (name.toLowerCase() === "home") return "/";
|
// Normalize token type to string or undefined
|
||||||
if (name.toLowerCase() === "login") return "/login";
|
const token =
|
||||||
if (name.toLowerCase() === "notfound") return "/*";
|
(typeof bearer === 'string' ? bearer : undefined) ??
|
||||||
if (name.endsWith("_page")) return name.replace("_page", "").toLowerCase();
|
(typeof cookie?.token?.value === 'string' ? cookie.token.value : undefined)
|
||||||
if (name.startsWith("form_")) return name.replace("form_", "").toLowerCase();
|
|
||||||
return name.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧭 Scan folder pages secara rekursif
|
let user: Awaited<ReturnType<typeof prisma.user.findUnique>> | null = null
|
||||||
function scan(dir: string): any[] {
|
|
||||||
const items = readdirSync(dir);
|
|
||||||
const routes: any[] = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
if (token) {
|
||||||
const full = join(dir, item);
|
try {
|
||||||
const stat = statSync(full);
|
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (decoded?.sub && typeof decoded.sub === 'string') {
|
||||||
routes.push({
|
user = await prisma.user.findUnique({
|
||||||
name: item,
|
where: { id: decoded.sub },
|
||||||
path: item.toLowerCase(),
|
})
|
||||||
children: scan(full),
|
}
|
||||||
});
|
} catch (err) {
|
||||||
} else if (extname(item) === ".tsx") {
|
console.warn('[SERVER][apiAuth] Invalid token:', (err as Error).message)
|
||||||
routes.push({
|
|
||||||
name: basename(item, ".tsx"),
|
|
||||||
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return routes;
|
|
||||||
|
return { user }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Protect all routes by default
|
||||||
|
.onBeforeHandle(({ user, set, request }) => {
|
||||||
|
// Whitelist public routes if needed
|
||||||
|
const publicPaths = ['/auth/login', '/auth/register', '/public']
|
||||||
|
if (publicPaths.some((path) => request.url.includes(path))) return
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
set.status = 401
|
||||||
|
return { error: 'Unauthorized' }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🏗️ Generate <Route> JSX dari struktur folder
|
|
||||||
function generateJSX(routes: any[], parentPath = ""): string {
|
|
||||||
let jsx = "";
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.children) {
|
|
||||||
// cari layout di folder
|
|
||||||
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
|
|
||||||
if (layout) {
|
|
||||||
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
|
|
||||||
const nested = route.children.filter((r: any) => r !== layout);
|
|
||||||
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
|
|
||||||
jsx += `
|
|
||||||
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
|
|
||||||
${nestedRoutes}
|
|
||||||
</Route>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const Component = toComponentName(route.name);
|
|
||||||
const routePath = toRoutePath(route.name);
|
|
||||||
|
|
||||||
// Hapus duplikasi segmen
|
|
||||||
const fullPath =
|
|
||||||
routePath.startsWith("/")
|
|
||||||
? routePath
|
|
||||||
: `${parentPath}/${_.kebabCase(routePath)}`.replace(/\/+/g, "/");
|
|
||||||
|
|
||||||
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return jsx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧾 Generate import otomatis
|
|
||||||
function generateImports(routes: any[]): string {
|
|
||||||
const imports = new Set<string>();
|
|
||||||
|
|
||||||
function collect(rs: any[]) {
|
|
||||||
for (const r of rs) {
|
|
||||||
if (r.children) collect(r.children);
|
|
||||||
else {
|
|
||||||
const Comp = toComponentName(r.name);
|
|
||||||
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collect(routes);
|
|
||||||
return Array.from(imports).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧠 Main generator
|
|
||||||
const allRoutes = scan(PAGES_DIR);
|
|
||||||
const imports = generateImports(allRoutes);
|
|
||||||
const jsxRoutes = generateJSX(allRoutes);
|
|
||||||
|
|
||||||
const finalCode = `
|
|
||||||
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
|
||||||
${imports}
|
|
||||||
|
|
||||||
export default function AppRoutes() {
|
|
||||||
return (
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
${jsxRoutes}
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
writeFileSync(OUTPUT_FILE, finalCode);
|
|
||||||
console.log("✅ Routes generated successfully → src/AppRoutes.generated.tsx");
|
|
||||||
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"])
|
|
||||||
|
|||||||
Reference in New Issue
Block a user