diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..98dd4e7 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,316 @@ +# Elysia API Documentation + +## Overview + +This project uses ElysiaJS integrated with Next.js App Router to create high-performance, type-safe APIs. The codebase follows a **Feature-based MVC pattern** for maintainability and scalability. + +## Base URL + +``` +http://localhost:3001 +``` + +## Endpoints + +### Customers API + +#### Get All Customers by Branch + +``` +GET /api/customers/:branch +``` + +**Parameters:** + +- `branch` (path parameter, required): Branch identifier + - Examples: `branch-01`, `branch-02`, `head-office` +- `status` (query parameter, optional): Filter by customer status + - Values: `active`, `inactive`, `pending` + +**Examples:** + +1. Get all customers from branch-01: + +```bash +curl http://localhost:3001/api/customers/branch-01 +``` + +2. Get active customers from branch-02: + +```bash +curl "http://localhost:3001/api/customers/branch-02?status=active" +``` + +3. Get pending customers from head-office: + +```bash +curl "http://localhost:3001/api/customers/head-office?status=pending" +``` + +**Response Format:** + +```json +{ + "success": true, + "data": [ + { + "id": "cust-001", + "branch": "branch-01", + "name": "สมชาย ใจดี", + "email": "somchai@example.com", + "phone": "081-234-5678", + "company": "บริษัท ไทยธุรกิจ จำกัด", + "address": "123 ถนนสุขุมวิท แขวงคลองตัน เขตคลองเตย กรุงเทพฯ 10110", + "status": "active", + "createdAt": "2024-01-15T09:00:00Z", + "updatedAt": "2024-01-15T09:00:00Z" + } + ], + "count": 1, + "message": "Found 1 customer(s) for branch: branch-01" +} +``` + +#### Get Single Customer by ID + +``` +GET /api/customers/:branch/:id +``` + +#### Create Customer + +``` +POST /api/customers +``` + +#### Update Customer + +``` +PUT /api/customers/:branch/:id +``` + +#### Delete Customer + +``` +DELETE /api/customers/:branch/:id +``` + +--- + +### Quotations API + +#### Get All Quotations by Branch + +``` +GET /api/quotations/:branch +``` + +**Parameters:** + +- `branch` (path parameter, required): Branch identifier + - Examples: `branch-01`, `branch-02`, `head-office` +- `status` (query parameter, optional): Filter by quotation status + - Values: `draft`, `sent`, `accepted`, `rejected`, `expired` + +**Examples:** + +1. Get all quotations from branch-01: + +```bash +curl http://localhost:3001/api/quotations/branch-01 +``` + +2. Get sent quotations from head-office: + +```bash +curl "http://localhost:3001/api/quotations/head-office?status=sent" +``` + +**Response Format:** + +```json +{ + "success": true, + "data": [ + { + "id": "quot-001", + "quotationNumber": "QT-2024-001", + "branch": "branch-01", + "customerId": "cust-001", + "customerName": "สมชาย ใจดี", + "date": "2024-01-20T00:00:00Z", + "validUntil": "2024-02-20T00:00:00Z", + "subtotal": 50000, + "taxRate": 0.07, + "taxAmount": 3500, + "totalAmount": 53500, + "status": "sent", + "notes": "Quotation for office supplies", + "createdAt": "2024-01-20T09:00:00Z", + "updatedAt": "2024-01-20T09:00:00Z" + } + ], + "count": 2, + "message": "Found 2 quotation(s) for branch: branch-01" +} +``` + +#### Get Single Quotation by ID + +``` +GET /api/quotations/:branch/:id +``` + +#### Create Quotation + +``` +POST /api/quotations +``` + +#### Update Quotation + +``` +PUT /api/quotations/:branch/:id +``` + +#### Delete Quotation + +``` +DELETE /api/quotations/:branch/:id +``` + +--- + +## Available Data + +### Customers + +- `branch-01`: 4 customers (3 active, 1 pending) +- `branch-02`: 3 customers (1 active, 1 inactive, 1 pending) +- `head-office`: 3 customers (all active) + +### Quotations + +- `branch-01`: 2 quotations (1 sent, 1 accepted) +- `branch-02`: 1 quotation (draft) +- `head-office`: 1 quotation (sent) + +## Testing with Browser + +Simply open these URLs in your browser: + +### Customers + +- http://localhost:3001/api/customers/branch-01 +- http://localhost:3001/api/customers/branch-02?status=active +- http://localhost:3001/api/customers/head-office + +### Quotations + +- http://localhost:3001/api/quotations/branch-01 +- http://localhost:3001/api/quotations/head-office?status=sent + +## Project Structure + +This project follows the **Feature-based MVC pattern** as recommended by ElysiaJS: + +``` +src/ +├── app/ +│ └── api/ +│ └── [[...slugs]]/ +│ └── route.ts # Main API entry point +├── modules/ +│ ├── customers/ +│ │ ├── controller.ts # HTTP handlers & routing +│ │ ├── service.ts # Business logic +│ │ └── model.ts # Schemas & validation +│ └── quotations/ +│ ├── controller.ts # HTTP handlers & routing +│ ├── service.ts # Business logic +│ └── model.ts # Schemas & validation +├── types/ +│ └── customer.ts # Shared types +├── lib/ +│ └── mock-data.ts # Mock data +``` + +### File Responsibilities + +#### Model (`model.ts`) + +- Define TypeBox schemas for validation +- Export TypeScript types from schemas +- All data structure definitions + +#### Service (`service.ts`) + +- Business logic and data operations +- Pure functions (no Elysia dependencies) +- CRUD operations +- Data transformation + +#### Controller (`controller.ts`) + +- Elysia instance for the module +- Route definitions and handlers +- Request/response validation +- Calls service functions +- HTTP-specific concerns + +#### Main Route (`app/api/[[...slugs]]/route.ts`) + +- Import all controllers +- Combine with `.use()` +- Export handlers for Next.js + +### Important Implementation Notes + +This project follows the **correct ElysiaJS + Next.js integration pattern**: + +- ✅ Single route file `[[...slugs]]/route.ts` with Elysia internal routing +- ✅ Uses `export const GET = app.fetch` (not `.handle`) +- ✅ Elysia instance has `prefix: '/api'` +- ✅ All routes defined within Elysia instances using `.get()`, `.post()`, etc. +- ✅ WinterCG compliant - works as normal Next.js API route +- ✅ Feature-based MVC pattern for maintainability +- ✅ Clear separation of concerns between Model, View, and Controller + +## Technologies Used + +- **ElysiaJS**: Type-safe, high-performance web framework +- **Next.js 16**: React framework with App Router +- **TypeScript**: Type safety throughout +- **TypeBox**: Schema validation (via `@elysiajs/schema`) + +## Features + +✅ Feature-based MVC architecture +✅ Dynamic branch parameter support +✅ Type-safe request/response validation +✅ Optional query parameter filtering +✅ Mock data for customers and quotations +✅ Full TypeScript support +✅ Auto-generated API documentation (Swagger/OpenAPI ready) +✅ Correct ElysiaJS + Next.js integration pattern +✅ Scalable and maintainable code structure +✅ Clear separation of concerns + +## Adding New Modules + +To add a new module (e.g., `products`): + +1. Create folder: `src/modules/products/` +2. Create `model.ts` - Define schemas +3. Create `service.ts` - Business logic +4. Create `controller.ts` - Routes and handlers +5. Update `src/app/api/[[...slugs]]/route.ts`: + + ```typescript + import { products } from "@/modules/products/controller"; + + const app = new Elysia({ prefix: "/api" }) + .use(customers) + .use(quotations) + .use(products); // Add new module + ``` diff --git a/KEYCLOAK_AUTH.md b/KEYCLOAK_AUTH.md new file mode 100644 index 0000000..93448de --- /dev/null +++ b/KEYCLOAK_AUTH.md @@ -0,0 +1,422 @@ +# Keycloak Authentication Implementation + +## Overview + +This document describes the Keycloak (OIDC) authentication implementation integrated into the Next.js + ElysiaJS application. + +## Architecture + +### Authentication Flow + +``` +┌─────────────┐ 1. Init ┌──────────────┐ 2. Login ┌──────────┐ +│ Browser │ ──────────────> │ Keycloak │ ──────────────> │ Browser │ +│ │ │ Server │ │ │ +└─────────────┘ └──────────────┘ └──────────┘ + │ │ + │ 3. Token (JWT) │ + ├──────────────────────────────────────────────────────────────>│ + │ │ + │ 4. API Call with Bearer Token │ + ├─────────────────────────────────────────────┐ │ + │ │ │ + ▼ ▼ │ +┌─────────────┐ 5. Verify Token ┌──────────────┐ │ +│ Next.js API │ ───────────────────> │ Database │ │ +│ (Elysia) │ │ (PostgreSQL) │ │ +└─────────────┘ └──────────────┘ │ + │ │ + │ 6. User Context │ + ├──────────────────────────────────────────────────────────────────>│ +``` + +## Components + +### Backend (ElysiaJS) + +#### 1. Database Layer (`src/database/`) + +**Files:** + +- `src/database/schema/users.ts` - User table schema +- `src/database/db.ts` - Database connection using Drizzle ORM +- `drizzle.config.ts` - Drizzle configuration + +**User Schema:** + +```typescript +{ + id: uuid (primary key) + keycloakId: text (unique, from Keycloak sub) + email: text + name: text + createdAt: timestamp + updatedAt: timestamp +} +``` + +#### 2. Keycloak Verification (`src/lib/keycloak.ts`) + +**Functions:** + +- `verifyToken(token)` - Verifies JWT using JWKS from Keycloak +- `extractToken(authHeader)` - Extracts Bearer token from Authorization header + +**Features:** + +- Automatic JWKS caching +- Token validation (issuer, audience, expiration) +- Type-safe token payload + +#### 3. Auth Middleware (`src/middleware/auth.ts`) + +**Exports:** + +- `authPlugin` - Elysia plugin that validates tokens and attaches user to context +- `requireAuth` - Helper function to require authentication + +**Usage:** + +```typescript +import { authPlugin } from "@/middleware/auth"; + +// Apply to all routes +const app = new Elysia().use(authPlugin).get("/protected", ({ user }) => { + return { message: "Hello!", user }; +}); +``` + +#### 4. User Service (`src/modules/auth/service.ts`) + +**Functions:** + +- `findOrCreateUser(payload)` - Finds existing user or creates new one from Keycloak payload +- `getUserByKeycloakId(keycloakId)` - Retrieves user by Keycloak ID + +### Frontend (Next.js) + +#### 1. Keycloak Client (`src/lib/keycloak-client.ts`) + +**Functions:** + +- `initKeycloak()` - Initializes Keycloak with `login-required` mode +- `logout()` - Logs out user and clears tokens +- `getUserInfo()` - Returns parsed token payload +- `getToken()` - Returns current access token +- `isAuthenticated()` - Check if user is authenticated + +**Features:** + +- Memory-only token storage (no localStorage) +- Automatic token refresh (30 seconds before expiry) +- Token refresh on 401 errors +- PKCE flow for security + +#### 2. Auth Provider (`src/providers/AuthProvider.tsx`) + +**Context:** + +```typescript +{ + isAuthenticated: boolean; + isLoading: boolean; + userInfo: any; + logout: () => Promise; +} +``` + +**Hook:** + +- `useAuth()` - Access auth context in components + +#### 3. API Client (`src/lib/api-client.ts`) + +**Enhanced Features:** + +- Automatically adds `Authorization: Bearer ` header +- Handles 401 errors by triggering token refresh +- Reads token from `window.__KEYCLOAK_TOKEN__` + +## Setup Instructions + +### 1. Database Setup + +```bash +# Copy environment template +cp .env.example .env + +# Edit .env with your database credentials +# DATABASE_URL=postgresql://user:password@localhost:5432/allaos + +# Generate and run migration +npx drizzle-kit generate +npx drizzle-kit migrate +``` + +### 2. Keycloak Setup + +#### Create a Realm and Client: + +1. Log in to Keycloak Admin Console +2. Create a new realm (e.g., `allaos`) +3. Create a new OpenID Connect client: + - Client ID: `allaos-frontend` + - Client Authentication: `ON` (for backend) + - Valid Redirect URIs: `http://localhost:3000/*` + - Web Origins: `http://localhost:3000` + - Access Type: `confidential` + +#### Configure Environment Variables: + +```env +# Backend (.env) +KEYCLOAK_URL=http://localhost:8080 +KEYCLOAK_REALM=allaos +KEYCLOAK_CLIENT_ID=allaos-frontend +KEYCLOAK_CLIENT_SECRET=your-client-secret + +# Frontend (.env.local or NEXT_PUBLIC_ in .env) +NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080 +NEXT_PUBLIC_KEYCLOAK_REALM=allaos +NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=allaos-frontend +``` + +### 3. Install Dependencies + +```bash +npm install keycloak jose +npm install -D @types/keycloak-js +``` + +### 4. Run Application + +```bash +npm run dev +``` + +## Usage Examples + +### Protecting API Routes + +```typescript +import { Elysia } from "elysia"; +import { authPlugin } from "@/middleware/auth"; + +const app = new Elysia({ prefix: "/api" }) + .use(authPlugin) + .get("/protected", ({ user, tokenPayload }) => { + // user is now available from database + // tokenPayload contains Keycloak claims + return { + message: "Protected data", + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }; + }); +``` + +### Accessing User Info in Components + +```typescript +"use client"; +import { useAuth } from "@/providers/AuthProvider"; + +export default function UserProfile() { + const { isAuthenticated, isLoading, userInfo, logout } = useAuth(); + + if (isLoading) return
Loading...
; + if (!isAuthenticated) return
Not authenticated
; + + return ( +
+

Welcome, {userInfo?.name}

+

Email: {userInfo?.email}

+ +
+ ); +} +``` + +### Making Authenticated API Calls + +```typescript +import { apiClient } from "@/lib/api-client"; + +// Automatically includes Bearer token +const data = await apiClient("/api/protected-endpoint"); +``` + +## Token Flow + +### 1. Initialization + +1. User visits application +2. `AuthProvider` initializes Keycloak +3. Keycloak redirects to login page (if not authenticated) +4. User logs in +5. Keycloak redirects back with code +6. Keycloak exchanges code for tokens +7. Access token stored in memory (`window.__KEYCLOAK_TOKEN__`) + +### 2. API Calls + +1. Component calls `apiClient()` +2. API client reads token from `window.__KEYCLOAK_TOKEN__` +3. Adds `Authorization: Bearer ` header +4. Backend receives request, extracts token +5. Verifies token using JWKS +6. Finds/creates user in database +7. Attaches user to request context +8. Route handler processes request + +### 3. Token Refresh + +1. Background interval checks token every second +2. If token expires in < 30 seconds, refresh automatically +3. If refresh fails, redirect to login +4. On 401 error, trigger immediate refresh attempt + +## Security Considerations + +### ✅ Implemented + +- **Memory-only token storage** - No localStorage/sessionStorage +- **PKCE flow** - Prevents authorization code interception +- **JWT verification** - Using JWKS from Keycloak +- **Token expiration** - Automatic refresh before expiry +- **HTTPS ready** - Works with secure cookies and headers +- **CORS configured** - Only allowed origins + +### ⚠️ Additional Recommendations + +1. **Enable HTTPS in production** +2. **Set up Keycloak SSL** +3. **Implement rate limiting** on auth endpoints +4. **Add session timeout** on client side +5. **Implement CSRF protection** for state-changing operations +6. **Add audit logging** for authentication events +7. **Enable Keycloak events** for security monitoring + +## Testing + +### Manual Testing + +1. Start Keycloak and your application +2. Visit `http://localhost:3000` +3. You should be redirected to Keycloak login +4. Login with test credentials +5. After login, you should see the application +6. Open browser DevTools → Network +7. Check that API calls have `Authorization: Bearer ` header + +### Testing Token Expiry + +1. Set Keycloak token expiry to 1 minute (for testing) +2. Login to application +3. Wait for token to expire +4. Try making an API call +5. Token should refresh automatically +6. If refresh fails, should redirect to login + +### Testing Invalid Token + +1. Manually modify `window.__KEYCLOAK_TOKEN__` in DevTools +2. Make an API call +3. Should receive 401 error +4. Should trigger token refresh + +## Troubleshooting + +### Issue: "Unauthorized: Invalid or expired token" + +**Possible causes:** + +- Token expired and refresh failed +- Keycloak URL/realm/client ID mismatch +- JWKS endpoint unreachable + +**Solutions:** + +- Check environment variables +- Verify Keycloak is running +- Check browser console for errors +- Verify JWKS endpoint is accessible + +### Issue: User not created in database + +**Possible causes:** + +- Database connection failed +- Migration not run +- Database permissions issue + +**Solutions:** + +- Run `npx drizzle-kit migrate` +- Check `DATABASE_URL` in .env +- Verify database is accessible + +### Issue: Redirect loop + +**Possible causes:** + +- Keycloak callback URL not configured +- Client not created or disabled +- Invalid redirect URI + +**Solutions:** + +- Check Keycloak client settings +- Verify Valid Redirect URIs +- Check client is enabled + +## File Structure + +``` +src/ +├── database/ +│ ├── db.ts # Database connection +│ └── schema/ +│ ├── users.ts # User schema +│ └── index.ts # Schema exports +├── lib/ +│ ├── keycloak.ts # JWT verification +│ ├── keycloak-client.ts # Keycloak JS client +│ └── api-client.ts # API client with auth +├── middleware/ +│ └── auth.ts # Elysia auth plugin +├── modules/ +│ └── auth/ +│ └── service.ts # User sync logic +├── providers/ +│ └── AuthProvider.tsx # React auth context +└── app/ + └── layout.tsx # Root layout with AuthProvider +``` + +## Next Steps (Phase 2 & 3) + +### Phase 2: Role-Based Access Control (RBAC) + +- Store user roles in database +- Add role claims to token verification +- Create role-based route protection +- Add admin/role management UI + +### Phase 3: Multi-Tenant Support + +- Add tenant_id to user schema +- Filter data by tenant +- Add tenant context to requests +- Implement tenant isolation + +## References + +- [Keycloak Documentation](https://www.keycloak.org/documentation) +- [OpenID Connect Core](https://openid.net/connect/) +- [ElysiaJS Documentation](https://elysiajs.com/) +- [Drizzle ORM Documentation](https://orm.drizzle.team/) +- [Next.js Documentation](https://nextjs.org/docs) diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..da067ba --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "drizzle-kit"; +import { config } from "dotenv"; + +config({ path: ".env" }); + +export default { + schema: "./src/database/schema", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL || "", + }, +} satisfies Config; diff --git a/package-lock.json b/package-lock.json index 8a70785..549aa71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@base-ui/react": "^1.4.0", + "@elysiajs/eden": "^1.4.9", "@hugeicons/core-free-icons": "^4.1.1", "@hugeicons/react": "^1.1.6", "@radix-ui/react-tooltip": "^1.2.8", @@ -19,15 +20,22 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "elysia": "^1.4.28", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "jose": "^6.2.2", "kbar": "^0.1.0-beta.48", + "keycloak": "^1.2.0", + "keycloak-js": "^26.2.4", "lucide-react": "^1.8.0", "motion": "^12.38.0", "next": "16.2.3", "next-themes": "^0.4.6", "nextjs-toploader": "^3.9.17", "nuqs": "^2.8.9", + "pg": "^8.20.0", "radix-ui": "^1.4.3", "react": "19.2.4", "react-day-picker": "^9.14.0", @@ -46,12 +54,15 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.2.3", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } }, @@ -536,6 +547,17 @@ } } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@date-fns/tz": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", @@ -726,6 +748,13 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@ecies/ciphers": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", @@ -740,6 +769,15 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@elysiajs/eden": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@elysiajs/eden/-/eden-1.4.9.tgz", + "integrity": "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA==", + "license": "MIT", + "peerDependencies": { + "elysia": ">=1.4.19" + } + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", @@ -773,6 +811,884 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -2435,6 +3351,17 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@opentelemetry/instrumentation-redis": { "version": "0.62.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz", @@ -5071,6 +5998,13 @@ "webpack": ">=5.0.0" } }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "license": "MIT", + "peer": true + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -5443,6 +6377,31 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT", + "peer": true + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -5661,9 +6620,9 @@ } }, "node_modules/@types/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -6981,8 +7940,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bundle-name": { "version": "4.1.0", @@ -7821,9 +8779,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", - "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -7832,6 +8790,147 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7875,6 +8974,47 @@ "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", "license": "ISC" }, + "node_modules/elysia": { + "version": "1.4.28", + "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.4.28.tgz", + "integrity": "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.1.1", + "exact-mirror": "^0.2.7", + "fast-decode-uri-component": "^1.0.1", + "memoirist": "^0.4.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">= 0.34.0 < 1", + "@types/bun": ">= 1.2.0", + "exact-mirror": ">= 0.0.9", + "file-type": ">= 20.0.0", + "openapi-types": ">= 12.0.0", + "typescript": ">= 5.0.0" + }, + "peerDependenciesMeta": { + "@types/bun": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/elysia/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", @@ -8141,6 +9281,48 @@ "benchmarks" ] }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8638,6 +9820,20 @@ "node": ">=18.0.0" } }, + "node_modules/exact-mirror": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.2.7.tgz", + "integrity": "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg==", + "license": "MIT", + "peerDependencies": { + "@sinclair/typebox": "^0.34.15" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + } + } + }, "node_modules/execa": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", @@ -8725,6 +9921,12 @@ "express": ">= 4.11" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8857,6 +10059,25 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", + "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9545,6 +10766,27 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -10459,6 +11701,21 @@ "react": "^16.6.3 || ^17.0.0" } }, + "node_modules/keycloak": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/keycloak/-/keycloak-1.2.0.tgz", + "integrity": "sha512-NZ/89CqAY59jAAzzmS2qT3iX3SH+ipa2U0KkZHTywUKM1GUQ+QbJHxfTdRNy2riAqA1NAUf6b9Xge+l0RCwZMA==", + "license": "ISC" + }, + "node_modules/keycloak-js": { + "version": "26.2.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.4.tgz", + "integrity": "sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10912,6 +12169,12 @@ "node": ">= 0.8" } }, + "node_modules/memoirist": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.4.0.tgz", + "integrity": "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -11634,6 +12897,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -11854,6 +13124,46 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -11863,6 +13173,15 @@ "node": ">=4.0.0" } }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, "node_modules/pg-protocol": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", @@ -11885,6 +13204,15 @@ "node": ">=4" } }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13268,12 +14596,20 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -13545,6 +14881,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -13789,6 +15142,25 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -13862,6 +15234,510 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", @@ -14029,6 +15905,19 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index 72c77fe..2816059 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@base-ui/react": "^1.4.0", + "@elysiajs/eden": "^1.4.9", "@hugeicons/core-free-icons": "^4.1.1", "@hugeicons/react": "^1.1.6", "@radix-ui/react-tooltip": "^1.2.8", @@ -20,15 +21,22 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "elysia": "^1.4.28", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "jose": "^6.2.2", "kbar": "^0.1.0-beta.48", + "keycloak": "^1.2.0", + "keycloak-js": "^26.2.4", "lucide-react": "^1.8.0", "motion": "^12.38.0", "next": "16.2.3", "next-themes": "^0.4.6", "nextjs-toploader": "^3.9.17", "nuqs": "^2.8.9", + "pg": "^8.20.0", "radix-ui": "^1.4.3", "react": "19.2.4", "react-day-picker": "^9.14.0", @@ -47,12 +55,15 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.2.3", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } } diff --git a/src/app/[branch]/customers/page.tsx b/src/app/[branch]/customers/page.tsx new file mode 100644 index 0000000..6097b45 --- /dev/null +++ b/src/app/[branch]/customers/page.tsx @@ -0,0 +1,28 @@ +import PageContainer from "@/components/layout/page-container"; +import { buttonVariants } from "@/components/ui/button"; +import { Heading } from "@/components/ui/heading"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { IconPlus } from "@tabler/icons-react"; +import Link from "next/link"; + +export default async function Page({ params }) { + const { branch } = await params; + console.log("branch", branch); + return ( + +
+
+ + + เพิ่มลูกค้า + +
+ +
+
+ ); +} diff --git a/src/app/[branch]/dashboard/page.tsx b/src/app/[branch]/dashboard/page.tsx new file mode 100644 index 0000000..4af794a --- /dev/null +++ b/src/app/[branch]/dashboard/page.tsx @@ -0,0 +1,4 @@ +export default async function Page({ params }) { + const { branch } = await params; + return
dashboard
; +} diff --git a/src/app/[branch]/layout.tsx b/src/app/[branch]/layout.tsx new file mode 100644 index 0000000..9cf7a76 --- /dev/null +++ b/src/app/[branch]/layout.tsx @@ -0,0 +1,34 @@ +import KBar from "@/components/kbar"; +import AppSidebar from "@/components/layout/app-sidebar"; +import Header from "@/components/layout/header"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import type { Metadata } from "next"; +import { cookies } from "next/headers"; + +export const metadata: Metadata = { + title: "ALLA-OS", + description: "ALLA-OS", +}; + +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + // Persisting the sidebar state in the cookie. + const cookieStore = await cookies(); + const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"; + return ( + + + + +
+ {/* page main content */} + {children} + {/* page main content ends */} + + + + ); +} diff --git a/src/app/[branch]/quotations/page.tsx b/src/app/[branch]/quotations/page.tsx new file mode 100644 index 0000000..35e2742 --- /dev/null +++ b/src/app/[branch]/quotations/page.tsx @@ -0,0 +1,4 @@ +export default async function Page({ params }) { + const { branch } = await params; + return
quotations
; +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 2a8f5cd..fe208d9 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,23 +1,23 @@ -import KBar from '@/components/kbar'; -import AppSidebar from '@/components/layout/app-sidebar'; -import Header from '@/components/layout/header'; -import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; -import type { Metadata } from 'next'; -import { cookies } from 'next/headers'; +import KBar from "@/components/kbar"; +import AppSidebar from "@/components/layout/app-sidebar"; +import Header from "@/components/layout/header"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import type { Metadata } from "next"; +import { cookies } from "next/headers"; export const metadata: Metadata = { - title: 'Next Shadcn Dashboard Starter', - description: 'Basic dashboard with Next.js and Shadcn' + title: "Admin", + description: "Admin", }; export default async function DashboardLayout({ - children + children, }: { children: React.ReactNode; }) { // Persisting the sidebar state in the cookie. const cookieStore = await cookies(); - const defaultOpen = cookieStore.get("sidebar_state")?.value === "true" + const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"; return ( diff --git a/src/app/api/[[...slugs]]/route.ts b/src/app/api/[[...slugs]]/route.ts new file mode 100644 index 0000000..aea5b58 --- /dev/null +++ b/src/app/api/[[...slugs]]/route.ts @@ -0,0 +1,16 @@ +import { Elysia } from "elysia"; +import { customers } from "@/modules/customers/controller"; +import { quotations } from "@/modules/quotations/controller"; +import { auth } from "@/modules/auth/controller"; + +// Create main Elysia instance with all modules +const app = new Elysia({ prefix: "/api" }) + .use(customers) // /api/customers/* + .use(quotations) // /api/quotations/* + .use(auth); // /api/auth/* + +// Export handlers for Next.js +export const GET = app.fetch; +export const POST = app.fetch; +export const PUT = app.fetch; +export const DELETE = app.fetch; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e0fb40d..3a60750 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import Providers from "@/components/layout/providers"; import { Toaster } from "@/components/ui/sonner"; import { fontVariables } from "@/lib/font"; import ThemeProvider from "@/components/layout/ThemeToggle/theme-provider"; +import { AuthProvider } from "@/providers/AuthProvider"; import { cn } from "@/lib/utils"; import type { Metadata, Viewport } from "next"; import { cookies } from "next/headers"; @@ -16,8 +17,8 @@ const META_THEME_COLORS = { }; export const metadata: Metadata = { - title: "Next Shadcn", - description: "Basic dashboard with Next.js and Shadcn", + title: "ALLA-OS", + description: "ALLA-OS [order-system]", }; export const viewport: Viewport = { @@ -65,10 +66,12 @@ export default async function RootLayout({ disableTransitionOnChange enableColorScheme > - - - {children} - + + + + {children} + + diff --git a/src/app/page.tsx b/src/app/page.tsx index 139262c..abc0d85 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default async function Page() { - redirect("/admin/overview"); + redirect("/alla/customers"); } diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index d5478a7..5225e30 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -29,7 +29,7 @@ import { SidebarRail, } from "@/components/ui/sidebar"; //import { UserAvatarProfile } from "@/components/user-avatar-profile"; -import { navItems } from "@/constants/data"; +import { navItems, tenantNavConfig } from "@/constants/data"; import { useMediaQuery } from "@/hooks/use-media-query"; import { @@ -47,31 +47,36 @@ import { usePathname, useRouter } from "next/navigation"; import * as React from "react"; import { Icons } from "../icons"; import { OrgSwitcher } from "../org-switcher"; +import { useAuth } from "@/providers/AuthProvider"; export const company = { - name: "Acme Inc", + name: "ALLA", logo: IconPhotoUp, plan: "Enterprise", }; const tenants = [ - { id: "1", name: "Acme Inc" }, - { id: "2", name: "Beta Corp" }, - { id: "3", name: "Gamma Ltd" }, + { id: "1", name: "ALLA" }, + { id: "2", name: "ONVALLA" }, ]; export default function AppSidebar() { const pathname = usePathname(); const { isOpen } = useMediaQuery(); const router = useRouter(); - const handleSwitchTenant = (_tenantId: string) => { - // Tenant switching functionality would be implemented here + const [activeTenant, setActiveTenant] = React.useState(tenants[0]); + const { isAuthenticated, userInfo, logout } = useAuth(); + + const handleSwitchTenant = (tenantId: string) => { + const newTenant = tenants.find((t) => t.id === tenantId); + if (newTenant) { + setActiveTenant(newTenant); + // Optional: Redirect to the tenant's dashboard after switching + // router.push(tenantNavConfig[tenantId][0]?.url || "/"); + } }; - const activeTenant = tenants[0]; - - React.useEffect(() => { - // Side effects based on sidebar state changes - }, [isOpen]); + // Get navItems based on active tenant + const currentNavItems = tenantNavConfig[activeTenant.id] || navItems; return ( @@ -86,7 +91,7 @@ export default function AppSidebar() { Overview - {navItems.map((item) => { + {currentNavItems.map((item) => { const Icon = item.icon ? Icons[item.icon] : Icons.logo; return item?.items && item?.items?.length > 0 ? ( - {/* {user && ( - - )} */} + {userInfo && ( +
+ +
+ + {userInfo?.name || + userInfo?.preferred_username || + "User"} + +
+
+ )} @@ -168,38 +178,33 @@ export default function AppSidebar() { sideOffset={4} > -
- {/* {user && ( - - )} */} +
+ {userInfo && ( + <> + +
+ + {userInfo?.name || + userInfo?.preferred_username || + "User"} + +
+ + )}
- router.push("/dashboard/profile")} - > - - Profile - - - - Billing - Notifications - + logout()}> - {/* */} + Logout diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index d6cd07a..d4085bd 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,27 +1,22 @@ -import React from 'react'; -import { SidebarTrigger } from '../ui/sidebar'; -import { Separator } from '../ui/separator'; -import { Breadcrumbs } from '../breadcrumbs'; -import SearchInput from '../search-input'; -import { UserNav } from './user-nav'; -import { ThemeSelector } from '../theme-selector'; -import { ModeToggle } from './ThemeToggle/theme-toggle'; -import CtaGithub from './cta-github'; +import React from "react"; +import { SidebarTrigger } from "../ui/sidebar"; +import { Separator } from "../ui/separator"; +import { Breadcrumbs } from "../breadcrumbs"; + +import { UserNav } from "./user-nav"; +import { ThemeSelector } from "../theme-selector"; +import { ModeToggle } from "./ThemeToggle/theme-toggle"; export default function Header() { return ( -
-
- - +
+
+ +
-
- -
- -
+
diff --git a/src/components/org-switcher.tsx b/src/components/org-switcher.tsx index 980b258..94a4e3e 100644 --- a/src/components/org-switcher.tsx +++ b/src/components/org-switcher.tsx @@ -1,19 +1,19 @@ -'use client'; +"use client"; -import { Check, ChevronsUpDown, GalleryVerticalEnd } from 'lucide-react'; -import * as React from 'react'; +import { Check, ChevronsUpDown, GalleryVerticalEnd } from "lucide-react"; +import * as React from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu'; + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { SidebarMenu, SidebarMenuButton, - SidebarMenuItem -} from '@/components/ui/sidebar'; + SidebarMenuItem, +} from "@/components/ui/sidebar"; interface Tenant { id: string; @@ -23,7 +23,7 @@ interface Tenant { export function OrgSwitcher({ tenants, defaultTenant, - onTenantSwitch + onTenantSwitch, }: { tenants: Tenant[]; defaultTenant: Tenant; @@ -49,31 +49,31 @@ export function OrgSwitcher({ -
- +
+
-
- Next Starter - {selectedTenant.name} +
+ ALLA OS + {selectedTenant.name}
- + {tenants.map((tenant) => ( handleTenantSwitch(tenant)} > - {tenant.name}{' '} + {tenant.name}{" "} {tenant.id === selectedTenant.id && ( - + )} ))} diff --git a/src/config/nav-config.ts b/src/config/nav-config.ts index 9d17561..46689b7 100644 --- a/src/config/nav-config.ts +++ b/src/config/nav-config.ts @@ -1,4 +1,4 @@ -import { NavGroup } from '@/types'; +import { NavGroup } from "@/types"; /** * Navigation configuration with RBAC support @@ -35,163 +35,163 @@ import { NavGroup } from '@/types'; */ export const navGroups: NavGroup[] = [ { - label: 'Overview', + label: "Overview", items: [ { - title: 'Dashboard', - url: '/dashboard/overview', - icon: 'dashboard', + title: "Dashboard", + url: "/dashboard/overview", + icon: "dashboard", isActive: false, - shortcut: ['d', 'd'], - items: [] + shortcut: ["d", "d"], + items: [], }, { - title: 'Workspaces', - url: '/dashboard/workspaces', - icon: 'workspace', - isActive: false, - items: [] - }, - { - title: 'Teams', - url: '/dashboard/workspaces/team', - icon: 'teams', + title: "Workspaces", + url: "/dashboard/workspaces", + icon: "workspace", isActive: false, items: [], - access: { requireOrg: true } }, { - title: 'Product', - url: '/dashboard/product', - icon: 'product', - shortcut: ['p', 'p'], + title: "Teams", + url: "/dashboard/workspaces/team", + icon: "teams", isActive: false, - items: [] + items: [], + access: { requireOrg: true }, }, { - title: 'Users', - url: '/dashboard/users', - icon: 'teams', - shortcut: ['u', 'u'], + title: "Product", + url: "/dashboard/product", + icon: "product", + shortcut: ["p", "p"], isActive: false, - items: [] + items: [], }, { - title: 'Kanban', - url: '/dashboard/kanban', - icon: 'kanban', - shortcut: ['k', 'k'], + title: "Users", + url: "/dashboard/users", + icon: "teams", + shortcut: ["u", "u"], isActive: false, - items: [] + items: [], }, { - title: 'Chat', - url: '/dashboard/chat', - icon: 'chat', - shortcut: ['c', 'c'], + title: "Kanban", + url: "/dashboard/kanban", + icon: "kanban", + shortcut: ["k", "k"], isActive: false, - items: [] - } - ] + items: [], + }, + { + title: "Chat", + url: "/dashboard/chat", + icon: "chat", + shortcut: ["c", "c"], + isActive: false, + items: [], + }, + ], }, { - label: 'Elements', + label: "Elements", items: [ { - title: 'Forms', - url: '#', - icon: 'forms', + title: "Forms", + url: "#", + icon: "forms", isActive: true, items: [ { - title: 'Basic Form', - url: '/dashboard/forms/basic', - icon: 'forms', - shortcut: ['f', 'f'] + title: "Basic Form", + url: "/dashboard/forms/basic", + icon: "forms", + shortcut: ["f", "f"], }, { - title: 'Multi-Step Form', - url: '/dashboard/forms/multi-step', - icon: 'forms' + title: "Multi-Step Form", + url: "/dashboard/forms/multi-step", + icon: "forms", }, { - title: 'Sheet & Dialog', - url: '/dashboard/forms/sheet-form', - icon: 'forms' + title: "Sheet & Dialog", + url: "/dashboard/forms/sheet-form", + icon: "forms", }, { - title: 'Advanced Patterns', - url: '/dashboard/forms/advanced', - icon: 'forms' - } - ] + title: "Advanced Patterns", + url: "/dashboard/forms/advanced", + icon: "forms", + }, + ], }, { - title: 'React Query', - url: '/dashboard/react-query', - icon: 'code', + title: "React Query", + url: "/dashboard/react-query", + icon: "code", isActive: false, - items: [] + items: [], }, { - title: 'Icons', - url: '/dashboard/elements/icons', - icon: 'palette', + title: "Icons", + url: "/dashboard/elements/icons", + icon: "palette", isActive: false, - items: [] - } - ] + items: [], + }, + ], }, { - label: '', + label: "", items: [ { - title: 'Pro', - url: '#', - icon: 'pro', + title: "Pro", + url: "#", + icon: "pro", isActive: true, items: [ { - title: 'Exclusive', - url: '/dashboard/exclusive', - icon: 'exclusive', - shortcut: ['e', 'e'] - } - ] + title: "Exclusive", + url: "/dashboard/exclusive", + icon: "exclusive", + shortcut: ["e", "e"], + }, + ], }, { - title: 'Account', - url: '#', - icon: 'account', + title: "Account", + url: "#", + icon: "account", isActive: true, items: [ { - title: 'Profile', - url: '/dashboard/profile', - icon: 'profile', - shortcut: ['m', 'm'] + title: "Profile", + url: "/dashboard/profile", + icon: "profile", + shortcut: ["m", "m"], }, { - title: 'Notifications', - url: '/dashboard/notifications', - icon: 'notification', - shortcut: ['n', 'n'] + title: "Notifications", + url: "/dashboard/notifications", + icon: "notification", + shortcut: ["n", "n"], }, { - title: 'Billing', - url: '/dashboard/billing', - icon: 'billing', - shortcut: ['b', 'b'], - access: { requireOrg: true } + title: "Billing", + url: "/dashboard/billing", + icon: "billing", + shortcut: ["b", "b"], + access: { requireOrg: true }, }, { - title: 'Login', - shortcut: ['l', 'l'], - url: '/', - icon: 'login' - } - ] - } - ] - } + title: "Login", + shortcut: ["l", "l"], + url: "/", + icon: "login", + }, + ], + }, + ], + }, ]; diff --git a/src/constants/data.ts b/src/constants/data.ts index cdc2226..90f9119 100644 --- a/src/constants/data.ts +++ b/src/constants/data.ts @@ -1,4 +1,42 @@ -import { NavItem } from '@/types'; +import { NavItem } from "@/types"; + +// Tenant-specific navigation configurations +export const tenantNavConfig: Record = { + "1": [ + // ALLA tenant + { + title: "Dashboard", + url: "/alla/dashboard/overview", + icon: "dashboard", + isActive: false, + items: [], + }, + { + title: "Customers", + url: "/alla/customers", + icon: "product", + isActive: false, + items: [], + }, + ], + "2": [ + // ONVALLA tenant + { + title: "Dashboard", + url: "/onvilla/dashboard/overview", + icon: "dashboard", + isActive: false, + items: [], + }, + { + title: "Customers", + url: "/onvilla/customers", + icon: "product", + isActive: false, + items: [], + }, + ], +}; export type Product = { photo_url: string; @@ -14,50 +52,19 @@ export type Product = { //Info: The following data is used for the sidebar navigation and Cmd K bar. export const navItems: NavItem[] = [ { - title: 'Dashboard', - url: '/dashboard/overview', - icon: 'dashboard', + title: "Dashboard", + url: "/alla/dashboard/overview", + icon: "dashboard", isActive: false, - shortcut: ['d', 'd'], - items: [] // Empty array as there are no child items for Dashboard + items: [], // Empty array as there are no child items for Dashboard }, { - title: 'Product', - url: '/dashboard/product', - icon: 'product', - shortcut: ['p', 'p'], + title: "Customers", + url: "/alla/customers", + icon: "product", isActive: false, - items: [] // No child items + items: [], // No child items }, - { - title: 'Account', - url: '#', // Placeholder as there is no direct link for the parent - icon: 'billing', - isActive: true, - - items: [ - { - title: 'Profile', - url: '/dashboard/profile', - icon: 'userPen', - shortcut: ['m', 'm'] - }, - { - title: 'Login', - shortcut: ['l', 'l'], - url: '/', - icon: 'login' - } - ] - }, - { - title: 'Kanban', - url: '/dashboard/kanban', - icon: 'kanban', - shortcut: ['k', 'k'], - isActive: false, - items: [] // No child items - } ]; export interface SaleUser { @@ -72,42 +79,42 @@ export interface SaleUser { export const recentSalesData: SaleUser[] = [ { id: 1, - name: 'Olivia Martin', - email: 'olivia.martin@email.com', - amount: '+$1,999.00', - image: 'https://api.slingacademy.com/public/sample-users/1.png', - initials: 'OM' + name: "Olivia Martin", + email: "olivia.martin@email.com", + amount: "+$1,999.00", + image: "https://api.slingacademy.com/public/sample-users/1.png", + initials: "OM", }, { id: 2, - name: 'Jackson Lee', - email: 'jackson.lee@email.com', - amount: '+$39.00', - image: 'https://api.slingacademy.com/public/sample-users/2.png', - initials: 'JL' + name: "Jackson Lee", + email: "jackson.lee@email.com", + amount: "+$39.00", + image: "https://api.slingacademy.com/public/sample-users/2.png", + initials: "JL", }, { id: 3, - name: 'Isabella Nguyen', - email: 'isabella.nguyen@email.com', - amount: '+$299.00', - image: 'https://api.slingacademy.com/public/sample-users/3.png', - initials: 'IN' + name: "Isabella Nguyen", + email: "isabella.nguyen@email.com", + amount: "+$299.00", + image: "https://api.slingacademy.com/public/sample-users/3.png", + initials: "IN", }, { id: 4, - name: 'William Kim', - email: 'will@email.com', - amount: '+$99.00', - image: 'https://api.slingacademy.com/public/sample-users/4.png', - initials: 'WK' + name: "William Kim", + email: "will@email.com", + amount: "+$99.00", + image: "https://api.slingacademy.com/public/sample-users/4.png", + initials: "WK", }, { id: 5, - name: 'Sofia Davis', - email: 'sofia.davis@email.com', - amount: '+$39.00', - image: 'https://api.slingacademy.com/public/sample-users/5.png', - initials: 'SD' - } + name: "Sofia Davis", + email: "sofia.davis@email.com", + amount: "+$39.00", + image: "https://api.slingacademy.com/public/sample-users/5.png", + initials: "SD", + }, ]; diff --git a/src/database/db.ts b/src/database/db.ts new file mode 100644 index 0000000..0e9a165 --- /dev/null +++ b/src/database/db.ts @@ -0,0 +1,9 @@ +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as schema from "./schema"; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +export const db = drizzle(pool, { schema }); diff --git a/src/database/schema/index.ts b/src/database/schema/index.ts new file mode 100644 index 0000000..ddf77b4 --- /dev/null +++ b/src/database/schema/index.ts @@ -0,0 +1 @@ +export * from "./users"; diff --git a/src/database/schema/users.ts b/src/database/schema/users.ts new file mode 100644 index 0000000..a9375ae --- /dev/null +++ b/src/database/schema/users.ts @@ -0,0 +1,13 @@ +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: uuid("id").primaryKey().defaultRandom(), + keycloakId: text("keycloak_id").notNull().unique(), + email: text("email").notNull(), + name: text("name").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 1dc16f4..06a1834 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -1,12 +1,36 @@ -const BASE_URL = '/api'; +const BASE_URL = "/api"; + +export async function apiClient( + endpoint: string, + options?: RequestInit, +): Promise { + // Get token from global window object (set by Keycloak client) + const token = + typeof window !== "undefined" ? (window as any).__KEYCLOAK_TOKEN__ : null; + + const headers: Record = { + "Content-Type": "application/json", + ...((options?.headers as Record) || {}), + }; + + // Add Authorization header if token exists + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } -export async function apiClient(endpoint: string, options?: RequestInit): Promise { const res = await fetch(`${BASE_URL}${endpoint}`, { - headers: { 'Content-Type': 'application/json' }, - ...options + ...options, + headers, }); if (!res.ok) { + // Handle 401 - token expired + if (res.status === 401) { + // Trigger token refresh by dispatching event + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("token-expired")); + } + } throw new Error(`API error: ${res.status} ${res.statusText}`); } diff --git a/src/lib/keycloak-client.ts b/src/lib/keycloak-client.ts new file mode 100644 index 0000000..669702d --- /dev/null +++ b/src/lib/keycloak-client.ts @@ -0,0 +1,143 @@ +import Keycloak from "keycloak-js"; + +const KEYCLOAK_URL = + process.env.NEXT_PUBLIC_KEYCLOAK_URL || "http://localhost:8080"; +const KEYCLOAK_REALM = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || "allaos"; +const KEYCLOAK_CLIENT_ID = + process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || "allaos-frontend"; + +// Initialize Keycloak instance +const keycloak = new Keycloak({ + url: KEYCLOAK_URL, + realm: KEYCLOAK_REALM, + clientId: KEYCLOAK_CLIENT_ID, +}); + +// Token refresh interval (in seconds) +const MIN_TOKEN_VALIDITY = 30; // Refresh 30 seconds before expiry + +/** + * Initialize Keycloak and authenticate user + */ +export async function initKeycloak(): Promise { + try { + const authenticated = await keycloak.init({ + onLoad: "login-required", + checkLoginIframe: false, + pkceMethod: "S256", + }); + + if (authenticated) { + // Store token in window object for API client + updateGlobalToken(); + + // Start token refresh timer + startTokenRefresh(); + + console.log("User authenticated:", keycloak.tokenParsed); + } + + return authenticated; + } catch (error) { + console.error("Keycloak initialization failed:", error); + return false; + } +} + +/** + * Update global window object with current token + */ +function updateGlobalToken() { + if (typeof window !== "undefined" && keycloak.token) { + (window as any).__KEYCLOAK_TOKEN__ = keycloak.token; + } +} + +/** + * Start automatic token refresh + */ +function startTokenRefresh() { + // Clear existing interval if any + if ((window as any).__TOKEN_REFRESH_INTERVAL__) { + clearInterval((window as any).__TOKEN_REFRESH_INTERVAL__); + } + + // Set up refresh interval + (window as any).__TOKEN_REFRESH_INTERVAL__ = setInterval(async () => { + try { + const refreshed = await keycloak.updateToken(MIN_TOKEN_VALIDITY); + if (refreshed) { + console.log("Token refreshed"); + updateGlobalToken(); + } + } catch (error) { + console.error("Failed to refresh token:", error); + // Redirect to login on refresh failure + await keycloak.login(); + } + }, 1000); // Check every second +} + +/** + * Logout user + */ +export async function logout() { + try { + // Clear refresh interval + if ((window as any).__TOKEN_REFRESH_INTERVAL__) { + clearInterval((window as any).__TOKEN_REFRESH_INTERVAL__); + } + + // Clear global token + if (typeof window !== "undefined") { + delete (window as any).__KEYCLOAK_TOKEN__; + } + + await keycloak.logout({ redirectUri: window.location.origin }); + } catch (error) { + console.error("Logout failed:", error); + } +} + +/** + * Get current user info + */ +export function getUserInfo() { + return keycloak.tokenParsed; +} + +/** + * Get current token + */ +export function getToken() { + return keycloak.token; +} + +/** + * Check if user is authenticated + */ +export function isAuthenticated(): boolean { + return !!keycloak.authenticated; +} + +// Listen for token expired events from API client +if (typeof window !== "undefined") { + window.addEventListener("token-expired", async () => { + console.log("Token expired, attempting refresh..."); + try { + const refreshed = await keycloak.updateToken(MIN_TOKEN_VALIDITY); + if (refreshed) { + updateGlobalToken(); + console.log("Token refreshed after expiry"); + } else { + console.log("Could not refresh token, redirecting to login"); + await keycloak.login(); + } + } catch (error) { + console.error("Failed to refresh expired token:", error); + await keycloak.login(); + } + }); +} + +export default keycloak; diff --git a/src/lib/keycloak.ts b/src/lib/keycloak.ts new file mode 100644 index 0000000..09ac8aa --- /dev/null +++ b/src/lib/keycloak.ts @@ -0,0 +1,59 @@ +import { jwtVerify, createRemoteJWKSet } from "jose"; + +const KEYCLOAK_URL = process.env.KEYCLOAK_URL || "http://localhost:8080"; +const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM || "allaos"; + +// JWKS endpoint for verifying tokens +const JWKS_URL = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs`; + +// Create JWKS cache +const JWKS = createRemoteJWKSet(new URL(JWKS_URL)); + +export interface KeycloakTokenPayload { + sub: string; // User ID + email?: string; + name?: string; + preferred_username?: string; + exp: number; + iat: number; + iss: string; + aud: string; +} + +/** + * Verify a Keycloak JWT access token + * @param token The JWT token string + * @returns Decoded token payload + * @throws Error if token is invalid + */ +export async function verifyToken( + token: string, +): Promise { + try { + const { payload } = await jwtVerify(token, JWKS, { + issuer: `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}`, + audience: process.env.KEYCLOAK_CLIENT_ID, + }); + + return payload as KeycloakTokenPayload; + } catch (error) { + console.error("Token verification failed:", error); + throw new Error("Invalid or expired token"); + } +} + +/** + * Extract Bearer token from Authorization header + * @param authHeader The Authorization header value + * @returns The token string or null + */ +export function extractToken(authHeader: string | null): string | null { + if (!authHeader) return null; + + const parts = authHeader.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") { + return null; + } + + return parts[1]; +} diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts new file mode 100644 index 0000000..b82141f --- /dev/null +++ b/src/lib/mock-data.ts @@ -0,0 +1,132 @@ +import { Customer } from "@/types/customer"; + +export const mockCustomers: Customer[] = [ + { + id: "cust-001", + branch: "branch-01", + name: "สมชาย ใจดี", + email: "somchai@example.com", + phone: "081-234-5678", + company: "บริษัท ไทยธุรกิจ จำกัด", + address: "123 ถนนสุขุมวิท แขวงคลองตัน เขตคลองเตย กรุงเทพฯ 10110", + status: "active", + createdAt: "2024-01-15T09:00:00Z", + updatedAt: "2024-01-15T09:00:00Z", + }, + { + id: "cust-002", + branch: "branch-01", + name: "วิภา สุขสันต์", + email: "wipa@example.com", + phone: "082-345-6789", + company: "บริษัท นวัตกรรมไทย จำกัด", + address: "456 ถนนพระราม 4 แขวงคลองเตย เขตคลองเตย กรุงเทพฯ 10110", + status: "active", + createdAt: "2024-02-20T10:30:00Z", + updatedAt: "2024-02-20T10:30:00Z", + }, + { + id: "cust-003", + branch: "branch-01", + name: "อนุชิต กล้าหาญ", + email: "anuchit@example.com", + phone: "083-456-7890", + company: "บริษัท พัฒนาธุรกิจ จำกัด", + address: "789 ถนนสีลม แขวงสีลม เขตบางรัก กรุงเทพฯ 10500", + status: "active", + createdAt: "2024-03-10T14:15:00Z", + updatedAt: "2024-03-10T14:15:00Z", + }, + { + id: "cust-004", + branch: "branch-02", + name: "มานี มีสุข", + email: "manee@example.com", + phone: "084-567-8901", + company: "บริษัท ค้าส่งสินค้า จำกัด", + address: "321 ถนนจรัญสนิทวงศ์ แขวงบางพลัด เขตบางพลัด กรุงเทพฯ 10700", + status: "active", + createdAt: "2024-01-25T11:00:00Z", + updatedAt: "2024-01-25T11:00:00Z", + }, + { + id: "cust-005", + branch: "branch-02", + name: "ประยุทธ์ จริงใจ", + email: "prayut@example.com", + phone: "085-678-9012", + company: "บริษัท อิเล็กทรอนิกส์ ไทย จำกัด", + address: "654 ถนนเพชรบุรี แขวงทุ่งพญาไท เขตราชเทวี กรุงเทพฯ 10400", + status: "inactive", + createdAt: "2024-02-05T08:45:00Z", + updatedAt: "2024-04-01T10:00:00Z", + }, + { + id: "cust-006", + branch: "branch-02", + name: "สมหญิง แก้วสะอาด", + email: "somying@example.com", + phone: "086-789-0123", + company: "บริษัท อาหารแห้ง จำกัด", + address: "987 ถนนลาดพร้าว แขวงลาดพร้าว เขตลาดพร้าว กรุงเทพฯ 10230", + status: "pending", + createdAt: "2024-04-15T16:20:00Z", + updatedAt: "2024-04-15T16:20:00Z", + }, + { + id: "cust-007", + branch: "head-office", + name: "ภูมิ รักษ์โลก", + email: "phumi@example.com", + phone: "087-890-1234", + company: "บริษัท เคมีภัณฑ์ ไทย จำกัด", + address: "147 ถนนวิภาวดีรังสิต แขวงดอนเมือง เขตดอนเมือง กรุงเทพฯ 10210", + status: "active", + createdAt: "2024-01-10T09:30:00Z", + updatedAt: "2024-01-10T09:30:00Z", + }, + { + id: "cust-008", + branch: "head-office", + name: "กัญญา มีเมตตา", + email: "kanya@example.com", + phone: "088-901-2345", + company: "บริษัท สิ่งทอ ไทย จำกัด", + address: "258 ถนนพหลโยธิน แขวงสามเสนใน เขตพญาไท กรุงเทพฯ 10400", + status: "active", + createdAt: "2024-03-20T13:00:00Z", + updatedAt: "2024-03-20T13:00:00Z", + }, + { + id: "cust-009", + branch: "head-office", + name: "สุเมธ รัตนา", + email: "sumet@example.com", + phone: "089-012-3456", + company: "บริษัท ก่อสร้าง รุ่งเรือง จำกัด", + address: "369 ถนนนิมิตรใหม่ แขวงบางบอน เขตบางบอน กรุงเทพฯ 10150", + status: "active", + createdAt: "2024-02-28T15:45:00Z", + updatedAt: "2024-02-28T15:45:00Z", + }, + { + id: "cust-010", + branch: "branch-01", + name: "นภา รัตนา", + email: "napha@example.com", + phone: "090-123-4567", + company: "บริษัท ซอฟต์แวร์ ไทย จำกัด", + address: "741 ถนนเทพรัตน แขวงบางนา เขตบางนา กรุงเทพฯ 10260", + status: "pending", + createdAt: "2024-04-10T11:30:00Z", + updatedAt: "2024-04-10T11:30:00Z", + }, +]; + +export const getCustomersByBranch = (branch: string): Customer[] => { + return mockCustomers.filter((customer) => customer.branch === branch); +}; + +export const getCustomerById = (id: string): Customer | undefined => { + return mockCustomers.find((customer) => customer.id === id); +}; diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..45f136e --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,65 @@ +import { Elysia, type Context } from "elysia"; +import { verifyToken, extractToken } from "@/lib/keycloak"; +import { findOrCreateUser } from "@/modules/auth/service"; +import type { KeycloakTokenPayload } from "@/lib/keycloak"; +import type { User } from "@/database/schema"; + +// Extend Elysia context to include user +declare module "elysia" { + interface Context { + user?: User; + tokenPayload?: KeycloakTokenPayload; + } +} + +/** + * Elysia plugin for Keycloak authentication + * Validates Bearer token and attaches user to context + */ +export const authPlugin = new Elysia({ name: "auth" }).derive( + async ({ request, set }) => { + const authHeader = request.headers.get("Authorization"); + const token = extractToken(authHeader); + + if (!token) { + set.status = 401; + return { + error: "Unauthorized: No token provided", + user: undefined, + tokenPayload: undefined, + }; + } + + try { + // Verify token + const payload = await verifyToken(token); + + // Find or create user + const user = await findOrCreateUser(payload); + + return { + user, + tokenPayload: payload, + }; + } catch (error) { + console.error("Authentication failed:", error); + set.status = 401; + return { + error: "Unauthorized: Invalid or expired token", + user: undefined, + tokenPayload: undefined, + }; + } + }, +); + +/** + * Helper to require authentication on a route + * Returns 401 if no valid user + */ +export const requireAuth = (context: Context) => { + if (!context.user) { + throw new Error("Unauthorized"); + } + return context; +}; diff --git a/src/modules/auth/controller.ts b/src/modules/auth/controller.ts new file mode 100644 index 0000000..845aa6c --- /dev/null +++ b/src/modules/auth/controller.ts @@ -0,0 +1,64 @@ +import { Elysia, t } from "elysia"; +import { authPlugin } from "@/middleware/auth"; +import type { User } from "@/database/schema"; +import type { KeycloakTokenPayload } from "@/lib/keycloak"; + +export const auth = new Elysia({ prefix: "/auth", tags: ["auth"] }) + .use(authPlugin) + // GET /api/auth/me - Get current user info + .get( + "/me", + (context: any) => { + const user = context.user as User; + const tokenPayload = context.tokenPayload as KeycloakTokenPayload; + + if (!user || !tokenPayload) { + throw new Error("Unauthorized"); + } + + return { + success: true as const, + data: { + user: { + id: user.id, + keycloakId: user.keycloakId, + email: user.email, + name: user.name, + createdAt: user.createdAt.toISOString(), + }, + tokenInfo: { + sub: tokenPayload.sub, + email: tokenPayload.email, + name: tokenPayload.name, + exp: tokenPayload.exp, + iat: tokenPayload.iat, + }, + }, + }; + }, + { + response: t.Object({ + success: t.Literal(true), + data: t.Object({ + user: t.Object({ + id: t.String(), + keycloakId: t.String(), + email: t.String(), + name: t.String(), + createdAt: t.String(), + }), + tokenInfo: t.Object({ + sub: t.String(), + email: t.Optional(t.String()), + name: t.Optional(t.String()), + exp: t.Number(), + iat: t.Number(), + }), + }), + }), + detail: { + description: "Get current authenticated user information", + security: [{ Bearer: [] }], + }, + }, + ); diff --git a/src/modules/auth/service.ts b/src/modules/auth/service.ts new file mode 100644 index 0000000..61ba368 --- /dev/null +++ b/src/modules/auth/service.ts @@ -0,0 +1,47 @@ +import { db } from "@/database/db"; +import { users } from "@/database/schema"; +import { eq } from "drizzle-orm"; +import type { KeycloakTokenPayload } from "@/lib/keycloak"; + +/** + * Find or create user based on Keycloak token payload + * @param payload Keycloak token payload + * @returns User record from database + */ +export async function findOrCreateUser(payload: KeycloakTokenPayload) { + // Try to find existing user by keycloakId + const existingUser = await db + .select() + .from(users) + .where(eq(users.keycloakId, payload.sub)) + .limit(1); + + if (existingUser.length > 0) { + return existingUser[0]; + } + + // Create new user if not found + const newUser = { + keycloakId: payload.sub, + email: payload.email || "", + name: payload.name || payload.preferred_username || "Unknown User", + }; + + const [createdUser] = await db.insert(users).values(newUser).returning(); + return createdUser; +} + +/** + * Get user by Keycloak ID + * @param keycloakId The Keycloak user ID + * @returns User record or null + */ +export async function getUserByKeycloakId(keycloakId: string) { + const result = await db + .select() + .from(users) + .where(eq(users.keycloakId, keycloakId)) + .limit(1); + + return result[0] || null; +} diff --git a/src/modules/customers/controller.ts b/src/modules/customers/controller.ts new file mode 100644 index 0000000..551e21b --- /dev/null +++ b/src/modules/customers/controller.ts @@ -0,0 +1,216 @@ +import { Elysia, t } from "elysia"; +import * as service from "./service"; +import { CustomerModel } from "./model"; + +// Create Elysia instance for customers module +export const customers = new Elysia({ + prefix: "/customers", + tags: ["customers"], +}) + .model(CustomerModel) + // GET /api/customers/:branch - Get all customers by branch + .get( + "/:branch", + ({ params, query }) => { + const { branch } = params; + const { status } = query as { status?: string }; + + const customers = service.getAllCustomers( + branch, + status as "active" | "inactive" | "pending" | undefined, + ); + + return { + success: true, + data: customers, + count: customers.length, + message: `Found ${customers.length} customer(s) for branch: ${branch}`, + }; + }, + { + params: t.Object({ + branch: t.String(), + }), + query: t.Optional( + t.Object({ + status: t.Optional( + t.Union([ + t.Literal("active"), + t.Literal("inactive"), + t.Literal("pending"), + ]), + ), + }), + ), + response: CustomerModel.CustomerList, + detail: { + description: "Get all customers for a specific branch", + parameters: [ + { + name: "branch", + in: "path", + required: true, + schema: { type: "string" }, + description: + "Branch identifier (e.g., branch-01, branch-02, head-office)", + }, + { + name: "status", + in: "query", + required: false, + schema: { + type: "string", + enum: ["active", "inactive", "pending"], + }, + description: "Filter customers by status", + }, + ], + }, + }, + ) + // GET /api/customers/:branch/:id - Get single customer by ID + .get( + "/:branch/:id", + ({ params }) => { + const { branch, id } = params; + const customer = service.getCustomerByIdAndBranch(branch, id); + + if (!customer) { + return { + success: false, + error: "Customer not found", + }; + } + + return { + success: true, + data: customer, + }; + }, + { + params: t.Object({ + branch: t.String(), + id: t.String(), + }), + response: t.Union([ + t.Object({ + success: t.Literal(true), + data: CustomerModel.Customer, + }), + t.Object({ + success: t.Literal(false), + error: t.String(), + }), + ]), + detail: { + description: "Get a single customer by ID and branch", + }, + }, + ) + // POST /api/customers - Create new customer + .post( + "/", + ({ body }) => { + const customer = service.createCustomer(body); + + return { + success: true, + data: customer, + message: "Customer created successfully", + }; + }, + { + body: CustomerModel.CreateCustomer, + response: t.Object({ + success: t.Boolean(), + data: CustomerModel.Customer, + message: t.String(), + }), + detail: { + description: "Create a new customer", + }, + }, + ) + // PUT /api/customers/:branch/:id - Update customer + .put( + "/:branch/:id", + ({ params, body }) => { + const { branch, id } = params; + const customer = service.updateCustomer(branch, id, body); + + if (!customer) { + return { + success: false, + error: "Customer not found", + }; + } + + return { + success: true, + data: customer, + message: "Customer updated successfully", + }; + }, + { + params: t.Object({ + branch: t.String(), + id: t.String(), + }), + body: CustomerModel.UpdateCustomer, + response: t.Union([ + t.Object({ + success: t.Literal(true), + data: CustomerModel.Customer, + message: t.String(), + }), + t.Object({ + success: t.Literal(false), + error: t.String(), + }), + ]), + detail: { + description: "Update an existing customer", + }, + }, + ) + // DELETE /api/customers/:branch/:id - Delete customer + .delete( + "/:branch/:id", + ({ params }) => { + const { branch, id } = params; + const customer = service.deleteCustomer(branch, id); + + if (!customer) { + return { + success: false, + error: "Customer not found", + }; + } + + return { + success: true, + data: customer, + message: "Customer deleted successfully", + }; + }, + { + params: t.Object({ + branch: t.String(), + id: t.String(), + }), + response: t.Union([ + t.Object({ + success: t.Literal(true), + data: CustomerModel.Customer, + message: t.String(), + }), + t.Object({ + success: t.Literal(false), + error: t.String(), + }), + ]), + detail: { + description: "Delete a customer", + }, + }, + ); diff --git a/src/modules/customers/model.ts b/src/modules/customers/model.ts new file mode 100644 index 0000000..8254ea8 --- /dev/null +++ b/src/modules/customers/model.ts @@ -0,0 +1,82 @@ +import { t } from "elysia"; + +// Schemas for validation +export const CustomerModel = { + Customer: t.Object({ + id: t.String(), + branch: t.String(), + name: t.String(), + email: t.String({ format: "email" }), + phone: t.String(), + company: t.String(), + address: t.String(), + status: t.Union([ + t.Literal("active"), + t.Literal("inactive"), + t.Literal("pending"), + ]), + createdAt: t.String({ format: "date-time" }), + updatedAt: t.String({ format: "date-time" }), + }), + + CreateCustomer: t.Object({ + branch: t.String(), + name: t.String(), + email: t.String({ format: "email" }), + phone: t.String(), + company: t.String(), + address: t.String(), + status: t.Optional( + t.Union([ + t.Literal("active"), + t.Literal("inactive"), + t.Literal("pending"), + ]), + ), + }), + + UpdateCustomer: t.Object({ + name: t.Optional(t.String()), + email: t.Optional(t.String({ format: "email" })), + phone: t.Optional(t.String()), + company: t.Optional(t.String()), + address: t.Optional(t.String()), + status: t.Optional( + t.Union([ + t.Literal("active"), + t.Literal("inactive"), + t.Literal("pending"), + ]), + ), + }), + + CustomerList: t.Object({ + success: t.Boolean(), + data: t.Array( + t.Object({ + id: t.String(), + branch: t.String(), + name: t.String(), + email: t.String(), + phone: t.String(), + company: t.String(), + address: t.String(), + status: t.Union([ + t.Literal("active"), + t.Literal("inactive"), + t.Literal("pending"), + ]), + createdAt: t.String(), + updatedAt: t.String(), + }), + ), + count: t.Number(), + message: t.Optional(t.String()), + }), +}; + +// Export types from schemas +export type Customer = typeof CustomerModel.Customer.static; +export type CreateCustomer = typeof CustomerModel.CreateCustomer.static; +export type UpdateCustomer = typeof CustomerModel.UpdateCustomer.static; +export type CustomerList = typeof CustomerModel.CustomerList.static; diff --git a/src/modules/customers/service.ts b/src/modules/customers/service.ts new file mode 100644 index 0000000..9d305f3 --- /dev/null +++ b/src/modules/customers/service.ts @@ -0,0 +1,109 @@ +import { getCustomersByBranch, getCustomerById } from "@/lib/mock-data"; +import type { Customer, CreateCustomer, UpdateCustomer } from "./model"; + +/** + * Get all customers for a specific branch + * @param branch - Branch identifier + * @param status - Optional status filter + * @returns Array of customers + */ +export function getAllCustomers( + branch: string, + status?: "active" | "inactive" | "pending", +): Customer[] { + let customers = getCustomersByBranch(branch); + + if (status) { + customers = customers.filter((customer) => customer.status === status); + } + + return customers; +} + +/** + * Get a single customer by ID and branch + * @param branch - Branch identifier + * @param id - Customer ID + * @returns Customer or undefined if not found + */ +export function getCustomerByIdAndBranch( + branch: string, + id: string, +): Customer | undefined { + const customer = getCustomerById(id); + + // Only return if customer belongs to the specified branch + if (customer && customer.branch === branch) { + return customer; + } + + return undefined; +} + +/** + * Create a new customer + * @param data - Customer creation data + * @returns Newly created customer + */ +export function createCustomer(data: CreateCustomer): Customer { + const newCustomer: Customer = { + id: `cust-${Date.now()}`, + ...data, + status: data.status || "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + // In a real app, this would save to database + // For now, we'll just return the new customer + return newCustomer; +} + +/** + * Update an existing customer + * @param branch - Branch identifier + * @param id - Customer ID + * @param data - Customer update data + * @returns Updated customer or undefined if not found + */ +export function updateCustomer( + branch: string, + id: string, + data: UpdateCustomer, +): Customer | undefined { + const customer = getCustomerByIdAndBranch(branch, id); + + if (!customer) { + return undefined; + } + + // Merge update data + const updatedCustomer: Customer = { + ...customer, + ...data, + updatedAt: new Date().toISOString(), + }; + + // In a real app, this would update database + return updatedCustomer; +} + +/** + * Delete a customer + * @param branch - Branch identifier + * @param id - Customer ID + * @returns Deleted customer or undefined if not found + */ +export function deleteCustomer( + branch: string, + id: string, +): Customer | undefined { + const customer = getCustomerByIdAndBranch(branch, id); + + if (!customer) { + return undefined; + } + + // In a real app, this would delete from database + return customer; +} diff --git a/src/modules/quotations/controller.ts b/src/modules/quotations/controller.ts new file mode 100644 index 0000000..065c3e6 --- /dev/null +++ b/src/modules/quotations/controller.ts @@ -0,0 +1,224 @@ +import { Elysia, t } from "elysia"; +import * as service from "./service"; +import { QuotationModel } from "./model"; + +// Create Elysia instance for quotations module +export const quotations = new Elysia({ + prefix: "/quotations", + tags: ["quotations"], +}) + .model(QuotationModel) + // GET /api/quotations/:branch - Get all quotations by branch + .get( + "/:branch", + ({ params, query }) => { + const { branch } = params; + const { status } = query as { status?: string }; + + const quotations = service.getAllQuotations( + branch, + status as + | "draft" + | "sent" + | "accepted" + | "rejected" + | "expired" + | undefined, + ); + + return { + success: true, + data: quotations, + count: quotations.length, + message: `Found ${quotations.length} quotation(s) for branch: ${branch}`, + }; + }, + { + params: t.Object({ + branch: t.String(), + }), + query: t.Optional( + t.Object({ + status: t.Optional( + t.Union([ + t.Literal("draft"), + t.Literal("sent"), + t.Literal("accepted"), + t.Literal("rejected"), + t.Literal("expired"), + ]), + ), + }), + ), + response: QuotationModel.QuotationList, + detail: { + description: "Get all quotations for a specific branch", + parameters: [ + { + name: "branch", + in: "path", + required: true, + schema: { type: "string" }, + description: + "Branch identifier (e.g., branch-01, branch-02, head-office)", + }, + { + name: "status", + in: "query", + required: false, + schema: { + type: "string", + enum: ["draft", "sent", "accepted", "rejected", "expired"], + }, + description: "Filter quotations by status", + }, + ], + }, + }, + ) + // GET /api/quotations/:branch/:id - Get single quotation by ID + .get( + "/:branch/:id", + ({ params }) => { + const { branch, id } = params; + const quotation = service.getQuotationByIdAndBranch(branch, id); + + if (!quotation) { + return { + success: false, + error: "Quotation not found", + }; + } + + return { + success: true, + data: quotation, + }; + }, + { + params: t.Object({ + branch: t.String(), + id: t.String(), + }), + response: t.Union([ + t.Object({ + success: t.Literal(true), + data: QuotationModel.Quotation, + }), + t.Object({ + success: t.Literal(false), + error: t.String(), + }), + ]), + detail: { + description: "Get a single quotation by ID and branch", + }, + }, + ) + // POST /api/quotations - Create new quotation + .post( + "/", + ({ body }) => { + const quotation = service.createQuotation(body); + + return { + success: true, + data: quotation, + message: "Quotation created successfully", + }; + }, + { + body: QuotationModel.CreateQuotation, + response: t.Object({ + success: t.Boolean(), + data: QuotationModel.Quotation, + message: t.String(), + }), + detail: { + description: "Create a new quotation", + }, + }, + ) + // PUT /api/quotations/:branch/:id - Update quotation + .put( + "/:branch/:id", + ({ params, body }) => { + const { branch, id } = params; + const quotation = service.updateQuotation(branch, id, body); + + if (!quotation) { + return { + success: false, + error: "Quotation not found", + }; + } + + return { + success: true, + data: quotation, + message: "Quotation updated successfully", + }; + }, + { + params: t.Object({ + branch: t.String(), + id: t.String(), + }), + body: QuotationModel.UpdateQuotation, + response: t.Union([ + t.Object({ + success: t.Literal(true), + data: QuotationModel.Quotation, + message: t.String(), + }), + t.Object({ + success: t.Literal(false), + error: t.String(), + }), + ]), + detail: { + description: "Update an existing quotation", + }, + }, + ) + // DELETE /api/quotations/:branch/:id - Delete quotation + .delete( + "/:branch/:id", + ({ params }) => { + const { branch, id } = params; + const quotation = service.deleteQuotation(branch, id); + + if (!quotation) { + return { + success: false, + error: "Quotation not found", + }; + } + + return { + success: true, + data: quotation, + message: "Quotation deleted successfully", + }; + }, + { + params: t.Object({ + branch: t.String(), + id: t.String(), + }), + response: t.Union([ + t.Object({ + success: t.Literal(true), + data: QuotationModel.Quotation, + message: t.String(), + }), + t.Object({ + success: t.Literal(false), + error: t.String(), + }), + ]), + detail: { + description: "Delete a quotation", + }, + }, + ); diff --git a/src/modules/quotations/model.ts b/src/modules/quotations/model.ts new file mode 100644 index 0000000..9b3e5fc --- /dev/null +++ b/src/modules/quotations/model.ts @@ -0,0 +1,104 @@ +import { t } from "elysia"; + +// Schemas for validation +export const QuotationModel = { + Quotation: t.Object({ + id: t.String(), + quotationNumber: t.String(), + branch: t.String(), + customerId: t.String(), + customerName: t.String(), + date: t.String({ format: "date-time" }), + validUntil: t.String({ format: "date-time" }), + subtotal: t.Number(), + taxRate: t.Number(), + taxAmount: t.Number(), + totalAmount: t.Number(), + status: t.Union([ + t.Literal("draft"), + t.Literal("sent"), + t.Literal("accepted"), + t.Literal("rejected"), + t.Literal("expired"), + ]), + notes: t.Optional(t.String()), + createdAt: t.String({ format: "date-time" }), + updatedAt: t.String({ format: "date-time" }), + }), + + CreateQuotation: t.Object({ + branch: t.String(), + customerId: t.String(), + customerName: t.String(), + date: t.String({ format: "date-time" }), + validUntil: t.String({ format: "date-time" }), + subtotal: t.Number(), + taxRate: t.Number(), + notes: t.Optional(t.String()), + status: t.Optional( + t.Union([ + t.Literal("draft"), + t.Literal("sent"), + t.Literal("accepted"), + t.Literal("rejected"), + t.Literal("expired"), + ]), + ), + }), + + UpdateQuotation: t.Object({ + customerId: t.Optional(t.String()), + customerName: t.Optional(t.String()), + date: t.Optional(t.String({ format: "date-time" })), + validUntil: t.Optional(t.String({ format: "date-time" })), + subtotal: t.Optional(t.Number()), + taxRate: t.Optional(t.Number()), + notes: t.Optional(t.String()), + status: t.Optional( + t.Union([ + t.Literal("draft"), + t.Literal("sent"), + t.Literal("accepted"), + t.Literal("rejected"), + t.Literal("expired"), + ]), + ), + }), + + QuotationList: t.Object({ + success: t.Boolean(), + data: t.Array( + t.Object({ + id: t.String(), + quotationNumber: t.String(), + branch: t.String(), + customerId: t.String(), + customerName: t.String(), + date: t.String(), + validUntil: t.String(), + subtotal: t.Number(), + taxRate: t.Number(), + taxAmount: t.Number(), + totalAmount: t.Number(), + status: t.Union([ + t.Literal("draft"), + t.Literal("sent"), + t.Literal("accepted"), + t.Literal("rejected"), + t.Literal("expired"), + ]), + notes: t.Optional(t.String()), + createdAt: t.String(), + updatedAt: t.String(), + }), + ), + count: t.Number(), + message: t.Optional(t.String()), + }), +}; + +// Export types from schemas +export type Quotation = typeof QuotationModel.Quotation.static; +export type CreateQuotation = typeof QuotationModel.CreateQuotation.static; +export type UpdateQuotation = typeof QuotationModel.UpdateQuotation.static; +export type QuotationList = typeof QuotationModel.QuotationList.static; diff --git a/src/modules/quotations/service.ts b/src/modules/quotations/service.ts new file mode 100644 index 0000000..cc89154 --- /dev/null +++ b/src/modules/quotations/service.ts @@ -0,0 +1,222 @@ +import type { Quotation, CreateQuotation, UpdateQuotation } from "./model"; + +// Mock quotations data +const mockQuotations: Quotation[] = [ + { + id: "quot-001", + quotationNumber: "QT-2024-001", + branch: "branch-01", + customerId: "cust-001", + customerName: "สมชาย ใจดี", + date: "2024-01-20T00:00:00Z", + validUntil: "2024-02-20T00:00:00Z", + subtotal: 50000, + taxRate: 0.07, + taxAmount: 3500, + totalAmount: 53500, + status: "sent", + notes: "Quotation for office supplies", + createdAt: "2024-01-20T09:00:00Z", + updatedAt: "2024-01-20T09:00:00Z", + }, + { + id: "quot-002", + quotationNumber: "QT-2024-002", + branch: "branch-01", + customerId: "cust-002", + customerName: "วิภา สุขสันต์", + date: "2024-02-25T00:00:00Z", + validUntil: "2024-03-25T00:00:00Z", + subtotal: 120000, + taxRate: 0.07, + taxAmount: 8400, + totalAmount: 128400, + status: "accepted", + notes: "Quotation for computer equipment", + createdAt: "2024-02-25T10:30:00Z", + updatedAt: "2024-02-28T14:20:00Z", + }, + { + id: "quot-003", + quotationNumber: "QT-2024-003", + branch: "branch-02", + customerId: "cust-004", + customerName: "มานี มีสุข", + date: "2024-03-10T00:00:00Z", + validUntil: "2024-04-10T00:00:00Z", + subtotal: 75000, + taxRate: 0.07, + taxAmount: 5250, + totalAmount: 80250, + status: "draft", + notes: null, + createdAt: "2024-03-10T11:00:00Z", + updatedAt: "2024-03-10T11:00:00Z", + }, + { + id: "quot-004", + quotationNumber: "QT-2024-004", + branch: "head-office", + customerId: "cust-007", + customerName: "ภูมิ รักษ์โลก", + date: "2024-04-01T00:00:00Z", + validUntil: "2024-05-01T00:00:00Z", + subtotal: 200000, + taxRate: 0.07, + taxAmount: 14000, + totalAmount: 214000, + status: "sent", + notes: "Quotation for laboratory equipment", + createdAt: "2024-04-01T09:30:00Z", + updatedAt: "2024-04-01T09:30:00Z", + }, +]; + +/** + * Get all quotations for a specific branch + * @param branch - Branch identifier + * @param status - Optional status filter + * @returns Array of quotations + */ +export function getAllQuotations( + branch: string, + status?: "draft" | "sent" | "accepted" | "rejected" | "expired", +): Quotation[] { + let quotations = mockQuotations.filter((q) => q.branch === branch); + + if (status) { + quotations = quotations.filter((q) => q.status === status); + } + + return quotations; +} + +/** + * Get a single quotation by ID and branch + * @param branch - Branch identifier + * @param id - Quotation ID + * @returns Quotation or undefined if not found + */ +export function getQuotationByIdAndBranch( + branch: string, + id: string, +): Quotation | undefined { + const quotation = mockQuotations.find((q) => q.id === id); + + // Only return if quotation belongs to the specified branch + if (quotation && quotation.branch === branch) { + return quotation; + } + + return undefined; +} + +/** + * Calculate tax and total amounts + * @param subtotal - Subtotal amount + * @param taxRate - Tax rate (e.g., 0.07 for 7%) + * @returns Object with taxAmount and totalAmount + */ +function calculateTotals(subtotal: number, taxRate: number) { + const taxAmount = subtotal * taxRate; + const totalAmount = subtotal + taxAmount; + return { taxAmount, totalAmount }; +} + +/** + * Create a new quotation + * @param data - Quotation creation data + * @returns Newly created quotation + */ +export function createQuotation(data: CreateQuotation): Quotation { + const { taxAmount, totalAmount } = calculateTotals( + data.subtotal, + data.taxRate, + ); + + const newQuotation: Quotation = { + id: `quot-${Date.now()}`, + quotationNumber: `QT-${new Date().getFullYear()}-${String(mockQuotations.length + 1).padStart(3, "0")}`, + ...data, + taxAmount, + totalAmount, + status: data.status || "draft", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + // In a real app, this would save to database + mockQuotations.push(newQuotation); + return newQuotation; +} + +/** + * Update an existing quotation + * @param branch - Branch identifier + * @param id - Quotation ID + * @param data - Quotation update data + * @returns Updated quotation or undefined if not found + */ +export function updateQuotation( + branch: string, + id: string, + data: UpdateQuotation, +): Quotation | undefined { + const quotation = getQuotationByIdAndBranch(branch, id); + + if (!quotation) { + return undefined; + } + + // Recalculate totals if subtotal or taxRate changed + let { taxAmount, totalAmount } = quotation; + if (data.subtotal !== undefined || data.taxRate !== undefined) { + const newSubtotal = data.subtotal ?? quotation.subtotal; + const newTaxRate = data.taxRate ?? quotation.taxRate; + const calculated = calculateTotals(newSubtotal, newTaxRate); + taxAmount = calculated.taxAmount; + totalAmount = calculated.totalAmount; + } + + // Merge update data + const updatedQuotation: Quotation = { + ...quotation, + ...data, + taxAmount, + totalAmount, + updatedAt: new Date().toISOString(), + }; + + // In a real app, this would update database + const index = mockQuotations.findIndex((q) => q.id === id); + if (index !== -1) { + mockQuotations[index] = updatedQuotation; + } + + return updatedQuotation; +} + +/** + * Delete a quotation + * @param branch - Branch identifier + * @param id - Quotation ID + * @returns Deleted quotation or undefined if not found + */ +export function deleteQuotation( + branch: string, + id: string, +): Quotation | undefined { + const quotation = getQuotationByIdAndBranch(branch, id); + + if (!quotation) { + return undefined; + } + + // In a real app, this would delete from database + const index = mockQuotations.findIndex((q) => q.id === id); + if (index !== -1) { + mockQuotations.splice(index, 1); + } + + return quotation; +} diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx new file mode 100644 index 0000000..f7588cc --- /dev/null +++ b/src/providers/AuthProvider.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; +import { + initKeycloak, + logout as keycloakLogout, + getUserInfo, + isAuthenticated as isKeycloakAuthenticated, +} from "@/lib/keycloak-client"; + +interface AuthContextType { + isAuthenticated: boolean; + isLoading: boolean; + userInfo: any; + logout: () => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [userInfo, setUserInfo] = useState(null); + + useEffect(() => { + let mounted = true; + + async function initAuth() { + try { + const authenticated = await initKeycloak(); + + if (mounted) { + setIsAuthenticated(authenticated); + setIsLoading(false); + if (authenticated) { + setUserInfo(getUserInfo()); + } + } + } catch (error) { + console.error("Auth initialization failed:", error); + if (mounted) { + setIsLoading(false); + } + } + } + + initAuth(); + + return () => { + mounted = false; + }; + }, []); + + const handleLogout = async () => { + await keycloakLogout(); + setIsAuthenticated(false); + setUserInfo(null); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/src/types/customer.ts b/src/types/customer.ts new file mode 100644 index 0000000..c9245b6 --- /dev/null +++ b/src/types/customer.ts @@ -0,0 +1,31 @@ +export interface Customer { + id: string; + branch: string; + name: string; + email: string; + phone: string; + company: string; + address: string; + status: "active" | "inactive" | "pending"; + createdAt: string; + updatedAt: string; +} + +export interface CreateCustomerInput { + branch: string; + name: string; + email: string; + phone: string; + company: string; + address: string; + status?: "active" | "inactive" | "pending"; +} + +export interface UpdateCustomerInput { + name?: string; + email?: string; + phone?: string; + company?: string; + address?: string; + status?: "active" | "inactive" | "pending"; +}