diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 015b810..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,150 +0,0 @@ -# CLAUDE.md - -## Project Overview - -This repository uses **Bun + Elysia** as the single HTTP server and **Vite (middleware mode)** to serve a **React** frontend on the **same port** during development. The goal is a clean DX with **one origin**, **no proxy**, **no CORS**, and support for **react-dev-inspector** (click element → open editor). - -**Key principles**: - -* Bun/Elysia owns the port (e.g. `http://localhost:3000`). -* Vite runs **as middleware**, not as a standalone dev server. -* React Dev Inspector is enabled **only in dev**. -* Production build does **not** depend on Vite middleware. - ---- - -## Stack - -* Runtime: **Bun** -* Server: **Elysia** -* Frontend: **React** -* Tooling (dev): **Vite (middleware mode)** -* Inspector: **react-dev-inspector** - ---- - -## Dev Architecture (Single Port) - -``` -Browser - ↓ -http://localhost:3000 - ↓ -Elysia (Bun) - ├─ API routes - ├─ react-dev-inspector middleware - └─ Vite middlewares (HMR, transforms) -``` - -**Why this matters**: - -* No split ports -* No proxy rewrites -* Stable source maps for inspector - ---- - -## Vite API (Important) - -> **Vite version matters.** - -* For **Vite v3–v4**: `createServer` is imported from `'vite'`. -* For **Vite v7+**: **Node APIs are exported from `vite/node`**. - -**Use this for Vite v7+**: - -```ts -import { createServer } from 'vite/node' -import type { ViteDevServer } from 'vite' -``` - -Do **not** import from internal paths like `vite/dist/*`. - ---- - -## TypeScript Requirements - -Ensure TypeScript can resolve Vite types correctly: - -```json -{ - "compilerOptions": { - "moduleResolution": "Bundler", - "types": ["bun-types", "vite/client"], - "jsx": "react-jsx" - } -} -``` - -If TypeScript cannot find `createServer`, check: - -* Vite major version -* Import path (`vite/node` for v7+) -* `types` includes `vite/client` - ---- - -## Development Flow - -1. Start Bun server (`bun run dev`). -2. Elysia boots and creates a Vite dev server in **middleware mode**. -3. Requests are handled by Elysia and passed to Vite middlewares. -4. React loads with HMR and Inspector enabled. -5. **Alt/Option + Click** on a React element opens the source file. - ---- - -## Inspector Usage - -* Shortcut: - - * macOS: **Option + Click** - * Windows/Linux: **Alt + Click** -* Editor can be configured in the Vite plugin: - -```ts -Inspector({ editor: 'code' }) // VS Code -``` - -Inspector should be **disabled in production**. - ---- - -## Production Notes - -* Vite middleware is **dev-only**. -* Production should serve: - - * Prebuilt static assets (Vite build output), or - * SSR output (if enabled later). -* Elysia remains the single server in all environments. - ---- - -## Common Pitfalls - -* ❌ Running Vite as a separate server (breaks single-port goal) -* ❌ Importing `createServer` from `'vite'` on Vite v7+ -* ❌ Using internal Vite paths (`vite/dist/*`) -* ❌ Missing `vite/client` types - ---- - -## Goals for Contributors - -* Keep **one-port architecture** intact. -* Do not introduce dev proxies unless absolutely required. -* Prefer Bun-native solutions. -* Avoid relying on undocumented Vite internals. - ---- - -## References - -* Vite JavaScript API (v3): [https://v3.vite.dev/guide/api-javascript.html](https://v3.vite.dev/guide/api-javascript.html) -* Vite latest docs: [https://vite.dev/](https://vite.dev/) -* react-dev-inspector: [https://react-dev-inspector.zthxxx.me/](https://react-dev-inspector.zthxxx.me/) - ---- - -If anything here becomes unclear, **check the Vite major version first** — most integration issues come from API changes across versions. diff --git a/GEMINI.md b/GEMINI.md index 82637e0..db0c6da 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,64 +1,59 @@ # GEMINI.md -This file provides instructional context for the Gemini AI agent to understand and interact with this project efficiently. +This project is a high-performance, full-stack React development template leveraging the Bun runtime. It is designed for a seamless developer experience with a unified "single-port" architecture. ## Project Overview -A high-performance web application template using a "Single Port" architecture. It combines a Bun/Elysia backend with a React frontend, served through Vite in middleware mode. +* **Runtime**: [Bun](https://bun.sh/) +* **Architecture**: "Single Port" (default: 3000). [ElysiaJS](https://elysiajs.com/) serves as the main HTTP server, integrating [Vite](https://vitejs.dev/) in **middleware mode** during development to provide HMR and React Dev Inspector support. +* **Frontend**: React 19 with [TanStack React Router](https://tanstack.com/router/latest) for type-safe, file-based routing. +* **UI Framework**: [Mantine UI](https://mantine.dev/) for a comprehensive component library and hooks. +* **Authentication**: [Better Auth](https://www.better-auth.com/) integrated with Elysia. +* **Database**: [Prisma ORM](https://www.prisma.io/) for type-safe database access. +* **Tooling**: [Biome](https://biomejs.dev/) for ultra-fast linting and formatting. -- **Runtime**: [Bun](https://bun.sh/) -- **Backend**: [ElysiaJS](https://elysiajs.com/) -- **Frontend**: React 19 -- **Routing**: [TanStack React Router](https://tanstack.com/router/latest) (File-based) -- **UI Framework**: [Mantine UI](https://mantine.dev/) -- **Bundler/Dev Tooling**: [Vite](https://vitejs.dev/) (Middleware Mode) -- **Linting/Formatting**: [Biome](https://biomejs.dev/) -- **Developer Experience**: Single port (3000), HMR, and [react-dev-inspector](https://github.com/zthxxx/react-dev-inspector) integration. +## Building and Running -## Architecture: Single Port (DX) +### Development +* **Install dependencies**: `bun install` +* **Start development server**: `bun run dev` (Runs Elysia + Vite Middleware) +* **Update Route Tree**: `bun x tsr generate` (usually automatic via Vite plugin) +* **Database Migration**: `bun x prisma migrate dev` -The Elysia server (running on Bun) acts as the primary entry point. During development, it bridges requests to Vite's middleware. +### Production +* **Build Frontend**: `bun run build` (Outputs to `dist/`) +* **Start Production Server**: `bun run start` (Serves pre-built assets from `dist/` via Elysia) -- **Backend**: Handles API routes (`/api/*`) and custom developer tools (e.g., `/__open-in-editor`). -- **Frontend**: All other requests are passed to Vite for HMR and asset transformation. -- **Entry Points**: - - Server: `src/index.ts` - - Vite Config: `src/vite.ts` - - Frontend: `src/frontend.tsx` - - HTML: `src/index.html` +### Quality Control +* **Lint**: `bun run lint` (Biome check) +* **Format**: `bun run format` (Biome write) +* **Type Check**: `bun x tsc --noEmit` -## Development Commands +## Development Conventions -- **Install Dependencies**: `bun install` -- **Start Dev Server**: `bun run dev` (Runs Elysia + Vite Middleware) -- **Lint & Fix**: `bun run lint` (Biome check) -- **Format Code**: `bun run format` (Biome format) -- **Type Check**: `bun x tsc --noEmit` -- **Production Build**: `bun run build` (Static build) -- **Production Start**: `bun run start` (Serve production build) +### Code Style & Structure +* **Formatting**: Strictly use **Biome**. The project uses **tab indentation** and **double quotes** for JavaScript/TypeScript. +* **Imports**: + * Use the `node:` protocol for Node.js built-ins (e.g., `import fs from "node:fs"`). + * Use the `@/` alias for absolute paths from the `src/` directory (e.g., `import { auth } from "@/utils/auth"`). +* **Routing**: New routes should be added as files in `src/routes/` to leverage TanStack Router's file-based routing system. -## Project Structure +### Backend/API +* **Prefix**: All backend API routes are prefixed with `/api`. +* **Documentation**: Swagger documentation is available at `/api/docs` in development. +* **Authentication**: Handled at `/api/auth/*`. Protected routes use the `apiMiddleware` and custom guards. -- `src/`: Main source code. - - `src/index.ts`: Elysia server entry point. - - `src/vite.ts`: Vite server configuration for middleware mode. - - `src/frontend.tsx`: React client entry point. - - `src/routes/`: TanStack Router file-based routes. - - `src/routes/__root.tsx`: Root layout with authentication guards. - - `src/routes/index.tsx`: Home page. - - `src/utils/`: Helper utilities (e.g., `open-in-editor.ts`, API clients). -- `biome.json`: Biome configuration (tabs, double quotes, import organization). -- `postcss.config.cjs`: PostCSS configuration for Mantine UI. +### Frontend +* **Theme**: Mantine is configured via `MantineProvider` in `src/App.tsx`. +* **State Management**: [Valtio](https://valtio.pmnd.rs/) is used for simple proxy-based state (see `src/store/`). +* **Dev Tools**: TanStack Router Devtools and React Dev Inspector are enabled in development. -## Coding Conventions +## Project Layout -- **Formatter/Linter**: Strictly use **Biome**. Indentation is set to **tabs**. -- **Routing**: Use TanStack Router's file-based system in `src/routes/`. Avoid manual route definitions unless necessary. -- **UI Components**: Prefer Mantine UI components. Always wrap the app with `MantineProvider`. -- **Imports**: Use the `node:` protocol for Node.js built-ins (e.g., `import fs from "node:fs"`). Biome handles import organization automatically. -- **Types**: Maintain strict TypeScript compliance. Use `tsc --noEmit` to verify. - -## Integration Details - -- **React Dev Inspector**: Active in development. Use `Alt/Option + Click` to jump from the browser to the code in your editor. -- **Elysia-Vite Bridge**: The bridge in `src/index.ts` mocks Node.js `req`/`res` objects using a `Proxy` to make Bun's fetch-based requests compatible with Vite's Connect middleware. +* `src/index.ts`: Unified server entry point (Dev/Prod conditional logic). +* `src/vite.ts`: Vite server configuration (Dev-only). +* `src/routes/`: Frontend route definitions and layouts. +* `src/api/`: Elysia route modules. +* `src/utils/`: Shared utilities (Auth, DB, Logging). +* `prisma/`: Database schema and migrations. +* `dist/`: Production build output (Git ignored). diff --git a/bun.lock b/bun.lock index 14b671c..526caa8 100644 --- a/bun.lock +++ b/bun.lock @@ -12,9 +12,9 @@ "@mantine/core": "^8.3.14", "@mantine/dates": "^8.3.13", "@mantine/hooks": "^8.3.14", + "@mantine/modals": "^8.3.14", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^6.19.2", - "@react-dev-inspector/vite-plugin": "^2.0.1", "@tabler/icons-react": "^3.36.1", "@tanstack/react-router": "^1.158.1", "better-auth": "^1.4.18", @@ -23,7 +23,6 @@ "pino": "^10.3.0", "pino-pretty": "^13.1.3", "react": "^19", - "react-dev-inspector": "^2.0.1", "react-dom": "^19", "valtio": "^2.3.0", }, @@ -31,6 +30,7 @@ "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@biomejs/biome": "2.3.14", + "@react-dev-inspector/vite-plugin": "^2.0.1", "@tanstack/react-router-devtools": "^1.158.1", "@tanstack/router-cli": "^1.157.16", "@tanstack/router-plugin": "^1.157.16", @@ -45,6 +45,7 @@ "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "prisma": "^6.19.2", + "react-dev-inspector": "^2.0.1", "vite": "^7.3.1", }, }, @@ -250,6 +251,8 @@ "@mantine/hooks": ["@mantine/hooks@8.3.14", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-0SbHnGEuHcF2QyjzBBcqidpjNmIb6n7TC3obnhkBToYhUTbMcJSK/8ei/yHtAelridJH4CPeohRlQdc0HajHyQ=="], + "@mantine/modals": ["@mantine/modals@8.3.14", "", { "peerDependencies": { "@mantine/core": "8.3.14", "@mantine/hooks": "8.3.14", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-BBM53MBq0vKZ7MKmTbqdt6i5eZEoAbfllCHVlQ7J4Xlr1LehoxO3q0MuwPr5kkjSWAPw5okiviKoMYXIKBn53w=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], @@ -750,7 +753,7 @@ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -764,7 +767,7 @@ "is-root": ["is-root@2.1.0", "", {}, "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="], @@ -1232,6 +1235,8 @@ "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "node-abi/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -1260,6 +1265,8 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "@prisma/config/c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -1284,10 +1291,6 @@ "react-dev-utils/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - "react-dev-utils/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "react-dev-utils/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "@prisma/config/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "fork-ts-checker-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], diff --git a/package.json b/package.json index 4cdc0ab..cf1c486 100644 --- a/package.json +++ b/package.json @@ -20,25 +20,26 @@ "@elysiajs/eden": "^1.4.6", "@elysiajs/swagger": "^1.3.1", "@mantine/core": "^8.3.14", + "@mantine/dates": "^8.3.13", "@mantine/hooks": "^8.3.14", - "@mantine/dates": "^8.3.13", - "@react-dev-inspector/vite-plugin": "^2.0.1", - "@tanstack/react-router": "^1.158.1", - "elysia": "^1.4.22", - "react": "^19", - "react-dev-inspector": "^2.0.1", - "react-dom": "^19", - "@prisma/adapter-pg": "^7.3.0", + "@mantine/modals": "^8.3.14", + "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^6.19.2", "@tabler/icons-react": "^3.36.1", - "better-auth": "^1.4.18", + "@tanstack/react-router": "^1.158.1", + "better-auth": "^1.4.18", "dayjs": "^1.11.19", - "pino": "^10.3.0", + "elysia": "^1.4.22", + "pino": "^10.3.0", "pino-pretty": "^13.1.3", - "valtio": "^2.3.0" + "react": "^19", + "react-dom": "^19", + "valtio": "^2.3.0" }, "devDependencies": { - "@babel/core": "^7.29.0", + "@react-dev-inspector/vite-plugin": "^2.0.1", + "react-dev-inspector": "^2.0.1", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@biomejs/biome": "2.3.14", "@tanstack/react-router-devtools": "^1.158.1", @@ -51,10 +52,10 @@ "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "vite": "^7.3.1", - "prisma": "^6.19.2", - "@tanstack/router-cli": "^1.157.16", + "prisma": "^6.19.2", + "@tanstack/router-cli": "^1.157.16", "@tanstack/router-plugin": "^1.157.16", - "concurrently": "^9.2.1", + "concurrently": "^9.2.1", "fast-glob": "^3.3.3" } } diff --git a/prisma.config.ts b/prisma.config.ts index 8ded1a5..9602954 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -1,16 +1,15 @@ - // This file was generated by Prisma and assumes you have installed the following: // npm install --save-dev prisma dotenv import "dotenv/config"; import { defineConfig, env } from "prisma/config"; export default defineConfig({ - schema: "prisma/schema.prisma", - migrations: { - path: "prisma/migrations", - }, - engine: "classic", - datasource: { - url: env("DATABASE_URL"), - }, + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + engine: "classic", + datasource: { + url: env("DATABASE_URL"), + }, }); diff --git a/prisma/seed.ts b/prisma/seed.ts index 801eaf1..e641e5a 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,49 +1,53 @@ import { prisma } from "@/utils/db"; async function seedAdminUser() { - // Load environment variables - const adminEmail = process.env.ADMIN_EMAIL; + // Load environment variables + const adminEmail = process.env.ADMIN_EMAIL; - if (!adminEmail) { - console.log("No ADMIN_EMAIL environment variable found. Skipping admin role assignment."); - return; - } + if (!adminEmail) { + console.log( + "No ADMIN_EMAIL environment variable found. Skipping admin role assignment.", + ); + return; + } - try { - // Check if admin user already exists - const existingUser = await prisma.user.findUnique({ - where: { email: adminEmail }, - }); + try { + // Check if admin user already exists + const existingUser = await prisma.user.findUnique({ + where: { email: adminEmail }, + }); - if (existingUser) { - // Update existing user to have admin role if they don't already - if (existingUser.role !== "admin") { - await prisma.user.update({ - where: { email: adminEmail }, - data: { role: "admin" }, - }); - console.log(`User with email ${adminEmail} updated to admin role.`); - } else { - console.log(`User with email ${adminEmail} already has admin role.`); - } - } else { - console.log(`No user found with email ${adminEmail}. Skipping admin role assignment.`); - } - } catch (error) { - console.error("Error seeding admin user:", error); - throw error; - } + if (existingUser) { + // Update existing user to have admin role if they don't already + if (existingUser.role !== "admin") { + await prisma.user.update({ + where: { email: adminEmail }, + data: { role: "admin" }, + }); + console.log(`User with email ${adminEmail} updated to admin role.`); + } else { + console.log(`User with email ${adminEmail} already has admin role.`); + } + } else { + console.log( + `No user found with email ${adminEmail}. Skipping admin role assignment.`, + ); + } + } catch (error) { + console.error("Error seeding admin user:", error); + throw error; + } } async function main() { - console.log("Seeding database..."); + console.log("Seeding database..."); - await seedAdminUser(); + await seedAdminUser(); - console.log("Database seeding completed."); + console.log("Database seeding completed."); } main().catch((error) => { - console.error("Error during seeding:", error); - process.exit(1); -}); \ No newline at end of file + console.error("Error during seeding:", error); + process.exit(1); +}); diff --git a/src/frontend.tsx b/src/frontend.tsx index d8a5d62..3660ea5 100644 --- a/src/frontend.tsx +++ b/src/frontend.tsx @@ -7,14 +7,12 @@ /** biome-ignore-all lint/style/noNonNullAssertion: <>{children}; + ? Inspector + : ({ children }: { children: React.ReactNode }) => <>{children}; const elem = document.getElementById("root")!; const app = ( - { - if (!e.codeInfo) return; + { + if (!e.codeInfo) return; - const url = import.meta.env.VITE_PUBLIC_URL; - fetch(`${url}/__open-in-editor`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - relativePath: e.codeInfo.relativePath, - lineNumber: e.codeInfo.lineNumber, - columnNumber: e.codeInfo.columnNumber, - }), - }); - }} - > - - - - + const url = import.meta.env.VITE_PUBLIC_URL; + fetch(`${url}/__open-in-editor`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + relativePath: e.codeInfo.relativePath, + lineNumber: e.codeInfo.lineNumber, + columnNumber: e.codeInfo.columnNumber, + }), + }); + }} + > + + + + + + ); if (import.meta.hot) { - // With hot module reloading, `import.meta.hot.data` is persisted. - const root = (import.meta.hot.data.root ??= createRoot(elem)); - root.render(app); + // With hot module reloading, `import.meta.hot.data` is persisted. + const root = (import.meta.hot.data.root ??= createRoot(elem)); + root.render(app); } else { - // The hot module reloading API is not available in production. - createRoot(elem).render(app); + // The hot module reloading API is not available in production. + createRoot(elem).render(app); } diff --git a/src/index.ts b/src/index.ts index ff0c8a3..1885690 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,15 @@ /** biome-ignore-all lint/suspicious/noExplicitAny: penjelasannya */ import fs from "node:fs"; import path from "node:path"; -import { Elysia } from "elysia"; -import { openInEditor } from "./utils/open-in-editor"; -import { createVite } from "./vite"; -import { apikey } from "./api/apikey"; -import { auth } from "./utils/auth"; 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 { openInEditor } from "./utils/open-in-editor"; +const isProduction = process.env.NODE_ENV === "production"; const api = new Elysia({ prefix: "/api", @@ -34,10 +34,14 @@ const api = new Elysia({ .use(apiMiddleware) .use(apikey); -const vite = await createVite(); -const app = new Elysia() +const app = new Elysia().use(api); - .post("/__open-in-editor", ({ body }) => { +if (!isProduction) { + // Development: Use Vite middleware + const { createVite } = await import("./vite"); + const vite = await createVite(); + + app.post("/__open-in-editor", ({ body }) => { const { relativePath, lineNumber, columnNumber } = body as { relativePath: string; lineNumber: number; @@ -51,104 +55,135 @@ const app = new Elysia() }); return { ok: true }; - }) - .use(api); + }); -// Vite middleware for other requests -app.all("*", async ({ request }) => { - const url = new URL(request.url); - const pathname = url.pathname; + // Vite middleware for other requests + app.all("*", async ({ request }) => { + const url = new URL(request.url); + const pathname = url.pathname; - // Serve transformed index.html for root or any path that should be handled by the SPA - // We check if it's not a file request (doesn't have a file extension or is a known SPA route) - if ( - pathname === "/" || - (!pathname.includes(".") && - !pathname.startsWith("/@") && - !pathname.startsWith("/inspector") && - !pathname.startsWith("/__open-stack-frame-in-editor")) - ) { - try { - const htmlPath = path.resolve("src/index.html"); - let html = fs.readFileSync(htmlPath, "utf-8"); - html = await vite.transformIndexHtml(pathname, html); + // Serve transformed index.html for root or any path that should be handled by the SPA + if ( + pathname === "/" || + (!pathname.includes(".") && + !pathname.startsWith("/@") && + !pathname.startsWith("/inspector") && + !pathname.startsWith("/__open-stack-frame-in-editor")) + ) { + try { + const htmlPath = path.resolve("src/index.html"); + let html = fs.readFileSync(htmlPath, "utf-8"); + html = await vite.transformIndexHtml(pathname, html); - return new Response(html, { - headers: { "Content-Type": "text/html" }, - }); - } catch (e) { - console.error(e); - } - } - - return new Promise((resolve) => { - // Use a Proxy to mock Node.js req because Bun's Request is read-only - const req = new Proxy(request, { - get(target, prop) { - if (prop === "url") return pathname + url.search; - if (prop === "method") return request.method; - if (prop === "headers") - return Object.fromEntries(request.headers as any); - return (target as any)[prop]; - }, - }) as any; - - const res = { - statusCode: 200, - setHeader(name: string, value: string) { - this.headers[name.toLowerCase()] = value; - }, - getHeader(name: string) { - return this.headers[name.toLowerCase()]; - }, - headers: {} as Record, - end(data: any) { - // Handle potential Buffer or string data from Vite - let body = data; - if (data instanceof Uint8Array) { - body = data; - } else if (typeof data === "string") { - body = data; - } else if (data) { - body = String(data); - } - - resolve( - new Response(body || "", { - status: this.statusCode, - headers: this.headers, - }), - ); - }, - // Minimal event emitter mock - once() { - return this; - }, - on() { - return this; - }, - emit() { - return this; - }, - removeListener() { - return this; - }, - } as any; - - vite.middlewares(req, res, (err: any) => { - if (err) { - console.error("Vite middleware error:", err); - resolve(new Response(err.stack || err.toString(), { status: 500 })); - return; + return new Response(html, { + headers: { "Content-Type": "text/html" }, + }); + } catch (e) { + console.error(e); } - // If Vite doesn't handle it, return 404 - resolve(new Response("Not Found", { status: 404 })); + } + + return new Promise((resolve) => { + // Use a Proxy to mock Node.js req because Bun's Request is read-only + const req = new Proxy(request, { + get(target, prop) { + if (prop === "url") return pathname + url.search; + if (prop === "method") return request.method; + if (prop === "headers") + return Object.fromEntries(request.headers as any); + return (target as any)[prop]; + }, + }) as any; + + const res = { + statusCode: 200, + setHeader(name: string, value: string) { + this.headers[name.toLowerCase()] = value; + }, + getHeader(name: string) { + return this.headers[name.toLowerCase()]; + }, + headers: {} as Record, + end(data: any) { + // Handle potential Buffer or string data from Vite + let body = data; + if (data instanceof Uint8Array) { + body = data; + } else if (typeof data === "string") { + body = data; + } else if (data) { + body = String(data); + } + + resolve( + new Response(body || "", { + status: this.statusCode, + headers: this.headers, + }), + ); + }, + // Minimal event emitter mock + once() { + return this; + }, + on() { + return this; + }, + emit() { + return this; + }, + removeListener() { + return this; + }, + } as any; + + vite.middlewares(req, res, (err: any) => { + if (err) { + console.error("Vite middleware error:", err); + resolve(new Response(err.stack || err.toString(), { status: 500 })); + return; + } + // If Vite doesn't handle it, return 404 + resolve(new Response("Not Found", { status: 404 })); + }); }); }); -}); +} else { + // Production: Serve static files from dist + app.get("*", async ({ request }) => { + const url = new URL(request.url); + let pathname = url.pathname; + + // Skip API routes + if (pathname.startsWith("/api")) { + return new Response("Not Found", { status: 404 }); + } + + if (pathname === "/") { + pathname = "/index.html"; + } + + const filePath = path.join("dist", pathname); + + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + const file = Bun.file(filePath); + return new Response(file); + } + + // SPA Fallback + const indexHtml = path.join("dist", "index.html"); + if (fs.existsSync(indexHtml)) { + return new Response(Bun.file(indexHtml)); + } + + return new Response("Not Found", { status: 404 }); + }); +} app.listen(3000); -console.log("🚀 Server running at http://localhost:3000"); +console.log( + `🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`, +); -export type ApiApp = typeof app; \ No newline at end of file +export type ApiApp = typeof app; diff --git a/src/middleware/authMiddleware.tsx b/src/middleware/authMiddleware.tsx index 0bc14ee..3e71e30 100644 --- a/src/middleware/authMiddleware.tsx +++ b/src/middleware/authMiddleware.tsx @@ -21,8 +21,7 @@ type SessionResponse = { async function fetchSession(): Promise { try { - const baseURL = - import.meta.env.VITE_PUBLIC_URL || window.location.origin; + const baseURL = import.meta.env.VITE_PUBLIC_URL || window.location.origin; const res = await fetch(`${baseURL}/api/session`, { method: "GET", credentials: "include", diff --git a/src/routes/dashboard/apikey.tsx b/src/routes/dashboard/apikey.tsx index f849112..30bf548 100644 --- a/src/routes/dashboard/apikey.tsx +++ b/src/routes/dashboard/apikey.tsx @@ -94,7 +94,9 @@ function DashboardApikeyComponent() { setCreating(true); const response = await apiClient.api.apikey.post({ name: newKeyName, - expiresAt: newKeyExpiresAt ? dayjs(newKeyExpiresAt).toISOString() : undefined, + expiresAt: newKeyExpiresAt + ? dayjs(newKeyExpiresAt).toISOString() + : undefined, }); if (response.data) { diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 7b71443..1411384 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -7,13 +7,13 @@ import { Container, Grid, Group, - Modal, Progress, SimpleGrid, Stack, Text, Title, } from "@mantine/core"; +import { modals } from "@mantine/modals"; import { IconClock, IconDatabase, @@ -21,7 +21,6 @@ import { IconUserCheck, } from "@tabler/icons-react"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { useState } from "react"; import { useSnapshot } from "valtio"; import { authClient } from "@/utils/auth-client"; import { authStore } from "../../store/auth"; @@ -33,7 +32,19 @@ export const Route = createFileRoute("/dashboard/")({ function DashboardComponent() { const snap = useSnapshot(authStore); const navigate = useNavigate(); - const [logoutModalOpen, setLogoutModalOpen] = useState(false); + + const openLogoutModal = () => + modals.openConfirmModal({ + title: "Confirm Logout", + centered: true, + children: Are you sure you want to log out?, + labels: { confirm: "Logout", cancel: "Cancel" }, + confirmProps: { color: "red" }, + onConfirm: async () => { + await authClient.signOut(); + navigate({ to: "/signin" }); + }, + }); // Mock data for dashboard stats const statsData = [ @@ -84,11 +95,7 @@ function DashboardComponent() { - @@ -197,28 +204,6 @@ function DashboardComponent() { - setLogoutModalOpen(false)} - title="Confirm Logout" - centered - > - Are you sure you want to log out? - - - - - ); } diff --git a/src/routes/dashboard/route.tsx b/src/routes/dashboard/route.tsx index a5df6dd..c509c77 100644 --- a/src/routes/dashboard/route.tsx +++ b/src/routes/dashboard/route.tsx @@ -1,20 +1,28 @@ import { ActionIcon, AppShell, + Avatar, + Box, Burger, Group, + Menu, NavLink, rem, ScrollArea, + Stack, Text, - Title + Tooltip, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; +import { modals } from "@mantine/modals"; import { + IconChevronRight, IconHome, IconKey, + IconLogout, IconSettings, - IconUsers + IconUser, + IconUsers, } from "@tabler/icons-react"; import { createFileRoute, @@ -22,6 +30,9 @@ import { useLocation, useNavigate, } from "@tanstack/react-router"; +import { useSnapshot } from "valtio"; +import { authStore } from "../../store/auth"; +import { authClient } from "@/utils/auth-client"; export const Route = createFileRoute("/dashboard")({ component: DashboardLayout, @@ -29,93 +40,279 @@ export const Route = createFileRoute("/dashboard")({ function DashboardLayout() { const location = useLocation(); + const navigate = useNavigate(); + const snap = useSnapshot(authStore); const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); - const navigate = useNavigate(); const navItems = [ - { icon: IconHome, label: "Beranda", to: "/dashboard" }, - { icon: IconUsers, label: "Pengguna", to: "/dashboard/users" }, - { icon: IconKey, label: "API Key", to: "/dashboard/apikey" }, - { icon: IconSettings, label: "Pengaturan", to: "/dashboard/settings" }, + { + icon: IconHome, + label: "Beranda", + to: "/dashboard", + description: "Ringkasan sistem & statistik", + }, + { + icon: IconUsers, + label: "Pengguna", + to: "/dashboard/users", + description: "Kelola akun & hak akses", + }, + { + icon: IconKey, + label: "API Key", + to: "/dashboard/apikey", + description: "Manajemen kunci akses API", + }, + { + icon: IconSettings, + label: "Pengaturan", + to: "/dashboard/settings", + description: "Konfigurasi sistem", + }, ]; + const handleLogout = async () => { + modals.openConfirmModal({ + title: "Konfirmasi Keluar", + centered: true, + children: ( + + Apakah Anda yakin ingin keluar dari sistem? Sesi Anda akan berakhir. + + ), + labels: { confirm: "Keluar", cancel: "Batal" }, + confirmProps: { color: "red" }, + onConfirm: async () => { + await authClient.signOut(); + navigate({ to: "/signin" }); + }, + }); + }; + const isActive = (path: string) => { const current = location.pathname; - - if (path === "/dashboard") { - return current === "/dashboard"; - } - - return current === path || current.startsWith(`${path}/`); + if (path === "/dashboard") + return current === "/dashboard" || current === "/dashboard/"; + return current.startsWith(path); }; return ( - + - + - Dashboard + + + ADMIN + + PANEL + + + - - - - + + + + + +
+ + {snap.user?.name} + + + Administrator + +
+ + {snap.user?.name?.charAt(0)} + +
+
+ + + Akun + + } + onClick={() => navigate({ to: "/profile" })} + > + Profil Saya + + + } + onClick={() => navigate({ to: "/dashboard/settings" })} + > + Pengaturan + + + + + Bahaya + + } + onClick={handleLogout} + > + Keluar Sistem + + +
- - - - - Navigasi Utama - - - {navItems.map((item) => ( - { - navigate({ to: item.to }); - }} - leftSection={ - + + + {navItems.map((item) => ( + + { + navigate({ to: item.to }); + if (mobileOpened) toggleMobile(); + }} + leftSection={ + + } + label={ + + + {item.label} + + + } + rightSection={} + active={isActive(item.to)} + variant="filled" + color="orange" + styles={{ + root: { + borderRadius: rem(8), + marginBottom: rem(4), + backgroundColor: isActive(item.to) + ? "rgba(243, 213, 163, 0.1)" + : "transparent", + color: isActive(item.to) ? "#f3d5a3" : "#fbf0df", + "&:hover": { + backgroundColor: "rgba(243, 213, 163, 0.05)", + }, + }, + label: { + fontSize: rem(14), + }, + }} /> - } - label={item.label} - active={isActive(item.to)} - /> - ))} - + + ))} + + + + + + } + styles={{ root: { borderRadius: rem(8) } }} + /> + + } + c="red" + styles={{ root: { borderRadius: rem(8) } }} + /> + - - + + + +
); diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx index d33cf58..c8766c6 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile.tsx @@ -1,24 +1,31 @@ +import { protectedRouteMiddleware } from "@/middleware/authMiddleware"; +import { authClient } from "@/utils/auth-client"; import { ActionIcon, Avatar, Badge, + Box, Button, Card, Code, Container, + Divider, + Grid, Group, - Modal, - SimpleGrid, + Paper, Stack, Text, Title, Tooltip, + rem, } from "@mantine/core"; +import { modals } from "@mantine/modals"; import { IconAt, IconCheck, IconCopy, IconDashboard, + IconExternalLink, IconId, IconLogout, IconShield, @@ -27,291 +34,207 @@ import { import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { useSnapshot } from "valtio"; -import { authClient } from "@/utils/auth-client"; import { authStore } from "../store/auth"; export const Route = createFileRoute("/profile")({ component: Profile, + beforeLoad: protectedRouteMiddleware, + onEnter({ context }) { + authStore.user = context?.user as any; + authStore.session = context?.session as any; + }, }); function Profile() { const snap = useSnapshot(authStore); const navigate = useNavigate(); - const [opened, setOpened] = useState(false); - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(null); async function logout() { await authClient.signOut(); navigate({ to: "/signin" }); } - const copyToClipboard = (text: string) => { + const openLogoutModal = () => + modals.openConfirmModal({ + title: Konfirmasi Keluar, + centered: true, + size: "sm", + children: ( + + Apakah Anda yakin ingin keluar dari akun Anda? Anda harus masuk kembali untuk mengakses data Anda. + + ), + labels: { confirm: "Keluar", cancel: "Batal" }, + confirmProps: { color: "red", leftSection: }, + onConfirm: logout, + }); + + const copyToClipboard = (text: string, key: string) => { if (navigator.clipboard) { navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + setCopied(key); + setTimeout(() => setCopied(null), 2000); } }; - return ( - - - User Profile - - - {/* Profile Header Card */} - - - - {snap.user?.name?.charAt(0).toUpperCase()} - - - - {snap.user?.name} + const InfoField = ({ icon: Icon, label, value, copyable = false, id = "" }: any) => ( + + + + + + + + {label} + + + + {value || "N/A"} - - - {snap.user?.email} - - - - - {snap.user?.role || "user"} - - - - - - - - Account Information - - - - - - -
- - User ID - - - - {snap.user?.id || "N/A"} - - + copyToClipboard(value, id)} > - - snap.user?.id && copyToClipboard(snap.user.id) - } - > - {copied ? : } - - - -
+ {copied === id ? : } + + + )}
-
- - - -
- - Email - - - - {snap.user?.email || "N/A"} - - - - snap.user?.email && copyToClipboard(snap.user.email) - } - > - {copied ? : } - - - -
-
-
- - - -
- - Name - - - - {snap.user?.name || "N/A"} - - - - snap.user?.name && copyToClipboard(snap.user.name) - } - > - {copied ? : } - - - -
-
-
- - - -
- - Role - - - {snap.user?.role || "user"} - -
-
-
-
+ + + + ); - + return ( + + + {/* Header Section */} - Session Information + + Profil Saya + Kelola informasi akun dan pengaturan keamanan Anda + {snap.user?.role === "admin" && ( )} - -
- - Session Token - - - - {snap.session?.token - ? `${snap.session.token.substring(0, 30)}...` - : "N/A"} - - - - snap.session?.token && copyToClipboard(snap.session.token) - } - > - {copied ? : } - - - -
-
-
- setOpened(false)} - title="Confirm Sign Out" - centered - size="sm" - > - - Are you sure you want to sign out? You will need to sign in again to - access your account. - - - - - - + + + {/* Profile Overview Card */} + + + + + + {snap.user?.name?.charAt(0).toUpperCase()} + + + {snap.user?.name} + + {snap.user?.email} + + + {snap.user?.role || "user"} + + + + + + + + + + + Informasi Identitas + + + + + + + + + + + + + + + + + + + + Keamanan & Sesi + + + + Sesi Saat Ini + + Aktif Sekarang + ID: {snap.session?.id?.substring(0, 8)}... + + + + + Session Token + + + {snap.session?.token ? `${snap.session.token.substring(0, 32)}...` : "N/A"} + + snap.session?.token && copyToClipboard(snap.session.token, "token")} + > + {copied === "token" ? : } + + + + + + + + + + +
); } diff --git a/src/vite.ts b/src/vite.ts index 6035f29..2eb74b3 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -5,36 +5,36 @@ import react from "@vitejs/plugin-react"; import { createServer as createViteServer } from "vite"; export async function createVite() { - return createViteServer({ - root: process.cwd(), - resolve: { - alias: { - "@": path.resolve(process.cwd(), "./src"), - }, - }, - plugins: [ - react({ - babel: { - plugins: [ - [ - "@react-dev-inspector/babel-plugin", - { - relativePath: true, - }, - ], - ], - }, - }), - inspectorServer(), - tanstackRouter(), - ], - server: { - middlewareMode: true, - hmr: true, - }, - appType: "custom", - optimizeDeps: { - include: ["react", "react-dom", "@mantine/core"], - }, - }); + return createViteServer({ + root: process.cwd(), + resolve: { + alias: { + "@": path.resolve(process.cwd(), "./src"), + }, + }, + plugins: [ + react({ + babel: { + plugins: [ + [ + "@react-dev-inspector/babel-plugin", + { + relativePath: true, + }, + ], + ], + }, + }), + inspectorServer(), + tanstackRouter(), + ], + server: { + middlewareMode: true, + hmr: true, + }, + appType: "custom", + optimizeDeps: { + include: ["react", "react-dom", "@mantine/core"], + }, + }); }