From 1f71d34d97bcecc831f797eaf2180e834eab40c5 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Tue, 7 Oct 2025 16:26:23 +0800 Subject: [PATCH] feat: add app-create and frp commands --- bin/g3n.ts | 1 + bin/src/app-create.ts | 170 +++++++++++++++++++++++++++++++++++++----- bin/src/frp.ts | 22 ++++-- 3 files changed, 169 insertions(+), 24 deletions(-) diff --git a/bin/g3n.ts b/bin/g3n.ts index ee94def..bbc114a 100755 --- a/bin/g3n.ts +++ b/bin/g3n.ts @@ -71,6 +71,7 @@ if (!(await Bun.file(g3nConf).exists())) { FRP_HOST= FRP_USER= FRP_SECRET= + FRP_AUTH_TOKEN= ` Bun.write(g3nConf, conf); console.log(`✅ G3N config created at ${g3nConf}`); diff --git a/bin/src/app-create.ts b/bin/src/app-create.ts index a64c256..613b90c 100644 --- a/bin/src/app-create.ts +++ b/bin/src/app-create.ts @@ -60,32 +60,35 @@ export default function Home() { ` const serverTemplate = ` + import Elysia from "elysia"; import Swagger from "@elysiajs/swagger"; import html from "./index.html" +import Darmasaba from "./server/routes/darmasaba"; +import apiAuth from "./server/middlewares/apiAuth"; -const Docs = new Elysia({}) +const Docs = new Elysia() .use(Swagger({ path: "/docs", })) - const Api = new Elysia({ prefix: "/api", }) - .use(Docs) - .post("/hello", () => "Hello, world!") - + .use(apiAuth) + .use(Darmasaba) const app = new Elysia() .use(Api) - .get("/*", html) + .use(Docs) + .get("*", html) .listen(3000, () => { console.log("Server running at http://localhost:3000"); }); export type Server = typeof app; + ` const notFoundTemplate = ` @@ -98,6 +101,104 @@ export default function NotFound() { } ` +const prismaTemplate = ` +import { PrismaClient } from 'generated/prisma' + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined +} + +export const prisma = globalForPrisma.prisma ?? new PrismaClient() + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma +} +` + +const apiAuthTemplate = ` +//* eslint-disable @typescript-eslint/no-explicit-any */ +import jwt, { type JWTPayloadSpec } from '@elysiajs/jwt' +import Elysia from 'elysia' +import { prisma } from '../lib/prisma' + +const secret = process.env.JWT_SECRET + +export default function apiAuth(app: Elysia) { + if (!secret) { + throw new Error('JWT_SECRET is not defined') + } + return app + .use( + jwt({ + name: 'jwt', + secret, + }) + ) + .derive(async ({ cookie, headers, jwt }) => { + let token: string | undefined + + if (cookie?.token?.value) { + token = cookie.token.value as any + } + if (headers['x-token']?.startsWith('Bearer ')) { + token = (headers['x-token'] as string).slice(7) + } + if (headers['authorization']?.startsWith('Bearer ')) { + token = (headers['authorization'] as string).slice(7) + } + + let user: null | Awaited> = null + if (token) { + try { + const decoded = (await jwt.verify(token)) as JWTPayloadSpec + if (decoded.sub) { + user = await prisma.user.findUnique({ + where: { id: decoded.sub as string }, + }) + } + } catch (err) { + console.warn('[SERVER][apiAuth] Invalid token', err) + } + } + + return { user } + }) + .onBeforeHandle(({ user, set }) => { + if (!user) { + set.status = 401 + return { error: 'Unauthorized' } + } + }) +} + +` + +const envFileTemplate = (appName: string) => ` +DATABASE_URL="postgresql://bip:Production_123@localhost:5432/${appName}?schema=public" +JWT_SECRET=super_sangat_rahasia_sekali +` + +const prismaSchemaTemplate = ` +generator client { + provider = "prisma-client-js" + output = "../generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +` const cmd = (appName: string) => ` @@ -105,45 +206,78 @@ bun init --react ${appName} echo "init react" cd ${appName} echo "cd ${appName}" + +echo "install dependencies" bun add react-router-dom -echo "add react-router-dom" bun add @mantine/core @mantine/hooks -echo "add @mantine/core @mantine/hooks" bun add --dev postcss postcss-preset-mantine postcss-simple-vars -echo "add --dev postcss postcss-preset-mantine postcss-simple-vars" -bun add elysia @elysiajs/cors @elysiajs/swagger @elysiajs/eden -echo "add elysia @elysiajs/cors @elysiajs/swagger @elysiajs/eden" +bun add elysia @elysiajs/cors @elysiajs/swagger @elysiajs/eden @elysiajs/jwt prisma @prisma/client + +echo "init prisma" +bun x prisma init + +echo "generate file ..." + +echo "generate postcss.config.js" cat < postcss.config.js ${postCssTemplate} EOF -echo "postcss.config.js" + +echo "generate src/App.tsx" cat < src/App.tsx ${appTemplate} EOF -echo "src/App.tsx" +echo "generate src/AppRoutes.tsx" cat < src/AppRoutes.tsx ${appRoutesTemplate} EOF -echo "src/AppRoutes.tsx" +echo "create dir src/pages" mkdir src/pages -echo "mkdir src/pages" + +echo "generate src/pages/Home.tsx" cat < src/pages/Home.tsx ${homeTemplate} EOF -echo "src/pages/Home.tsx" +echo "generate src/index.tsx" cat < src/index.tsx ${serverTemplate} EOF -echo "src/index.tsx" +echo "generate src/pages/NotFound.tsx" cat < src/pages/NotFound.tsx ${notFoundTemplate} EOF -echo "src/pages/NotFound.tsx" +echo "create dir src/server/lib" +mkdir -p src/server/lib + +echo "generate src/server/lib/prisma.ts" +cat < src/server/lib/prisma.ts +${prismaTemplate} +EOF + +echo "create dir server/middlewares" +mkdir -p server/middlewares + +echo "generate server/middlewares/apiAuth.ts" +cat < server/middlewares/apiAuth.ts +${apiAuthTemplate} +EOF + +echo "generate .env" +cat < .env +${envFileTemplate(appName)} +EOF + +echo "generate prisma/schema.prisma" +cat < prisma/schema.prisma +${prismaSchemaTemplate} +EOF + +echo "remove src/APITester.tsx" rm src/APITester.tsx ls diff --git a/bin/src/frp.ts b/bin/src/frp.ts index 88058c5..5acd61e 100644 --- a/bin/src/frp.ts +++ b/bin/src/frp.ts @@ -11,6 +11,7 @@ interface FrpConfig { FRP_USER: string; FRP_SECRET: string; FRP_PROTO: string; + FRP_AUTH_TOKEN: string; } interface ProxyConf { @@ -30,17 +31,19 @@ interface ProxyResponse { proxies?: Proxy[]; } +const templateConfig = ` +FRP_HOST="" +FRP_USER="" +FRP_SECRET="" +FRP_AUTH_TOKEN=""`; + async function ensureConfigFile(): Promise { try { await fs.access(CONFIG_FILE); } catch { - const template = ` -FRP_HOST="" -FRP_USER="" -FRP_SECRET="" -`; + console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); - console.log(template); + console.log(templateConfig); process.exit(1); } } @@ -66,12 +69,19 @@ async function loadConfig(): Promise { conf[key] = value; } + if (!conf.FRP_HOST || !conf.FRP_USER || !conf.FRP_SECRET || !conf.FRP_AUTH_TOKEN) { + console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); + console.log(raw); + process.exit(1); + } + return { FRP_HOST: conf.FRP_HOST || "", FRP_PORT: "443", FRP_USER: conf.FRP_USER || "", FRP_SECRET: conf.FRP_SECRET || "", FRP_PROTO: "https", + FRP_AUTH_TOKEN: conf.FRP_AUTH_TOKEN || "", }; }