batman
This commit is contained in:
+66
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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', () => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user