diff --git a/.env.example b/.env.example index 977675b..1b858a4 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,14 @@ -# Forte — Development Environment Variables -# These are used inside Docker Compose (docker-compose.dev.yml overrides most of these) +# Forte — Environment Variables +# Copy to .env and adjust values for your setup. +# Docker Compose overrides host values (postgres, valkey) automatically. -# Database -DATABASE_URL=postgresql://forte:forte@postgres:5432/forte +# Database (PostgreSQL 16) +DATABASE_URL=postgresql://forte:forte@localhost:5432/forte -# Valkey (Redis-compatible) -REDIS_URL=redis://valkey:6379 +# Valkey / Redis +REDIS_URL=redis://localhost:6379 -# JWT +# JWT — use a strong random secret in production JWT_SECRET=change-me-in-production-use-a-long-random-string # API Server @@ -16,3 +17,17 @@ HOST=0.0.0.0 # Environment NODE_ENV=development + +# Logging (optional) +# LOG_LEVEL=info +# LOG_FILE=./logs/forte.log + +# File Storage (optional — defaults to local) +# STORAGE_PROVIDER=local +# STORAGE_LOCAL_PATH=./data/files + +# CORS (optional — defaults to * in development) +# CORS_ORIGINS=https://admin.example.com + +# Frontend URL (used in password reset links) +# APP_URL=http://localhost:5173 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b54f25 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Forte + +Music store management platform — POS, inventory, rentals, lessons, repairs, and accounting. + +Built by [Lunarfront Tech LLC](https://lunarfront.com). + +## Tech Stack + +- **Runtime:** Bun +- **Language:** TypeScript (end-to-end) +- **API:** Fastify + Drizzle ORM + PostgreSQL 16 +- **Frontend:** React + TanStack Router + TanStack Query +- **Validation:** Zod (shared schemas) +- **Queue/Cache:** BullMQ + Valkey 8 +- **Monorepo:** Turborepo + Bun workspaces + +## Quick Start + +```bash +bun install +cp .env.example .env # configure DATABASE_URL, REDIS_URL, JWT_SECRET +cd packages/backend && bunx drizzle-kit migrate +bun run dev # starts backend (:8000) + admin UI (:5173) +``` + +## Packages + +| Package | Description | +|---------|-------------| +| `packages/backend` | Fastify API server | +| `packages/admin` | Admin UI (React + Vite) | +| `packages/shared` | Zod schemas, types, shared utils | + +## Documentation + +| Doc | Description | +|-----|-------------| +| [Setup](docs/setup.md) | Prerequisites, environment, installation, running | +| [Architecture](docs/architecture.md) | Monorepo structure, backend/frontend design, state management | +| [API Reference](docs/api.md) | All endpoints, pagination, auth, permissions | +| [Database](docs/database.md) | Schema overview, migrations, multi-tenancy | +| [Testing](docs/testing.md) | Test runner, suites, writing tests, assertions | + +## Commands + +```bash +bun run dev # start all packages in dev mode +bun run test # run all tests +bun run lint # lint all packages +bun run format # format with Prettier +``` + +## License + +Proprietary. All rights reserved. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..2e85f55 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,146 @@ +# API Reference + +Base URL: `http://localhost:8000/v1` + +All authenticated endpoints require `Authorization: Bearer `. Registration and login require `X-Company-ID` header. + +## Pagination + +Every list endpoint accepts: + +| Param | Default | Description | +|-------|---------|-------------| +| `page` | `1` | Page number | +| `limit` | `25` | Items per page (max 100) | +| `q` | — | Search query (ilike across relevant columns) | +| `sort` | varies | Sort field name | +| `order` | `asc` | `asc` or `desc` | + +Response shape: + +```json +{ + "data": [...], + "pagination": { + "page": 1, + "limit": 25, + "total": 142, + "totalPages": 6 + } +} +``` + +## Auth + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | `/auth/register` | No (needs `X-Company-ID`) | Create user account | +| POST | `/auth/login` | No | Login, returns JWT | +| GET | `/auth/me` | Yes | Current user profile | +| PATCH | `/auth/me` | Yes | Update profile (firstName, lastName) | +| POST | `/auth/change-password` | Yes | Change password | +| POST | `/auth/reset-password/:userId` | Yes (`users.admin`) | Generate password reset link | + +## Accounts + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/accounts` | `accounts.view` | List accounts (paginated, searchable) | +| POST | `/accounts` | `accounts.edit` | Create account | +| GET | `/accounts/:id` | `accounts.view` | Get account | +| PATCH | `/accounts/:id` | `accounts.edit` | Update account | +| DELETE | `/accounts/:id` | `accounts.admin` | Soft-delete account | + +## Members + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/members` | `accounts.view` | List all members (paginated) | +| GET | `/accounts/:id/members` | `accounts.view` | List members for account | +| POST | `/accounts/:id/members` | `accounts.edit` | Create member | +| GET | `/members/:id` | `accounts.view` | Get member | +| PATCH | `/members/:id` | `accounts.edit` | Update member | +| DELETE | `/members/:id` | `accounts.admin` | Delete member | +| POST | `/members/:id/move` | `accounts.edit` | Move member to another account | + +## Member Sub-Resources + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/members/:id/identifiers` | `accounts.view` | List identifiers | +| POST | `/members/:id/identifiers` | `accounts.edit` | Create identifier | +| PATCH | `/identifiers/:id` | `accounts.edit` | Update identifier | +| DELETE | `/identifiers/:id` | `accounts.admin` | Delete identifier | +| GET | `/accounts/:id/payment-methods` | `accounts.view` | List payment methods | +| POST | `/accounts/:id/payment-methods` | `accounts.edit` | Create payment method | +| PATCH | `/payment-methods/:id` | `accounts.edit` | Update payment method | +| DELETE | `/payment-methods/:id` | `accounts.admin` | Delete payment method | +| GET | `/accounts/:id/tax-exemptions` | `accounts.view` | List tax exemptions | +| POST | `/accounts/:id/tax-exemptions` | `accounts.edit` | Create tax exemption | +| PATCH | `/tax-exemptions/:id` | `accounts.edit` | Update tax exemption | +| DELETE | `/tax-exemptions/:id` | `accounts.admin` | Delete tax exemption | +| GET | `/accounts/:id/processor-links` | `accounts.view` | List processor links | +| POST | `/accounts/:id/processor-links` | `accounts.edit` | Create processor link | +| PATCH | `/processor-links/:id` | `accounts.edit` | Update processor link | +| DELETE | `/processor-links/:id` | `accounts.admin` | Delete processor link | + +## Users & RBAC + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/users` | `users.view` | List users (paginated, includes roles) | +| PATCH | `/users/:id/status` | `users.admin` | Enable/disable user | +| GET | `/users/:id/roles` | `users.view` | Get user's roles | +| POST | `/users/:id/roles` | `users.edit` | Assign role to user | +| DELETE | `/users/:id/roles/:roleId` | `users.edit` | Remove role from user | +| GET | `/permissions` | `users.view` | List all permissions | +| GET | `/roles` | `users.view` | List roles (paginated) | +| GET | `/roles/all` | `users.view` | List all roles (unpaginated, for dropdowns) | +| GET | `/roles/:id` | `users.view` | Get role with permissions | +| POST | `/roles` | `users.admin` | Create custom role | +| PATCH | `/roles/:id` | `users.admin` | Update role | +| DELETE | `/roles/:id` | `users.admin` | Delete custom role | +| GET | `/me/permissions` | Yes | Current user's permissions + roles | + +## Files + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/files?entityType=&entityId=` | `files.view` | List files for entity | +| POST | `/files` | `files.upload` | Upload file (multipart) | +| GET | `/files/:id` | `files.view` | Get file metadata | +| GET | `/files/serve/*` | `files.view` | Serve file content | +| DELETE | `/files/:id` | `files.delete` | Delete file | + +Upload accepts multipart form with fields: `file`, `entityType`, `entityId`, `category`. + +Valid entity types: `user`, `member`, `member_identifier`, `product`, `rental_agreement`, `repair_ticket`. + +## Products & Inventory + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/products` | `inventory.view` | List products (paginated) | +| POST | `/products` | `inventory.edit` | Create product | +| GET | `/products/:id` | `inventory.view` | Get product | +| PATCH | `/products/:id` | `inventory.edit` | Update product | +| DELETE | `/products/:id` | `inventory.admin` | Delete product | +| GET | `/categories` | `inventory.view` | List categories | +| POST | `/categories` | `inventory.edit` | Create category | +| GET | `/suppliers` | `inventory.view` | List suppliers | +| POST | `/suppliers` | `inventory.edit` | Create supplier | + +## Lookup Tables + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| GET | `/lookups/unit-statuses` | `inventory.view` | List unit statuses | +| POST | `/lookups/unit-statuses` | `inventory.admin` | Create custom status | +| GET | `/lookups/item-conditions` | `inventory.view` | List item conditions | +| POST | `/lookups/item-conditions` | `inventory.admin` | Create custom condition | + +## Health + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/health` | No | Health check | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..7d690cd --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,120 @@ +# Architecture + +## Monorepo Structure + +``` +forte/ + packages/ + shared/ @forte/shared — Zod schemas, types, business logic, utils + backend/ @forte/backend — Fastify API server + admin/ @forte/admin — Admin UI (React + Vite) + planning/ Domain planning docs (01-26) + docs/ Technical documentation +``` + +Managed with Turborepo and Bun workspaces. `@forte/shared` is a dependency of both `backend` and `admin`. + +## Backend + +**Fastify** API server with a plugin-based architecture. + +``` +src/ + main.ts App entry, plugin registration, server start + plugins/ + database.ts Drizzle ORM + PostgreSQL connection + auth.ts JWT auth, permission checking, inheritance + redis.ts Valkey/Redis (ioredis) + cors.ts CORS configuration + storage.ts File storage provider registration + error-handler.ts Centralized error → HTTP response mapping + dev-auth.ts Dev-only auth bypass + routes/v1/ + auth.ts Register, login, password change, reset, profile + accounts.ts Accounts, members, identifiers, payment methods, etc. + rbac.ts Users list, roles CRUD, role assignments, permissions + files.ts File upload/download/delete + services/ + account.service.ts Account + member business logic + rbac.service.ts Roles, permissions, user role management + file.service.ts File validation, storage, metadata + product.service.ts Products + inventory + lookup.service.ts Lookup table management + inventory.service.ts Stock receipts, unit tracking + db/ + schema/ Drizzle table definitions + migrations/ SQL migrations (drizzle-kit) + seeds/ System permission + role definitions + storage/ + provider.ts StorageProvider interface + local.ts Local filesystem provider + s3.ts S3 provider (placeholder) + utils/ + pagination.ts withPagination, withSort, buildSearchCondition, paginatedResponse +``` + +### Request Flow + +1. Fastify receives request +2. `onRequest` hook sets `companyId` from header +3. `authenticate` preHandler verifies JWT, loads permissions, checks `is_active` +4. `requirePermission` preHandler checks user has required permission slug +5. Route handler validates input with Zod, calls service, returns response +6. Error handler catches typed errors and maps to HTTP status codes + +### Permission Inheritance + +Permissions follow a hierarchy: `admin` implies `edit`, which implies `view` for the same domain. Having `accounts.admin` automatically grants `accounts.edit` and `accounts.view`. Non-hierarchical actions (`upload`, `delete`, `send`, `export`) don't cascade. + +## Frontend (Admin UI) + +**React** SPA with TanStack Router (file-based routing) and TanStack Query (data fetching). + +``` +src/ + routes/ + _authenticated.tsx Layout with sidebar, permission-gated nav + _authenticated/ + accounts/ Account list + detail pages + members/ Member list + detail pages + users.tsx Users admin page + roles/ Roles list + create/edit pages + profile.tsx User profile + settings + help.tsx In-app wiki + login.tsx Login page + api/ React Query options + mutations per domain + components/ + ui/ shadcn/ui primitives + shared/ DataTable, AvatarUpload + accounts/ Domain-specific forms + stores/ + auth.store.ts Zustand — token, user, permissions + theme.store.ts Zustand — color theme + light/dark mode + hooks/ + use-pagination.ts URL-based pagination state + lib/ + api-client.ts Fetch wrapper with JWT + error handling + themes.ts Color theme definitions + wiki/ + index.ts In-app help content +``` + +### State Management + +| Concern | Solution | +|---------|----------| +| Auth + permissions | Zustand store, persisted to sessionStorage | +| Server data | TanStack Query (cache, refetch, invalidation) | +| URL state (pagination) | TanStack Router search params | +| Theme | Zustand store, persisted to localStorage | +| Component state | React `useState` | + +## Multi-Tenancy + +Every domain table has a `company_id` column. All queries filter by the authenticated user's company. Location-scoped tables (inventory, transactions) additionally filter by `location_id`. + +## Database + +PostgreSQL 16 with Drizzle ORM. Migrations are generated with `bunx drizzle-kit generate` and applied with `bunx drizzle-kit migrate`. Schema files live in `packages/backend/src/db/schema/`. + +Key tables: `company`, `location`, `user`, `account`, `member`, `member_identifier`, `product`, `inventory_unit`, `role`, `permission`, `user_role_assignment`, `role_permission`, `file`. diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..d955b76 --- /dev/null +++ b/docs/database.md @@ -0,0 +1,86 @@ +# Database + +## Setup + +PostgreSQL 16. Two databases: + +| Database | Port | Usage | +|----------|------|-------| +| `forte` | 5432 | Development | +| `forte_api_test` | 5432 | API integration tests (auto-created by test runner) | + +## Migrations + +Migrations are managed by Drizzle Kit and live in `packages/backend/src/db/migrations/`. + +```bash +cd packages/backend + +# Generate a migration from schema changes +bunx drizzle-kit generate + +# Apply pending migrations +bunx drizzle-kit migrate +``` + +Schema files: `packages/backend/src/db/schema/` + +## Multi-Tenancy + +All domain tables include `company_id` (uuid FK to `company`). Every query filters by the authenticated user's company. Location-scoped tables additionally include `location_id`. + +## Schema Overview + +### Core + +| Table | Description | +|-------|-------------| +| `company` | Tenant (music store business) | +| `location` | Physical store location | +| `user` | Staff/admin user account | + +### Accounts & Members + +| Table | Description | +|-------|-------------| +| `account` | Billing entity (family, individual, business) | +| `member` | Individual person on an account | +| `member_identifier` | ID documents (DL, passport, school ID) | +| `payment_method` | Stored payment methods | +| `processor_link` | Payment processor integrations | +| `tax_exemption` | Tax exemption records | + +### Inventory + +| Table | Description | +|-------|-------------| +| `product` | Product catalog entry | +| `inventory_unit` | Individual serialized/non-serialized unit | +| `stock_receipt` | Incoming stock records | +| `category` | Product categories | +| `supplier` | Product suppliers | +| `inventory_unit_status` | Lookup: unit statuses | +| `item_condition` | Lookup: item conditions | + +### RBAC + +| Table | Description | +|-------|-------------| +| `permission` | System permissions (global, seeded) | +| `role` | Company-scoped roles (system + custom) | +| `role_permission` | Role-to-permission mapping | +| `user_role_assignment` | User-to-role mapping | + +### Files + +| Table | Description | +|-------|-------------| +| `file` | File metadata (path, type, size, entity reference) | + +## Key Conventions + +- UUIDs for all primary keys (`defaultRandom()`) +- `created_at` and `updated_at` timestamps with timezone on all tables +- Soft deletes via `is_active` boolean where applicable +- Auto-generated sequential numbers: `account_number` (6-digit), `member_number` +- Lookup tables support both system (immutable) and custom (company-scoped) values diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..f445a2f --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,89 @@ +# Development Setup + +## Prerequisites + +- [Bun](https://bun.sh) v1.1+ +- PostgreSQL 16 +- Valkey 8 (or Redis 7+) + +## Installation + +```bash +git clone && cd forte +bun install +``` + +## Environment + +Create a `.env` file in the project root: + +```env +DATABASE_URL=postgresql://forte:forte@localhost:5432/forte +REDIS_URL=redis://localhost:6379 +JWT_SECRET=your-secret-here +NODE_ENV=development +``` + +### All Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DATABASE_URL` | `postgresql://forte:forte@localhost:5432/forte` | PostgreSQL connection string | +| `REDIS_URL` | `redis://localhost:6379` | Valkey/Redis connection string | +| `JWT_SECRET` | (auto-generated in dev) | Secret for signing JWTs. **Required in production.** | +| `PORT` | `8000` | Backend API port | +| `HOST` | `0.0.0.0` | Backend bind address | +| `NODE_ENV` | — | `development`, `test`, or `production` | +| `LOG_LEVEL` | `info` | Pino log level (`debug`, `info`, `warn`, `error`, `silent`) | +| `LOG_FILE` | — | Path to log file (production only, also logs to stdout) | +| `STORAGE_PROVIDER` | `local` | File storage provider (`local` or `s3`) | +| `STORAGE_LOCAL_PATH` | `./data/files` | Local file storage directory | +| `CORS_ORIGINS` | `*` (dev) | Comma-separated allowed origins | +| `APP_URL` | `http://localhost:5173` | Frontend URL (used in password reset links) | + +## Database + +```bash +# Create the database +createdb forte + +# Run migrations +cd packages/backend +source ../.env # or source .env from project root +bunx drizzle-kit migrate +``` + +## Running + +```bash +# From project root — starts all packages +bun run dev + +# Or individually: +cd packages/backend && source .env && bun run dev # API on :8000 +cd packages/admin && bun run dev # UI on :5173 +``` + +## Testing + +```bash +# API integration tests (starts a backend, seeds DB, runs HTTP tests) +cd packages/backend +source .env +bun run api-test + +# Filter by suite or tag +bun run api-test --suite accounts +bun run api-test --tag crud + +# Unit tests +bun run test +``` + +## Useful Commands + +```bash +bun run lint # ESLint across all packages +bun run format # Prettier write +bun run build # Build all packages +``` diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..4db51a4 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,81 @@ +# Testing + +## API Integration Tests + +The primary test suite lives at `packages/backend/api-tests/`. It uses a custom runner that: + +1. Creates/migrates a `forte_api_test` database +2. Seeds company, lookup tables, RBAC permissions/roles +3. Starts the backend on port 8001 +4. Registers a test user with admin role +5. Runs test suites via HTTP requests +6. Tears down the server + +### Running + +```bash +cd packages/backend +source .env # needs DB credentials +bun run api-test +``` + +### Filtering + +```bash +bun run api-test --suite accounts # run only the accounts suite +bun run api-test --tag crud # run tests tagged 'crud' +``` + +### Suites + +| Suite | Tests | Coverage | +|-------|-------|----------| +| `accounts` | 17 | CRUD, search, pagination, sort, billing mode | +| `members` | 19 | CRUD, search, move, isMinor, address inheritance | +| `files` | 7 | Upload, list, get, delete, validation, profile pictures | +| `rbac` | 21 | Permission enforcement, role CRUD, user status, pagination | + +### Writing Tests + +Tests are defined in `api-tests/suites/`. Each file exports a suite: + +```typescript +import { suite } from '../lib/context.js' + +suite('MyDomain', { tags: ['domain'] }, (t) => { + t.test('does something', { tags: ['crud'] }, async () => { + const res = await t.api.post('/v1/endpoint', { name: 'Test' }) + t.assert.status(res, 201) + t.assert.equal(res.data.name, 'Test') + }) +}) +``` + +Available on `t`: +- `t.api` — HTTP client (`get`, `post`, `patch`, `del`) with auth token +- `t.token` — JWT token string +- `t.baseUrl` — Backend URL +- `t.assert` — Assertion helpers (`status`, `equal`, `ok`, `includes`, `greaterThan`, etc.) + +### Assert Methods + +| Method | Description | +|--------|-------------| +| `status(res, code)` | Check HTTP status | +| `equal(a, b)` | Strict equality | +| `notEqual(a, b)` | Strict inequality | +| `ok(value)` | Truthy check | +| `includes(arr, value)` | Array includes | +| `contains(str, sub)` | String contains | +| `greaterThan(a, b)` | Numeric comparison | + +## Unit Tests + +Unit tests use `bun:test` and live alongside the code or in `__tests__/`: + +```bash +cd packages/backend +bun run test +``` + +Current unit test coverage: shared utils (date helpers, currency formatting, state normalization).