This commit is contained in:
2026-09-19 22:19:35 +02:00
commit 231b3e006a
124 changed files with 13816 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
# API Documentation
Base URL: `http://localhost:3001/api/v1`
Swagger UI: `http://localhost:3001/api/docs`
## Authentication
### Dashboard Users
- Login to receive JWT access token (24h) + refresh token (30 days)
- Include access token in `Authorization: Bearer <token>` header
### Devices
- Register to receive device bearer token (90 days)
- Include device token in `Authorization: Bearer <token>` header
- Device ID must match URL parameter
## Endpoints
### Health
`GET /health` - Server health check
### Auth
`POST /auth/login` - Login with email/password
`POST /auth/refresh` - Refresh access token
`POST /auth/logout` - Logout and revoke refresh token
### Venues
`GET /venues` - List venues
`GET /venues/:venueId` - Get venue details
`GET /venues/:venueId/devices` - List devices in venue
### Devices
`POST /devices/register` - Register a new device
`POST /devices/:deviceId/sync` - Sync device state (device auth)
`POST /devices/:deviceId/commands/:commandId/ack` - Acknowledge command (device auth)
`GET /devices/generation-code` - Generate registration code (admin)
### Tabs
`GET /venues/:venueId/tabs` - List open tabs
### Commands
`POST /venues/:venueId/commands` - Create a command
`GET /venues/:venueId/commands` - List command history
`POST /venues/:venueId/devices/:deviceId/revoke` - Revoke device (admin)
## Error Format
```json
{
"error": {
"code": "error_code",
"message": "Human-readable message"
}
}
```
## HTTP Status Codes
- 200 Success
- 201 Created
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 409 Conflict
- 429 Too Many Requests
+49
View File
@@ -0,0 +1,49 @@
# Architecture Decisions
## 1. Registration Code Model
**Decision:** Added a `RegistrationCode` model to the database schema.
**Rationale:** The specification requires a registration-code mechanism. Rather than overloading an existing model or implementing a complex external system, a simple `RegistrationCode` table with venue scoping, expiration, and single-use semantics provides a clean implementation.
## 2. Payment Method Must Be Explicitly Selected
**Decision:** The UI never defaults to `cash`. The user must explicitly select a payment method.
**Rationale:** The specification explicitly forbids silent defaults. This prevents accidental charges and ensures compliance with the business requirement.
## 3. Cursor Implementation
**Decision:** Opaque cursor encoded as base64url containing timestamp + UUID tiebreaker.
**Rationale:** Provides a simple, privacy-preserving cursor that allows the server to determine which commands the device has already seen without exposing database internals.
## 4. Refresh Token Storage
**Decision:** Refresh tokens are stored hashed (SHA-256) in a `RefreshToken` table, supporting revocation and rotation.
**Rationale:** Provides security (tokens can be revoked) and aligns with OAuth2 best practices for refresh token management.
## 5. SQLite for Development
**Decision:** SQLite is the default development database.
**Rationale:** Zero configuration, fast for local development, and Prisma abstracts most differences. PostgreSQL migration path is documented in production.md.
## 6. No WebSockets
**Decision:** Dashboard uses polling (TanStack Query) instead of WebSockets.
**Rationale:** Simpler implementation, adequate for the expected update frequency (5-30 second intervals), and avoids WebSocket server infrastructure.
## 7. In-Memory Rate Limiting
**Decision:** Use NestJS ThrottlerModule with in-memory storage.
**Rationale:** Suitable for single-instance deployments. The abstraction allows swapping to Redis without controller changes.
## 8. Venue Isolation via JWT Claims
**Decision:** Venue ID is embedded in the JWT token. All service methods verify the requesting user/device's venue context rather than trusting URL parameters alone.
**Rationale:** Prevents cross-venue access even if a user manually modifies URL parameters.
+43
View File
@@ -0,0 +1,43 @@
# KoolTab Architecture
## Overview
KoolTab is a modular monolith designed for remote tab control in venues.
## Components
### Backend (NestJS)
- REST API with versioned endpoints (`/api/v1`)
- JWT authentication for dashboard users
- Bearer token authentication for devices
- SQLite database via Prisma ORM
- Scheduled command expiry job
- In-memory rate limiting
### Frontend (Next.js)
- App Router with client-side rendering
- TanStack Query for API data fetching
- shadcn/ui component library
- Tailwind CSS styling
- Secure HttpOnly cookie-based auth
### Shared Package
- TypeScript types and constants
- Shared across API and dashboard
## Data Flow
1. Device syncs open tabs → Server stores snapshots + ownership
2. Dashboard creates close command → Server resolves device owner
3. Device syncs → Receives pending commands
4. Device processes → Acknowledges command
5. Dashboard polls → Sees updated status
## Security Model
- Every user/device scoped to one venue
- Venue isolation enforced at service level
- Device tokens hashed with SHA-256
- Raw token returned only once during registration
- JWT tokens with refresh token rotation
- Rate limiting on sensitive endpoints
+64
View File
@@ -0,0 +1,64 @@
# Local Development
## Prerequisites
- Node.js >= 18
- pnpm >= 8
## Setup
```bash
# Clone the repository
git clone <repo>
cd kooltab
# Install dependencies
pnpm install
# Copy environment variables
cp .env.example apps/api/.env
# Generate Prisma client
cd apps/api
pnpm db:generate
# Run migrations
pnpm db:migrate
# Seed demo data
pnpm db:seed
cd ../..
```
## Running
```bash
# Start both API and Dashboard
pnpm dev
```
Or individually:
```bash
cd apps/api && pnpm dev
cd apps/dashboard && pnpm dev
```
## Database Commands
```bash
pnpm db:generate # Generate Prisma client
pnpm db:migrate # Run migrations
pnpm db:seed # Seed demo data
pnpm db:reset # Reset database and re-seed
```
## Environment Variables
### API (`apps/api/.env`)
- `DATABASE_URL` - SQLite path (default: `file:./dev.db`)
- `JWT_SECRET` - JWT signing secret
- `JWT_REFRESH_SECRET` - Refresh token secret
- `PORT` - API port (default: 3001)
- `CORS_ORIGIN` - Allowed CORS origin (default: http://localhost:3000)
### Dashboard
- `NEXT_PUBLIC_API_URL` - API base URL (default: http://localhost:3001/api/v1)
+122
View File
@@ -0,0 +1,122 @@
# KoolTab Production Deployment
## Option A: Docker (recommended)
The production setup uses PostgreSQL and configurable secrets.
### 1. Set environment variables
Create `.env` in the project root:
```env
# Database
DB_PASSWORD=<generate-a-strong-password>
# JWT secrets — generate with: openssl rand -hex 64
JWT_SECRET=<64-char-hex-string>
JWT_REFRESH_SECRET=<64-char-hex-string>
# URLs — set your actual domain/port
DASHBOARD_URL=https://dashboard.yourdomain.com
API_URL=https://api.yourdomain.com
DASHBOARD_PORT=3000
API_PORT=3001
```
### 2. Build and start
```bash
docker compose -f docker-compose.prod.yml up --build -d
```
### 3. Seed the database (first time only)
```bash
# Generate Prisma client and run migrations
docker compose -f docker-compose.prod.yml exec api npx prisma migrate deploy
# Seed demo data (or create your own venue/users)
docker compose -f docker-compose.prod.yml exec api npx ts-node prisma/seed.ts
```
### 4. Set up a reverse proxy (nginx/Caddy)
Example Caddyfile:
```
dashboard.yourdomain.com {
reverse_proxy dashboard:3000
}
api.yourdomain.com {
reverse_proxy api:3001
}
```
---
## Option B: Manual (Node.js + PostgreSQL)
### 1. Install PostgreSQL
Create database and user:
```sql
CREATE DATABASE kooltab;
CREATE USER kooltab WITH PASSWORD 'your-password';
GRANT ALL PRIVILEGES ON DATABASE kooltab TO kooltab;
```
### 2. Configure environment
```env
DATABASE_URL="postgresql://kooltab:your-password@localhost:5432/kooltab"
JWT_SECRET="<random-64-char-string>"
JWT_REFRESH_SECRET="<random-64-char-string>"
PORT=3001
CORS_ORIGIN="https://dashboard.yourdomain.com"
```
### 3. Update Prisma schema for PostgreSQL
In `apps/api/prisma/schema.prisma`, change:
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
### 4. Build and run
```bash
cd apps/api
pnpm install
npx prisma generate
npx prisma migrate deploy
pnpm build
node dist/main
```
---
## Migration Steps: SQLite → PostgreSQL
1. Update `schema.prisma` to use `postgresql` provider
2. Update `DATABASE_URL` environment variable
3. Run `prisma migrate dev` to create initial PostgreSQL migration
4. Export SQLite data and import to PostgreSQL (using `prisma db pull` + manual script, or insert via seed)
5. Run `prisma migrate deploy` on production
Key differences to handle:
- SQLite uses `Int` for booleans → use `Boolean` in PostgreSQL
- SQLite uses `String` for JSON → use `Json` type in PostgreSQL
- UUID generation is handled by Prisma on both
## Security Checklist
- [ ] JWT secrets are 64+ random hex characters
- [ ] DB_PASSWORD is strong and unique
- [ ] HTTPS is terminated at reverse proxy
- [ ] CORS_ORIGIN is set to the exact dashboard URL (no wildcard)
- [ ] API is not exposed directly to the internet (reverse proxy only)
- [ ] Rate limiting is configured
- [ ] Audit logs are monitored
+52
View File
@@ -0,0 +1,52 @@
# Security
## Authentication
### Device Tokens
- Format: `dev_` + 48 random bytes, base64url-encoded
- Stored as SHA-256 hash in database
- Raw token returned only once during registration
- Expire after 90 days
- Revocation immediately invalidates token
### Client Authentication
- Passwords hashed with bcrypt (10 rounds)
- JWT access tokens (24h expiration)
- Refresh tokens with rotation and revocation
- Tokens stored in HttpOnly cookies on dashboard
## Authorization
### Role-Based Access
- `admin`: Full access including device management and revocation
- `viewer`: Read-only access to tabs, devices, and commands
### Venue Isolation
- Every user and device is scoped to exactly one venue
- All service methods validate venue context from authentication
- Cross-venue access is blocked at the service layer
## API Security
### Headers
- Helmet for security headers
- Strict CORS (dashboard origin only)
- Body size limits enforced
### Rate Limiting
- Device sync: 1 request per 30 seconds per device
- Command creation: 10 requests per minute per user
- General: 100 requests per minute per client
### Error Handling
- Consistent error format never exposes internals
- No secrets in error messages
- No stack traces in production
## Audit Trail
All sensitive operations are logged:
- Command creation, acknowledgement, expiration
- Device registration and revocation
- Snapshot conflicts
- No sensitive data (passwords, tokens) in audit logs
+56
View File
@@ -0,0 +1,56 @@
# Testing
## Running Tests
```bash
# All unit tests
pnpm test
# E2E tests
pnpm test:e2e
# Specific test file
cd apps/api
npx jest --testPathPattern="auth.service.spec.ts"
```
## Test Structure
### Unit Tests
Located in `apps/api/src/` alongside source files with `.spec.ts` suffix.
- Authentication service tests
- Device token generation and validation
- Command creation and acknowledgement
- Venue isolation enforcement
- Rate limiting
### Integration/E2E Tests
Located in `apps/api/test/` with `.e2e-spec.ts` suffix.
- Login flow
- Device registration and sync
- Command lifecycle (create → sync → ack)
- Device revocation
- Cross-venue access prevention
## Writing Tests
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService, /* mocks */],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should generate valid device tokens', () => {
// ...
});
});
```