diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..7749699 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,92 @@ +# GEMINI.md - Project Context & Instructions + +## Project Overview +**wajs-server** is a full-stack WhatsApp integration platform built with Bun, ElysiaJS, and React. It provides a robust API and a web-based dashboard to manage WhatsApp sessions, send/receive messages, and integrate with external systems via webhooks. + +### Main Technologies +- **Runtime**: [Bun](https://bun.sh/) +- **Backend Framework**: [ElysiaJS](https://elysiajs.com/) +- **Frontend Library**: [React](https://react.dev/) with [Mantine UI](https://mantine.dev/) +- **Database ORM**: [Prisma](https://www.prisma.io/) (Targeting PostgreSQL) +- **WhatsApp Integration**: [whatsapp-web.js](https://github.com/pedroslopez/whatsapp-web.js) +- **State Management/Data Fetching**: [SWR](https://swr.vercel.app/) +- **Routing**: [React Router](https://reactrouter.com/) + +### Architecture +- **Server Entry Point**: `src/index.tsx` - Orchestrates the ElysiaJS server and serves the React frontend. +- **WhatsApp Service**: `src/server/lib/wa/wa_service.ts` - A singleton service managing the WhatsApp client lifecycle, event handling, and webhook dispatching. +- **API Routes**: Located in `src/server/routes/`, including: + - `wa_route.ts`: Core WhatsApp operations (send message, QR status, etc.). + - `webhook_route.ts`: CRUD for external webhooks. + - `auth_route.ts` & `apikey_route.ts`: Authentication and API key management. +- **Frontend**: Located in `src/pages/`, with routes defined in `src/AppRoutes.tsx`. It uses a dashboard layout (`src/pages/sq/dashboard/`). +- **Database Schema**: `prisma/schema.prisma` defines models for `User`, `ApiKey`, `WebHook`, `WaHook`, and `ChatFlows`. + +--- + +## Building and Running + +### Prerequisites +- [Bun](https://bun.sh/) installed. +- A PostgreSQL database instance. + +### Setup +1. **Install dependencies**: + ```bash + bun install + ``` +2. **Environment Configuration**: + Copy `.env.example` to `.env` and fill in the required variables: + - `DATABASE_URL`: PostgreSQL connection string. + - `JWT_SECRET`: Secret for JWT signing. + - `PORT`: Server port (default: 3000). + +3. **Database Migration**: + ```bash + bunx prisma migrate dev + ``` + +### Development +Start the development server with hot-reloading: +```bash +bun dev +``` + +### Production +1. **Build the frontend**: + ```bash + bun build + ``` +2. **Start the server**: + ```bash + bun start + ``` + +--- + +## Development Conventions + +### Coding Style +- **TypeScript**: The project is strictly typed. Ensure new features are properly typed. +- **Functional Components**: React frontend uses functional components and hooks. +- **Elysia Patterns**: Use Elysia's `use()` plugin system for modular routes and middlewares. + +### Key Workflows +- **WhatsApp Lifecycle**: The client uses `LocalAuth` for session persistence (stored in `.wwebjs_auth/`). In production, the client starts automatically on server boot. +- **Webhooks**: WhatsApp events are broadcast to all enabled webhooks defined in the database. +- **API Security**: Protected routes use the `apiAuth` middleware, which checks for either a valid JWT or a registered API key. + +### Directory Structure +- `src/server/`: Backend logic. +- `src/pages/`: Frontend views. +- `src/components/`: Reusable React components. +- `prisma/`: Database configuration and migrations. +- `generated/prisma/`: Auto-generated Prisma client (output directory is customized in `schema.prisma`). + +--- + +## Instructional Context for AI +- When modifying the WhatsApp logic, refer to `src/server/lib/wa/wa_service.ts`. +- When adding new API endpoints, register them in `src/index.tsx`. +- Frontend routing is managed in `src/AppRoutes.tsx`; follow the nested structure under `/sq/dashboard/` for new dashboard pages. +- Use `prisma` from `src/server/lib/prisma.ts` for database interactions. diff --git a/__tests__/seed.test.ts b/__tests__/seed.test.ts new file mode 100644 index 0000000..9d8c689 --- /dev/null +++ b/__tests__/seed.test.ts @@ -0,0 +1,49 @@ +import { mock } from "bun:test"; + +const mockPrisma = { + user: { + upsert: mock(async () => ({ id: "user-1", email: "admin@example.com" })), + }, + apiKey: { + upsert: mock(async () => ({ id: "key-1" })), + }, + webHook: { + findFirst: mock(async () => null as any), + create: mock(async () => ({ id: "webhook-1" })), + }, + chatFlows: { + upsert: mock(async () => ({ id: "flow-1" })), + }, + $disconnect: mock(async () => {}), +}; + +mock.module("@/server/lib/prisma", () => ({ + prisma: mockPrisma, +})); + +import { expect, test, describe } from "bun:test"; +import { seed } from "../prisma/seed"; + +describe("Prisma Seed", () => { + test("seed function should populate default data", async () => { + const result = await seed(); + + expect(result.user.email).toBe("admin@example.com"); + expect(mockPrisma.user.upsert).toHaveBeenCalled(); + expect(mockPrisma.apiKey.upsert).toHaveBeenCalled(); + expect(mockPrisma.webHook.findFirst).toHaveBeenCalled(); + expect(mockPrisma.webHook.create).toHaveBeenCalled(); + expect(mockPrisma.chatFlows.upsert).toHaveBeenCalled(); + }); + + test("seed function should skip webhook creation if it already exists", async () => { + // Reset mocks + mockPrisma.webHook.create.mockClear(); + mockPrisma.webHook.findFirst.mockResolvedValueOnce({ id: "existing-webhook" }); + + await seed(); + + expect(mockPrisma.webHook.findFirst).toHaveBeenCalled(); + expect(mockPrisma.webHook.create).not.toHaveBeenCalled(); + }); +}); diff --git a/generated/prisma/edge.js b/generated/prisma/edge.js index 1cd3482..65d877b 100644 --- a/generated/prisma/edge.js +++ b/generated/prisma/edge.js @@ -196,7 +196,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "/Users/bip/Documents/projects/jenna/wajs-server/generated/prisma", + "value": "/Users/bip/Documents/projects/projects_2026/wajs-server/generated/prisma", "fromEnvVar": null }, "config": { @@ -210,7 +210,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "/Users/bip/Documents/projects/jenna/wajs-server/prisma/schema.prisma", + "sourceFilePath": "/Users/bip/Documents/projects/projects_2026/wajs-server/prisma/schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": { @@ -224,7 +224,6 @@ const config = { "db" ], "activeProvider": "postgresql", - "postinstall": true, "inlineDatasources": { "db": { "url": { diff --git a/generated/prisma/index.js b/generated/prisma/index.js index 4fa9c9f..73ea5a6 100644 --- a/generated/prisma/index.js +++ b/generated/prisma/index.js @@ -197,7 +197,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "/Users/bip/Documents/projects/jenna/wajs-server/generated/prisma", + "value": "/Users/bip/Documents/projects/projects_2026/wajs-server/generated/prisma", "fromEnvVar": null }, "config": { @@ -211,7 +211,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "/Users/bip/Documents/projects/jenna/wajs-server/prisma/schema.prisma", + "sourceFilePath": "/Users/bip/Documents/projects/projects_2026/wajs-server/prisma/schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": { @@ -225,7 +225,6 @@ const config = { "db" ], "activeProvider": "postgresql", - "postinstall": true, "inlineDatasources": { "db": { "url": { diff --git a/generated/prisma/wasm.js b/generated/prisma/wasm.js index a863c1f..82fbbbf 100644 --- a/generated/prisma/wasm.js +++ b/generated/prisma/wasm.js @@ -196,7 +196,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "/Users/bip/Documents/projects/jenna/wajs-server/generated/prisma", + "value": "/Users/bip/Documents/projects/projects_2026/wajs-server/generated/prisma", "fromEnvVar": null }, "config": { @@ -210,7 +210,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "/Users/bip/Documents/projects/jenna/wajs-server/prisma/schema.prisma", + "sourceFilePath": "/Users/bip/Documents/projects/projects_2026/wajs-server/prisma/schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": { @@ -224,7 +224,6 @@ const config = { "db" ], "activeProvider": "postgresql", - "postinstall": true, "inlineDatasources": { "db": { "url": { diff --git a/package.json b/package.json index 4ea0ca8..0aa9b44 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "bun --hot src/index.tsx", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", "start": "NODE_ENV=production bun src/index.tsx", - "seed": "bun prisma/seed.ts" + "seed": "bun prisma/seed.ts", + "test": "bun test" }, "dependencies": { "@elysiajs/cors": "^1.4.1", diff --git a/prisma/seed.ts b/prisma/seed.ts index 430041f..38dae30 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,32 +1,93 @@ import { prisma } from "@/server/lib/prisma"; -const user = [ - { - name: "wibu", - email: "wibu@bip.com", - password: "Production_123", - } -]; +export async function seed() { + console.log("🌱 Starting seeding..."); -; (async () => { - for (const u of user) { - await prisma.user.upsert({ - where: { email: u.email }, - create: u, - update: u, + // 1. Seed User from environment variables or defaults + const adminEmail = process.env.ADMIN_EMAIL || "admin@example.com"; + const adminPassword = process.env.ADMIN_PASSWORD || "admin123"; + + const user = await prisma.user.upsert({ + where: { email: adminEmail }, + update: {}, + create: { + name: "Administrator", + email: adminEmail, + password: adminPassword, + }, + }); + console.log(`✅ User seeded: ${user.email}`); + + // 2. Seed Default API Key + const defaultKey = "wajs_sk_live_default_key_2026"; + await prisma.apiKey.upsert({ + where: { key: defaultKey }, + update: {}, + create: { + name: "Default API Key", + key: defaultKey, + userId: user.id, + description: "Auto-generated default API key", + }, + }); + console.log("✅ Default API Key seeded"); + + // 3. Seed Sample Webhook + const webhookUrl = "https://webhook.site/wajs-test"; + const existingWebhook = await prisma.webHook.findFirst({ + where: { url: webhookUrl }, + }); + + if (!existingWebhook) { + await prisma.webHook.create({ + data: { + name: "Sample Webhook", + url: webhookUrl, + description: "Test webhook for capturing events", + enabled: true, + method: "POST", + }, + }); + console.log("✅ Sample Webhook seeded"); + } else { + console.log("â„šī¸ Sample Webhook already exists, skipping"); + } + + // 4. Seed Initial ChatFlow + const flowUrl = "initial-flow"; + await prisma.chatFlows.upsert({ + where: { flowUrl: flowUrl }, + update: {}, + create: { + flowUrl: flowUrl, + flows: { + nodes: [ + { + id: "1", + type: "input", + data: { label: "Start" }, + position: { x: 250, y: 5 }, + }, + ], + edges: [], + }, + active: true, + defaultFlow: "Main Flow", + }, + }); + console.log("✅ Initial ChatFlow seeded"); + + console.log("✨ Seeding completed successfully!"); + return { user, defaultKey, webhookUrl, flowUrl }; +} + +if (import.meta.main) { + seed() + .catch((e) => { + console.error("❌ Seeding failed:", e); + process.exit(1); }) - - console.log(`✅ User ${u.email} seeded successfully`) - } - - -})().catch((e) => { - console.error(e) - process.exit(1) -}).finally(() => { - console.log("✅ Seeding completed successfully ") - process.exit(0) - -}) - -// use fix-code \ No newline at end of file + .finally(async () => { + await prisma.$disconnect(); + }); +} \ No newline at end of file