diff --git a/.gitignore b/.gitignore index 0e6afd70..34ac7a78 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ CLAUDE.md .github/instructions/ # AI-specific / internal planning docs -docs/ +docs/brain/ skills/ # Server runtime artifacts diff --git a/README.md b/README.md index ac50a0c9..46db8e47 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,46 @@ dependencies, works fully on LAN. ## Quick Start -1. Download the latest release from +1. Download `chatserver.exe` and the OwnCord installer from [GitHub Releases](https://github.com/J3vb/OwnCord/releases) -2. Run `chatserver.exe` — generates `config.yaml` on first run -3. Open `https://localhost:8443/admin` to access the admin panel -4. Generate an invite code and share it with friends -5. Friends download the client installer and connect - using your server address +2. Run `chatserver.exe` — generates `config.yaml` and a `data/` + directory (database, TLS certs, uploads, backups) on first run +3. Open `https://localhost:8443/admin` to create the Owner account +4. Generate an invite code in the admin panel and share it +5. Friends install the client, enter your server address + (`ip:8443`), and register with the invite code + +The client uses TOFU (Trust On First Use) for self-signed +certificates — it prompts to trust the server on first +connection, then pins it for future sessions. + +### Voice & Video Setup (Optional) + +Voice and video require [LiveKit Server](https://github.com/livekit/livekit/releases): + +1. Download `livekit-server` from the LiveKit releases page +2. Edit `config.yaml` and set: + ```yaml + voice: + livekit_api_key: "devkey" # any string + livekit_api_secret: "secret-min-32-characters-long!!" # min 32 chars + livekit_binary: "C:/path/to/livekit-server.exe" + ``` +3. Restart `chatserver.exe` — it auto-starts LiveKit as a + companion process + +### Networking + +For friends outside your LAN, you need to forward these ports: + +| Port | Protocol | Purpose | +| ---- | -------- | ------- | +| `8443` | TCP | HTTPS, WebSocket, REST API | +| `7881` | TCP | LiveKit signaling (voice/video) | +| `50000-60000` | UDP | LiveKit WebRTC media (voice/video) | + +Alternatively, use Tailscale for zero-config networking +with no port forwarding. ## Architecture @@ -223,7 +256,18 @@ npm run lint:fix # ESLint auto-fix ## Configuration -The server generates a `config.yaml` on first run. Key settings: +The server generates a `config.yaml` on first run. All runtime data +is stored in a `data/` directory alongside the executable: + +```text +data/ +├── owncord.db # SQLite database +├── certs/ # TLS certificates (auto-generated if self_signed) +├── uploads/ # User-uploaded files +└── backups/ # Database backups +``` + +Key settings: | Setting | Default | Description | | ------- | ------- | ----------- | @@ -252,19 +296,27 @@ To enable signed releases in CI, add these GitHub repository secrets: ## Documentation -Detailed docs live in the `docs/brain/` Obsidian vault: +- [Quick Start Guide](docs/quick-start.md) +- [Server Configuration](docs/server-configuration.md) +- [LiveKit Setup (Voice/Video)](docs/livekit-setup.md) +- [Deployment Guide](docs/deployment.md) +- [Port Forwarding](docs/port-forwarding.md) +- [Tailscale Guide](docs/tailscale.md) +- [REST API Reference](docs/api.md) +- [WebSocket Protocol](docs/protocol.md) +- [Database Schema](docs/schema.md) +- [Client Architecture](docs/client-architecture.md) +- [Contributing](docs/contributing.md) +- [Security Policy](docs/security.md) -- [Quick Start Guide](docs/brain/08-Guides/quick-start.md) -- [Port Forwarding Guide](docs/brain/08-Guides/port-forwarding.md) -- [Tailscale Guide](docs/brain/08-Guides/tailscale.md) -- [Client Architecture](docs/brain/06-Specs/CLIENT-ARCHITECTURE.md) -- [Server Spec](docs/brain/06-Specs/CHATSERVER.md) -- [WebSocket Protocol](docs/brain/06-Specs/PROTOCOL.md) -- [REST API](docs/brain/06-Specs/API.md) -- [Database Schema](docs/brain/06-Specs/SCHEMA.md) -- [Testing Strategy](docs/brain/06-Specs/TESTING-STRATEGY.md) -- [Contributing](docs/brain/08-Guides/CONTRIBUTING.md) -- [Security](docs/brain/08-Guides/SECURITY.md) +## Contributing + +1. Fork the repo and create a feature branch from `dev` +2. Follow existing code style and conventions +3. Write tests for new functionality +4. Open a PR against `dev` with a clear description + +See [Contributing Guide](docs/contributing.md) for details. ## Tech Stack diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..7ca9bd98 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,849 @@ +# REST API Reference + +OwnCord server REST API reference. All endpoints use the base URL `https://{server}:{port}/api/v1`. + +--- + +## Authentication + +All authenticated endpoints require a session token delivered via the `Authorization: Bearer {token}` header. Tokens are obtained from `POST /api/v1/auth/login`, `POST /api/v1/auth/register`, or `POST /api/v1/auth/verify-totp` after a partial 2FA challenge. + +### Session Lifecycle + +- Sessions are created on login/register and stored with a SHA-256 hash of the raw token, the client IP, User-Agent, and an expiry timestamp. +- Each authenticated request updates the session's `last_active` timestamp. +- Banned users are rejected at the middleware level with `403 FORBIDDEN`. + +### Middleware Stack (all routes) + +1. **RequestID** -- assigns a unique `X-Request-Id` response header. +2. **Recoverer** -- catches panics and returns 500. +3. **Request Logger** -- structured logging of method, path, status, duration. +4. **SecurityHeaders** -- sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `X-XSS-Protection: 0`, `Referrer-Policy: strict-origin-when-cross-origin`, `Content-Security-Policy: default-src 'self'`, `Permissions-Policy: camera=(), microphone=(), geolocation=()`, `Cache-Control: no-store`. +5. **MaxBodySize** -- 1 MiB default for all routes except `/api/v1/uploads` (which has its own 100 MiB limit). + +--- + +## Standard Error Response + +All error responses use this JSON envelope: + +```json +{ + "error": "ERROR_CODE", + "message": "Human-readable detail" +} +``` + +### Error Codes + +| Code | HTTP Status | When It Occurs | +| ---- | ----------- | -------------- | +| `UNAUTHORIZED` | 401 | Missing/invalid/expired session token | +| `INVALID_CREDENTIALS` | 401 | Login/register with bad username/password/invite (generic to prevent enumeration) | +| `FORBIDDEN` | 403 | Insufficient permissions, banned account, or admin IP restriction | +| `NOT_FOUND` | 404 | Resource (channel, message, user, invite, file, backup) not found | +| `RATE_LIMITED` | 429 | Too many requests; response includes `Retry-After` header (seconds) | +| `INVALID_INPUT` / `BAD_REQUEST` | 400 | Malformed body, missing required fields, invalid query params | +| `CONFLICT` | 409 | Duplicate username on register, or server already up-to-date on update | +| `TOO_LARGE` | 413 | File exceeds upload size limit | +| `SERVER_ERROR` / `INTERNAL` | 500 | Internal server error | +| `BAD_GATEWAY` | 502 | Upstream failure (GitHub API, LiveKit, asset download) | + +--- + +## Auth Endpoints + +### POST /api/v1/auth/register + +Create a new account using an invite code. The first user is created via `/admin/api/setup` instead. + +**Auth:** None (public) +**Rate limit:** 3 requests/minute per IP + +#### Request + +```json +{ + "username": "alex", + "password": "MyStr0ng!Pass", + "invite_code": "abc123def" +} +``` + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| `username` | string | Yes | HTML-stripped, trimmed. Must be non-empty. | +| `password` | string | Yes | Validated for strength (min length, complexity). | +| `invite_code` | string | Yes | Must be a valid, non-expired, non-revoked invite with remaining uses. | + +#### Response 201 Created + +```json +{ + "token": "raw-session-token-64-chars", + "user": { + "id": 2, + "username": "alex", + "avatar": "", + "status": "offline", + "role_id": 4, + "totp_enabled": false, + "created_at": "2026-03-24T12:00:00Z" + } +} +``` + +#### Errors + +| Status | Code | Cause | +| ------ | ---- | ----- | +| 400 | `INVALID_INPUT` | Missing username/password/invite_code, or weak password | +| 400 | `INVALID_CREDENTIALS` | Bad invite code, expired/revoked invite, or duplicate username | +| 403 | `FORBIDDEN` | Registration is closed or unavailable while server-wide 2FA is required | +| 429 | `RATE_LIMITED` | Exceeded 3 registrations/minute from this IP | +| 500 | `SERVER_ERROR` | Hashing failure, session creation failure, or DB error | + +--- + +### POST /api/v1/auth/login + +Authenticate with username and password. + +**Auth:** None (public) +**Rate limit:** 60 requests/minute per IP. After 10 consecutive failures from the same IP, the IP is locked out for 15 minutes. + +#### Request + +```json +{ + "username": "alex", + "password": "MyStr0ng!Pass" +} +``` + +#### Response 200 OK + +If the account does not have TOTP enabled: + +```json +{ + "token": "raw-session-token-64-chars", + "requires_2fa": false, + "user": { + "id": 1, + "username": "alex", + "avatar": "uuid.png", + "status": "offline", + "role_id": 4, + "totp_enabled": false, + "created_at": "2026-03-24T12:00:00Z" + } +} +``` + +If the account has TOTP enabled: + +```json +{ + "partial_token": "opaque-partial-token", + "requires_2fa": true +} +``` + +#### Errors + +| Status | Code | Cause | +| ------ | ---- | ----- | +| 400 | `INVALID_INPUT` | Missing username or password | +| 401 | `UNAUTHORIZED` | Wrong username or password | +| 403 | `FORBIDDEN` | Account is banned/suspended | +| 429 | `RATE_LIMITED` | IP locked out after 10 consecutive failures (15 min cooldown) | +| 500 | `SERVER_ERROR` | Session creation failure | + +--- + +### POST /api/v1/auth/verify-totp + +Complete a TOTP login challenge started by `POST /api/v1/auth/login`. + +**Auth:** Required with the `partial_token` from the login response +**Rate limit:** 10 requests/minute per IP, plus a 5-attempt budget per partial challenge + +#### Request + +```json +{ + "code": "123456" +} +``` + +#### Response 200 OK + +```json +{ + "token": "raw-session-token-64-chars", + "requires_2fa": false, + "user": { + "id": 1, + "username": "alex", + "avatar": "uuid.png", + "status": "offline", + "role_id": 4, + "totp_enabled": true, + "created_at": "2026-03-24T12:00:00Z" + } +} +``` + +#### Errors + +| Status | Code | Cause | +| ------ | ---- | ----- | +| 400 | `INVALID_INPUT` | Malformed request body | +| 401 | `UNAUTHORIZED` | Missing/expired challenge, invalid TOTP code, or challenge consumed | +| 500 | `SERVER_ERROR` | Session creation failure | + +--- + +### GET /api/v1/auth/me + +Get the current authenticated user's profile. + +**Auth:** Required (Bearer token) + +#### Response 200 OK + +```json +{ + "id": 1, + "username": "alex", + "avatar": "uuid.png", + "status": "online", + "role_id": 2, + "totp_enabled": true, + "created_at": "2026-03-24T12:00:00Z" +} +``` + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | int64 | User ID | +| `username` | string | Display name | +| `avatar` | string | Avatar filename (UUID) or empty string | +| `status` | string | One of: `online`, `idle`, `dnd`, `offline` | +| `role_id` | int64 | Numeric role ID (1=Owner, 2=Admin, 3=Moderator, 4=Member) | +| `totp_enabled` | bool | Whether the user has a confirmed TOTP secret | +| `created_at` | string | ISO 8601 timestamp | + +--- + +### POST /api/v1/auth/logout + +Invalidate the current session token. + +**Auth:** Required (Bearer token) + +#### Response 204 No Content + +--- + +### POST /api/v1/users/me/totp/enable + +Start TOTP enrollment for the authenticated user. The secret is not persisted until `/api/v1/users/me/totp/confirm` succeeds. + +**Auth:** Required +**Rate limit:** 5 requests/minute per IP + +#### Request + +```json +{ + "password": "MyStr0ng!Pass" +} +``` + +#### Response 200 OK + +```json +{ + "qr_uri": "otpauth://totp/OwnCord:alex?...", + "backup_codes": [] +} +``` + +--- + +### POST /api/v1/users/me/totp/confirm + +Confirm a pending TOTP enrollment. + +**Auth:** Required +**Rate limit:** 5 requests/minute per IP + +#### Request + +```json +{ + "password": "MyStr0ng!Pass", + "code": "123456" +} +``` + +#### Response 204 No Content + +--- + +### DELETE /api/v1/users/me/totp + +Disable TOTP for the authenticated user. + +**Auth:** Required +**Rate limit:** 5 requests/minute per IP + +#### Request + +```json +{ + "password": "MyStr0ng!Pass" +} +``` + +#### Response 204 No Content + +--- + +## Channel Endpoints + +### GET /api/v1/channels + +List all channels the authenticated user has `READ_MESSAGES` permission for. DM channels are NOT included (use `GET /api/v1/dms` instead). + +**Auth:** Required + +#### Response 200 OK + +```json +[ + { + "id": 1, + "name": "general", + "type": "text", + "topic": "Welcome to the server!", + "category": "Text Channels", + "position": 0, + "slow_mode": 0, + "archived": false + } +] +``` + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | int64 | Channel ID | +| `name` | string | Channel name | +| `type` | string | `text`, `voice`, or `announcement` | +| `topic` | string | Channel topic/description | +| `category` | string | Category grouping | +| `position` | int | Sort order within category | +| `slow_mode` | int | Slow-mode delay in seconds (0 = disabled) | +| `archived` | bool | Whether the channel is archived | + +--- + +### GET /api/v1/channels/{id}/messages + +Paginated message history for a channel. + +**Auth:** Required +**Permission:** `READ_MESSAGES` on the channel (or DM participant membership) + +#### Query Parameters + +| Param | Type | Default | Range | Description | +| ----- | ---- | ------- | ----- | ----------- | +| `before` | int64 | 0 (latest) | >= 0 | Cursor: return messages with ID less than this value | +| `limit` | int | 50 | 1-100 | Number of messages to return | + +#### Response 200 OK + +```json +{ + "messages": [ + { + "id": 1042, + "channel_id": 5, + "user": { + "id": 1, + "username": "alex", + "avatar": "uuid.png" + }, + "content": "Hello!", + "reply_to": null, + "attachments": [ + { + "id": "file-uuid", + "filename": "photo.jpg", + "size": 204800, + "mime_type": "image/jpeg", + "url": "/api/v1/files/file-uuid", + "width": 1920, + "height": 1080 + } + ], + "reactions": [ + { + "emoji": "\ud83d\udc4d", + "count": 2, + "me": true + } + ], + "pinned": false, + "edited_at": null, + "deleted": false, + "timestamp": "2026-03-14T10:30:00Z" + } + ], + "has_more": true +} +``` + +#### Pagination + +Use cursor-based pagination by passing the `id` of the last message as the `before` parameter: + +``` +GET /api/v1/channels/5/messages?before=1042&limit=50 +``` + +When `has_more` is `false`, you have reached the beginning of the channel history. + +--- + +### GET /api/v1/channels/{id}/pins + +Get all pinned messages for a channel. + +**Auth:** Required +**Permission:** `READ_MESSAGES` on the channel + +#### Response 200 OK + +Returns `{ "messages": [...], "has_more": false }`. `has_more` is always `false` for pins (all pinned messages are returned at once). + +--- + +### POST /api/v1/channels/{id}/pins/{messageId} + +Pin a message in a channel. + +**Auth:** Required +**Permission:** `MANAGE_MESSAGES` on the channel + +#### Response 204 No Content + +--- + +### DELETE /api/v1/channels/{id}/pins/{messageId} + +Unpin a message from a channel. + +**Auth:** Required +**Permission:** `MANAGE_MESSAGES` on the channel + +#### Response 204 No Content + +--- + +## Search + +### GET /api/v1/search + +Full-text search across messages in channels the user can read. Uses SQLite FTS5 for matching. + +**Auth:** Required + +#### Query Parameters + +| Param | Type | Default | Range | Description | +| ----- | ---- | ------- | ----- | ----------- | +| `q` | string | (required) | non-empty | Search query (FTS5 syntax) | +| `channel_id` | int64 | (all channels) | > 0 | Restrict search to a single channel | +| `limit` | int | 50 | 1-100 | Maximum results to return | + +#### Response 200 OK + +```json +{ + "results": [ + { + "message_id": 1042, + "channel_id": 5, + "channel_name": "general", + "user": { + "id": 1, + "username": "alex" + }, + "content": "...matched text...", + "timestamp": "2026-03-14T10:30:00Z" + } + ] +} +``` + +--- + +## Direct Messages + +DM channels use participant-based authorization rather than role-based permissions. + +### POST /api/v1/dms + +Create or retrieve a 1-on-1 DM channel with another user. If a DM channel already exists, it is returned and re-opened. + +**Auth:** Required + +#### Request + +```json +{ + "recipient_id": 2 +} +``` + +#### Response 200 OK (existing channel) or 201 Created (new channel) + +```json +{ + "channel_id": 100, + "recipient": { + "id": 2, + "username": "jordan", + "avatar": "uuid.png", + "status": "online" + }, + "created": false +} +``` + +--- + +### GET /api/v1/dms + +List all open DM channels for the authenticated user, ordered by most recent activity. + +**Auth:** Required + +#### Response 200 OK + +```json +{ + "dm_channels": [ + { + "channel_id": 100, + "recipient": { + "id": 2, + "username": "jordan", + "avatar": "uuid.png", + "status": "online" + }, + "last_message_id": 5042, + "last_message": "Hey, how's it going?", + "last_message_at": "2026-03-28T14:30:00Z", + "unread_count": 3 + } + ] +} +``` + +--- + +### DELETE /api/v1/dms/{channelId} + +Close a DM channel for the authenticated user (hides it from their sidebar). The channel and messages remain in the database. If the other user sends a new message, the channel is automatically re-opened. + +**Auth:** Required + +#### Response 204 No Content + +--- + +## Invite Endpoints + +All invite endpoints require authentication and the `MANAGE_INVITES` permission. + +### POST /api/v1/invites + +Create a new invite code. + +**Auth:** Required +**Permission:** `MANAGE_INVITES` + +#### Request + +```json +{ + "max_uses": 5, + "expires_in_hours": 48 +} +``` + +Both fields are optional. An empty body creates an invite with unlimited uses and no expiry. + +#### Response 201 Created + +```json +{ + "id": 1, + "code": "abc123def", + "max_uses": 5, + "uses": 0, + "expires_at": "2026-03-30T10:30:00Z", + "revoked": false, + "created_at": "2026-03-28T10:30:00Z" +} +``` + +--- + +### GET /api/v1/invites + +List all invites (active, expired, and revoked). + +**Auth:** Required +**Permission:** `MANAGE_INVITES` + +#### Response 200 OK + +Returns a JSON array of invite objects. + +--- + +### DELETE /api/v1/invites/{code} + +Revoke an invite by its code string. + +**Auth:** Required +**Permission:** `MANAGE_INVITES` + +#### Response 204 No Content + +--- + +## File Upload and Serving + +### POST /api/v1/uploads + +Upload a file as multipart form data. + +**Auth:** Required +**Body size limit:** 100 MiB +**Content-Type:** `multipart/form-data` + +Files are validated against blocked magic bytes (PE executables, ELF binaries, Mach-O binaries, shell scripts). Files are stored with UUID filenames. + +#### Response 201 Created + +```json +{ + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "filename": "photo.jpg", + "size": 204800, + "mime": "image/jpeg", + "url": "/api/v1/files/a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "width": 1920, + "height": 1080 +} +``` + +`width` and `height` are only present for image files. + +--- + +### GET /api/v1/files/{id} + +Serve a previously uploaded file by its UUID. + +**Auth:** None (URLs are unguessable UUIDs) +**Caching:** `Cache-Control: public, max-age=31536000, immutable` + +Supports HTTP range requests and conditional requests. + +--- + +## Health Check + +### GET /health + +### GET /api/v1/health + +Public health check endpoint, no authentication required. + +```json +{ + "status": "ok", + "version": "1.0.0", + "uptime": 86400, + "online_users": 3 +} +``` + +--- + +## Server Info + +### GET /api/v1/info + +Returns the server name and version. + +**Auth:** None + +```json +{ + "name": "My OwnCord Server", + "version": "1.2.0" +} +``` + +--- + +## Metrics + +### GET /api/v1/metrics + +Runtime server metrics. Restricted to admin-allowed CIDRs. + +**Auth:** Admin IP restriction (not token-based) + +```json +{ + "uptime": "2h30m15s", + "uptime_seconds": 9015.0, + "goroutines": 42, + "heap_alloc_mb": 12.5, + "heap_sys_mb": 24.0, + "num_gc": 156, + "connected_users": 8, + "livekit_healthy": true +} +``` + +--- + +## LiveKit Endpoints + +These endpoints are only registered when LiveKit voice is configured. + +### POST /api/v1/livekit/webhook + +LiveKit webhook receiver. Uses LiveKit JWT verification. Admin-IP-restricted. Called by the LiveKit server, not by clients. + +### GET /api/v1/livekit/health + +Check whether the LiveKit server is reachable. + +**Auth:** Admin IP restriction + +#### Response 200 OK + +```json +{ + "status": "ok", + "livekit_reachable": true +} +``` + +#### Response 503 Service Unavailable + +```json +{ + "status": "degraded", + "livekit_reachable": false, + "error": "connection refused" +} +``` + +### /livekit/* (Reverse Proxy) + +All requests to `/livekit/*` are reverse-proxied to the LiveKit server URL. The `/livekit` prefix is stripped before forwarding. This allows the client to connect to LiveKit through OwnCord's HTTPS server, avoiding mixed-content blocks. + +**Auth:** None (LiveKit handles its own JWT-based auth) +**Rate limit:** 30 requests/minute per IP + +--- + +## Diagnostics + +### GET /api/v1/diagnostics/connectivity + +Returns connectivity diagnostics for debugging voice/network issues. + +**Auth:** Required (any authenticated user) +**Rate limit:** 5 requests/minute per user + +```json +{ + "server": { + "version": "1.0.0", + "uptime_s": 3600, + "go_version": "go1.23.0", + "online_users": 5 + }, + "voice": { + "enabled": true, + "livekit_url": "ws://localhost:7880", + "livekit_health": true, + "node_ip": "203.0.113.1", + "proxy_path": "/livekit" + }, + "client": { + "remote_addr": "192.168.1.100", + "is_private_network": true + } +} +``` + +--- + +## Client Auto-Update + +### GET /api/v1/client-update/{target}/{current_version} + +Tauri-compatible update endpoint. The desktop client checks this to see if a newer version is available. + +**Auth:** None + +#### Path Parameters + +| Param | Type | Description | +| ----- | ---- | ----------- | +| `target` | string | Platform target (e.g., `windows-x86_64`) | +| `current_version` | string | Client's current semver version (e.g., `1.0.0`) | + +#### Response 200 OK (update available) + +```json +{ + "version": "1.2.0", + "notes": "## What's Changed\n...", + "pub_date": "2026-03-28T00:00:00Z", + "platforms": { + "windows-x86_64": { + "signature": "base64-encoded-signature", + "url": "https://github.com/J3vb/OwnCord/releases/download/v1.2.0/OwnCord_1.2.0_x64-setup.nsis.zip" + } + } +} +``` + +#### Response 204 No Content + +Client is already up-to-date. + +--- + +## WebSocket + +### GET /api/v1/ws + +WebSocket upgrade endpoint. Authentication is performed in-band (first message must be an `auth` frame with the session token). See [protocol.md](protocol.md) for the full WebSocket message protocol. diff --git a/docs/client-architecture.md b/docs/client-architecture.md new file mode 100644 index 00000000..5dd144a7 --- /dev/null +++ b/docs/client-architecture.md @@ -0,0 +1,280 @@ +# Client Architecture: Tauri v2 + +Comprehensive architecture reference for the OwnCord Tauri v2 desktop client. Covers project structure, data flow, component system, and subsystems. + +## Why Tauri v2 + +Tauri v2 uses the OS webview (WebView2 on Windows) so the install is ~10-15 MB and RAM usage is ~30-50 MB. The HTML/CSS mockups become the actual UI code, with CSS handling hover effects, conditional visibility, theming, and animations. + +--- + +## Project Layout + +```text +Client/tauri-client/ +├── src-tauri/ # Rust backend +│ ├── Cargo.toml +│ ├── tauri.conf.json # Window size, title, plugins, CSP, updater +│ └── src/ +│ ├── main.rs # Windows entry point +│ ├── lib.rs # Tauri Builder: plugins, commands, state +│ ├── credentials.rs # Win Credential Manager (DPAPI) +│ ├── commands.rs # Settings store, cert fingerprints, DevTools +│ ├── ws_proxy.rs # WSS proxy with TOFU cert pinning +│ ├── livekit_proxy.rs # TCP-to-TLS tunnel for LiveKit signaling +│ ├── ptt.rs # Push-to-talk via GetAsyncKeyState +│ ├── tray.rs # System tray icon and menu +│ ├── hotkeys.rs # Global shortcut registration +│ └── update_commands.rs # Auto-update check + install +│ +├── src/ # TypeScript frontend +│ ├── index.html # Single HTML entry point +│ ├── main.ts # Bootstrap, router, service wiring +│ │ +│ ├── styles/ +│ │ ├── tokens.css # CSS custom properties +│ │ ├── base.css # Reset, scrollbar, typography +│ │ ├── login.css # ConnectPage styles +│ │ ├── app.css # MainPage + component styles +│ │ └── theme-neon-glow.css # Default theme overrides +│ │ +│ ├── lib/ # Core services (no UI, no DOM) +│ │ ├── api.ts # REST client (Tauri plugin-http) +│ │ ├── ws.ts # WebSocket client (Tauri IPC proxy) +│ │ ├── types.ts # Protocol types (WS + REST + permissions) +│ │ ├── store.ts # Reactive store factory +│ │ ├── dispatcher.ts # WS message -> store action router +│ │ ├── router.ts # In-memory page router +│ │ ├── livekitSession.ts # LiveKit voice/video session +│ │ ├── connectionStats.ts # WebRTC stats poller +│ │ ├── rate-limiter.ts # Sliding-window rate limiter +│ │ ├── permissions.ts # Bitfield utilities +│ │ ├── profiles.ts # Server profile CRUD +│ │ ├── credentials.ts # Credential storage (Tauri IPC) +│ │ ├── disposable.ts # Component lifecycle cleanup +│ │ ├── dom.ts # XSS-safe DOM helpers +│ │ ├── safe-render.ts # Error boundary +│ │ ├── logger.ts # Structured logger +│ │ ├── notifications.ts # Desktop notifications +│ │ ├── tenor.ts # Tenor GIF API v2 +│ │ ├── themes.ts # Theme manager +│ │ ├── updater.ts # Auto-update +│ │ ├── reconcile.ts # Keyed DOM list reconciliation +│ │ ├── icons.ts # Lucide SVG icon factory +│ │ └── ... +│ │ +│ ├── stores/ # Reactive state stores +│ │ ├── auth.store.ts +│ │ ├── channels.store.ts +│ │ ├── dm.store.ts +│ │ ├── messages.store.ts +│ │ ├── members.store.ts +│ │ ├── voice.store.ts +│ │ └── ui.store.ts +│ │ +│ ├── components/ # UI components +│ │ ├── MessageList.ts, MessageInput.ts, ... +│ │ ├── message-list/ # MessageList sub-modules +│ │ └── settings/ # Settings tab components +│ │ +│ └── pages/ +│ ├── ConnectPage.ts # Login/register page +│ ├── MainPage.ts # Main app layout +│ └── main-page/ # MainPage sub-controllers +│ ├── SidebarArea.ts +│ ├── ChatArea.ts +│ ├── ChannelController.ts +│ ├── MessageController.ts +│ └── ... +│ +├── tests/ +│ ├── unit/ # Vitest unit tests +│ ├── integration/ # Vitest with mocked WS +│ └── e2e/ # Playwright E2E tests +│ +├── vite.config.ts +├── tsconfig.json +├── vitest.config.ts +└── playwright.config.ts +``` + +--- + +## Architecture Layers + +```text ++===================================================================+ +| UI Components | +| (HTML + CSS, vanilla TypeScript DOM manipulation) | +| Components are factory functions returning { mount, destroy } | ++===================================================================+ + | | | + | subscribe() | actions | events + v v v ++===================================================================+ +| Reactive Stores | +| auth | channels | dm | messages | members | voice | ui | +| Immutable state. Batched notifications via queueMicrotask. | ++===================================================================+ + ^ | + | WS events | send() ++===================================================================+ +| Core Services | +| ws.ts api.ts dispatcher.ts rate-limiter.ts | +| livekitSession.ts notifications.ts ptt.ts tenor.ts | ++===================================================================+ + | | + | invoke() | listen() + v v ++===================================================================+ +| Tauri IPC Bridge | ++===================================================================+ + | ^ + v | ++===================================================================+ +| Rust Backend | +| ws_proxy (WSS + TOFU) livekit_proxy (TCP-to-TLS tunnel) | +| credentials (Win32 DPAPI) ptt (GetAsyncKeyState polling) | +| commands (settings store) tray hotkeys update_commands | ++===================================================================+ +``` + +Data flows DOWN through layers. Events flow UP via subscriptions. No component directly calls the WebSocket or REST API; they go through stores and controllers. + +--- + +## Rust Backend Modules + +### ws_proxy.rs -- WebSocket Proxy with TOFU + +WebView2 rejects self-signed TLS certificates. All WebSocket traffic routes through Rust. The Rust proxy implements TOFU certificate pinning -- on first connect, the cert fingerprint is stored; on subsequent connects, it is verified. + +### livekit_proxy.rs -- LiveKit TLS Tunnel + +A local TCP listener proxies LiveKit SDK connections through TLS to the remote server, avoiding self-signed cert issues. + +### credentials.rs -- Windows Credential Manager + +Uses Win32 Credential Manager APIs. Credentials are stored as DPAPI-encrypted blobs tied to the Windows user account. + +### ptt.rs -- Push-to-Talk + +Uses `GetAsyncKeyState` for non-consuming key detection. 20ms polling loop on a background thread. + +### tray.rs -- System Tray + +System tray icon with Show/Hide, Status submenu, and Quit. + +### update_commands.rs -- Auto-Update + +Dynamic server URL updater endpoint. Update artifacts are verified via Ed25519 signature. + +--- + +## Store System + +The store factory (`createStore`) provides `getState`, `setState`, `subscribe`, `subscribeSelector`, `select`, and `flush`. State is always immutable. Notifications are batched via `queueMicrotask`. + +### Store Responsibilities + +| Store | Key State | WS Events Handled | +|-------|-----------|-------------------| +| **auth** | token, user, serverName, motd, isAuthenticated | `auth_ok`, `auth_error` | +| **channels** | channels (Map), activeChannelId | `ready`, `channel_create/update/delete` | +| **dm** | DM channel list | `dm_channel_open`, `dm_channel_close` | +| **messages** | per-channel messages, pending sends, hasMore | `chat_message`, `chat_edited`, `chat_deleted`, `chat_send_ok`, `reaction_update` | +| **members** | member Map, typing indicators | `ready`, `member_join/leave/update/ban`, `typing`, `presence` | +| **voice** | currentChannelId, voice users, local audio state | `voice_state`, `voice_leave`, `voice_config`, `voice_token` | +| **ui** | sidebar mode, modals, theme, connection status | `server_restart`, `error` | + +Messages per channel are capped at 500. Typing indicators auto-clear after 5 seconds. + +--- + +## Component System + +Components are factory functions returning `{ mount, destroy }`. `mount()` appends elements to a container; `destroy()` removes DOM, unsubscribes listeners, and clears intervals. + +### DOM List Reconciliation + +For efficient list updates (member list, channel list), a keyed reconciliation algorithm reuses existing DOM elements, updates in place, and removes stale elements -- preserving hover states, focus, and scroll position. + +--- + +## Sidebar Architecture + +```text ++----------------------------------+ +| SERVER HEADER | ++----------------------------------+ +| DIRECT MESSAGES (3) [+] | +| Top 3 DMs with unread badges | +| View all messages link | ++----------------------------------+ +| TEXT CHANNELS | +| Category-grouped, collapsible | ++----------------------------------+ +| VOICE CHANNELS | +| User avatars in channel | ++----------------------------------+ +| MEMBERS (collapsible) | +| Role-grouped, drag-to-resize | ++----------------------------------+ +| VOICE WIDGET | +| Mute/deafen/camera/screen/leave | ++----------------------------------+ +| USER BAR | +| Settings + quick-switch buttons | ++----------------------------------+ +``` + +Two sidebar modes: **"channels"** (full server view) and **"dms"** (full DM conversations list). + +--- + +## Chat Area Architecture + +The chat area composes: chat header, message list, typing indicator, message input, video grid (overlays when cameras are active), pinned messages panel, and search overlay. + +The `ChannelController` manages mounting/destroying per-channel components when the active channel changes. + +--- + +## Voice and Video (Client Side) + +### LiveKit Session + +The `LiveKitSession` class manages the full voice/video lifecycle via LiveKit's `livekit-client` JS SDK. + +**Stream Quality Presets:** + +| Preset | Camera Resolution | Camera Bitrate | Screen Resolution | Screen Bitrate | +|--------|------------------|----------------|-------------------|----------------| +| low | 360p | 600 Kbps | 720p@5fps | 1.5 Mbps | +| medium | 720p | 1.7 Mbps | 1080p@15fps | 3 Mbps | +| high | 1080p | 4 Mbps | 1080p@30fps | 6 Mbps | +| source | 1080p | 8 Mbps | native | 10 Mbps | + +### Connection Quality + +A 2-second polling interval collects WebRTC stats from both publisher and subscriber PeerConnections. Quality is color-coded: green (<100ms), yellow (100-200ms), red (>200ms). + +--- + +## REST API Client + +Uses `@tauri-apps/plugin-http` fetch (not browser fetch) to bypass self-signed cert rejection. All requests include `danger: { acceptInvalidCerts: true }` for server URLs only. Third-party fetches use standard cert validation. + +--- + +## Dispatcher + +`wireDispatcher(ws)` attaches listeners to the WsClient, routing each server message type to the appropriate store actions. Key mappings: + +- `ready` -> sets channels, members, voice states, DM channels +- `chat_message` -> adds message, increments unread, triggers notifications +- `voice_token` -> starts LiveKit session +- `presence` -> updates member status +- `server_restart` -> shows warning banner + +See [protocol.md](protocol.md) for complete message type reference. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..bdf1af78 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,88 @@ +# Contributing + +How to set up the development environment and contribute to OwnCord. + +## Development Setup + +### Prerequisites + +- **Windows 10+** (x64) +- **Go 1.22+** (server) +- **Node.js 20+** (client) +- **Rust / Cargo** (Tauri client) + +### Available Commands + +#### Server (Go) + +| Command | Description | +|---------|-------------| +| `go build -o chatserver.exe -ldflags "-s -w" .` | Build server binary | +| `go test ./...` | Run all server tests | +| `go test ./... -cover` | Run server tests with coverage | +| `go test -race ./...` | Run server tests with race detection | + +#### Client (Tauri v2) + +| Command | Description | +|---------|-------------| +| `npm run dev` | Start Vite dev server with hot reload | +| `npm run build` | TypeScript check + Vite production build | +| `npm run tauri dev` | Launch Tauri app in dev mode | +| `npm run tauri build` | Build release installer | +| `npm test` | Run all tests (vitest) | +| `npm run test:unit` | Unit tests only | +| `npm run test:integration` | Integration tests only | +| `npm run test:e2e` | Playwright E2E (mocked Tauri) | +| `npm run test:e2e:native` | Playwright E2E (real Tauri exe + CDP) | +| `npm run test:e2e:prod` | Playwright E2E (prod build) | +| `npm run test:e2e:ui` | Playwright UI mode | +| `npm run test:watch` | Vitest watch mode | +| `npm run test:coverage` | Coverage report | +| `npm run typecheck` | Full typecheck (all sources) | +| `npm run lint` | ESLint check (src/) | +| `npm run lint:fix` | ESLint auto-fix | + +## Active Branches + +- `main` -- stable releases +- `dev` -- active development + +## Branch Naming + +- `feature/` -- new features +- `fix/` -- bug fixes +- `docs/` -- documentation changes + +## Commit Format + +Use conventional commits: + +```text +feat: add thread support to channels +fix: prevent duplicate WebSocket connections +refactor: extract permission checks into middleware +docs: update quick-start guide +test: add integration tests for invite flow +chore: bump Go dependencies +perf: cache role permissions in memory +ci: add lint step to GitHub Actions +``` + +## Pull Request Process + +1. Branch from `dev` (the active development branch) +2. PRs target `dev`; `main` is for stable releases only +3. CI must pass (build + test + lint) +4. Request code review +5. Squash merge preferred + +## Testing + +Target **80%+ coverage**. Follow test-driven development workflow. + +## Code Style + +- **TypeScript**: See [Client Architecture](client-architecture.md) +- **Go**: `gofmt` + `golangci-lint`, standard library preferred +- **Rust**: `cargo fmt` + `cargo clippy`, minimal code (native APIs only) diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 00000000..aa807dcf --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,246 @@ +# Deployment Guide + +Production deployment guide for OwnCord server on Windows. + +## Prerequisites + +- **Windows 10+** (x64) +- **Go 1.22+** (only if building from source) +- **LiveKit Server** binary (for voice/video) -- see [LiveKit Setup](livekit-setup.md) +- Ports available: `8443` (default), `7880` (LiveKit), `80` (if using ACME/Let's Encrypt) + +## Building from Source + +```bash +cd Server +go build -o chatserver.exe -ldflags "-s -w -X main.version=1.0.0" . +``` + +- `-s -w` strips debug info (smaller binary) +- `-X main.version=...` embeds the version string + +Alternatively, download a pre-built `chatserver.exe` from GitHub Releases. + +## First Run Behavior + +When `chatserver.exe` starts for the first time: + +1. **Config creation** -- `config.yaml` is written to the working directory with defaults +2. **Data directory** -- `data/` is created (database, certs, uploads, backups) +3. **TLS certificate** -- A self-signed certificate is generated at `data/cert.pem` / `data/key.pem` +4. **Database migration** -- SQLite database is created and all migrations run +5. **Status reset** -- All user statuses are set to `offline`, stale voice states are cleared +6. **Admin setup page** -- Navigate to `https://localhost:8443/admin` to create the Owner account + +The server listens on `https://0.0.0.0:8443` by default. See [Server Configuration](server-configuration.md) for all options. + +## Running as a Windows Service + +### Option 1: NSSM (Non-Sucking Service Manager) + +```powershell +# Install NSSM (via Chocolatey or download from nssm.cc) +choco install nssm + +# Create service +nssm install OwnCord "C:\OwnCord\chatserver.exe" +nssm set OwnCord AppDirectory "C:\OwnCord" +nssm set OwnCord DisplayName "OwnCord Chat Server" +nssm set OwnCord Start SERVICE_AUTO_START + +# Manage +nssm start OwnCord +nssm stop OwnCord +nssm restart OwnCord +``` + +### Option 2: Task Scheduler + +1. Open Task Scheduler, create a new task +2. Trigger: **At startup** +3. Action: Start `chatserver.exe` +4. Set "Start in" to the directory containing `config.yaml` +5. Check "Run whether user is logged on or not" +6. Check "Run with highest privileges" + +## TLS Setup + +### Self-Signed (default) + +Auto-generated on first run. The Tauri client uses TOFU pinning to accept the cert on first connect. + +```yaml +tls: + mode: "self_signed" +``` + +### Let's Encrypt (ACME) + +Automatic certificate issuance and renewal. Requires port 80 open and a public domain. + +```yaml +tls: + mode: "acme" + domain: "chat.example.com" + acme_cache_dir: "data/acme_certs" +``` + +### Manual Certificate + +Use your own certificate files: + +```yaml +tls: + mode: "manual" + cert_file: "path/to/cert.pem" + key_file: "path/to/key.pem" +``` + +### TLS Off + +Not recommended. For development or when behind a TLS-terminating reverse proxy: + +```yaml +tls: + mode: "off" +``` + +## Backup Strategy + +### SQLite WAL Considerations + +The database uses SQLite WAL mode. Do NOT copy the `.db` file directly while the server is running -- use the backup endpoint instead. + +### Admin Backup Endpoint + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/admin/api/backups` | POST | Create a new backup | +| `/admin/api/backups` | GET | List all backups (newest first) | +| `/admin/api/backups/{name}` | DELETE | Delete a backup | +| `/admin/api/backups/{name}/restore` | POST | Restore from backup (creates pre-restore safety backup first) | + +Backups are stored in `data/backups/` with timestamps. + +### Scheduled Backups + +Use Windows Task Scheduler with PowerShell: + +```powershell +$headers = @{ "Cookie" = "session=" } +Invoke-RestMethod -Uri "https://localhost:8443/admin/api/backups" -Method POST -Headers $headers -SkipCertificateCheck +``` + +### Restore + +Restoring replaces the live database file. A pre-restore safety backup is created automatically. A server restart is recommended after restore. + +## Monitoring + +### Health Endpoint + +`GET /health` -- public, no authentication required. + +```json +{ + "status": "ok", + "version": "1.0.0", + "uptime": 86400, + "online_users": 12 +} +``` + +### Metrics Endpoint + +`GET /api/v1/metrics` -- admin IP restricted. + +```json +{ + "uptime": "24h0m0s", + "uptime_seconds": 86400, + "goroutines": 42, + "heap_alloc_mb": 15.3, + "heap_sys_mb": 24.0, + "num_gc": 150, + "connected_users": 12, + "voice_sessions": 3, + "livekit_healthy": true +} +``` + +### LiveKit Health + +`GET /api/v1/livekit/health` -- checks LiveKit companion process reachability. + +### Diagnostics + +`GET /api/v1/diagnostics/connectivity` -- connectivity diagnostics for troubleshooting. + +## Auto-Update + +### Server + +The server checks GitHub Releases for updates: +- Compares semver versions +- Results are cached for 1 hour +- Downloads `chatserver.exe` with SHA256 checksum verification +- On restart, the old binary is cleaned up + +Set `github.token` in config for higher API rate limits (5000/hr vs 60/hr unauthenticated). + +### Client + +The Tauri client uses NSIS installer updates: +- Server exposes client update assets from GitHub Releases +- Ed25519 signature verification before applying + +## Firewall and Ports + +| Port | Protocol | Purpose | +|------|----------|---------| +| `8443` | TCP | HTTPS server (configurable via `server.port`) | +| `80` | TCP | ACME HTTP-01 challenge (only if `tls.mode: acme`) | +| `7880` | TCP | LiveKit server (WebSocket signaling) | +| `7881` | TCP | LiveKit server (RTC/TURN over TCP) | +| `50000-60000` | UDP | LiveKit WebRTC media (ICE candidates) | + +For remote access, see the [Port Forwarding Guide](port-forwarding.md) or [Tailscale Guide](tailscale.md). + +## Hardening Checklist + +- [ ] **Change default admin password** -- create a strong Owner password during setup +- [ ] **Set `admin_allowed_cidrs`** -- restrict admin access to specific IPs if needed +- [ ] **Enable TLS** -- use `acme` or `manual` mode; avoid `off` in production +- [ ] **Set `allowed_origins`** -- restrict WebSocket origins to your domain +- [ ] **Set `trusted_proxies`** -- configure if behind a reverse proxy +- [ ] **Set stable voice credentials** -- set `livekit_api_key` and `livekit_api_secret` to avoid token breakage on restart +- [ ] **Set `voice.node_ip`** -- required for remote users behind NAT +- [ ] **Review upload limits** -- adjust `upload.max_size_mb` for your use case +- [ ] **Configure GitHub token** -- optional, for reliable update checks +- [ ] **Schedule backups** -- use the admin backup endpoint on a cron schedule +- [ ] **Monitor health** -- poll `/health` for uptime monitoring + +## Background Maintenance + +The server runs a maintenance loop every 15 minutes that: +- Purges expired user sessions +- Deletes orphaned file attachments (uploaded but never linked to a message, older than 1 hour) +- Uses a circuit breaker (pauses after 5 consecutive failures) + +## Graceful Shutdown + +The server handles `Ctrl+C` (SIGINT) and `SIGTERM`: +1. Stops accepting new connections +2. Closes all WebSocket connections and voice rooms +3. Drains HTTP connections with a 30-second timeout +4. Stops the maintenance loop +5. Closes the database + +## See Also + +- [Server Configuration](server-configuration.md) -- full config key reference +- [LiveKit Setup](livekit-setup.md) -- voice/video setup +- [Quick Start](quick-start.md) -- getting started +- [Port Forwarding](port-forwarding.md) -- port forwarding for remote access +- [Tailscale](tailscale.md) -- zero-config networking +- [Security](security.md) -- security guidelines diff --git a/docs/livekit-setup.md b/docs/livekit-setup.md new file mode 100644 index 00000000..1842990e --- /dev/null +++ b/docs/livekit-setup.md @@ -0,0 +1,132 @@ +# LiveKit Setup Guide + +LiveKit is an open-source SFU (Selective Forwarding Unit) that handles real-time voice and video. OwnCord uses it instead of rolling its own WebRTC stack -- LiveKit handles all the hard parts (DTLS, ICE, codec negotiation, simulcast) while OwnCord manages permissions, state, and room lifecycle. + +--- + +## 1. Get the LiveKit Binary + +Download `livekit-server` for Windows from one of: + +- **GitHub releases**: + - Grab the `livekit-server_*_windows_amd64.zip` asset +- **LiveKit website**: (Docs > Self Hosting) + +Extract the binary somewhere permanent (e.g. `C:\livekit\livekit-server.exe`). + +--- + +## 2. Server Configuration + +LiveKit settings live in the `voice:` section of `config.yaml`: + +```yaml +voice: + livekit_api_key: "devkey" + livekit_api_secret: "owncord-dev-secret-key-min-32chars" + livekit_url: "ws://localhost:7880" + livekit_binary: "C:/livekit/livekit-server.exe" + quality: "medium" +``` + +| Field | Purpose | Default | +|-------|---------|---------| +| `livekit_api_key` | Shared API key between OwnCord and LiveKit | `"devkey"` | +| `livekit_api_secret` | Shared secret for JWT signing (min 32 chars) | `"owncord-dev-secret-key-min-32chars"` | +| `livekit_url` | LiveKit WebSocket URL | `ws://localhost:7880` | +| `livekit_binary` | Path to `livekit-server` binary. Empty = assume externally managed | `""` (disabled) | +| `quality` | Default voice quality preset | `"medium"` | + +Environment variable overrides use the `OWNCORD_` prefix: `OWNCORD_VOICE_LIVEKIT_API_KEY`, `OWNCORD_VOICE_LIVEKIT_API_SECRET`, etc. + +> **Warning**: The server logs a warning at startup if you use the default dev key/secret. Always change these for production. + +--- + +## 3. Ports and Firewall + +| Port | Protocol | Purpose | +|------|----------|---------| +| **7880** | TCP (HTTP/WS) | LiveKit signaling (WebSocket + REST API) | +| **7881** | TCP | LiveKit internal RTC (TURN/TCP fallback) | +| **50000-60000** | UDP | Media transport (RTP audio/video) | + +For LAN-only setups, ensure these ports are open on Windows Firewall. For remote access, forward these through your router or use [Tailscale](tailscale.md). + +--- + +## 4. How the Companion Process Works + +When `livekit_binary` is set, OwnCord manages LiveKit as a companion process: + +1. **Config generation**: OwnCord auto-generates `data/livekit.yaml` with the API key/secret, port 7880, and UDP range 50000-60000 +2. **Process launch**: `livekit-server --config data/livekit.yaml` +3. **Crash recovery**: Exponential backoff restart (3s -> 6s -> 12s ... up to 60s), gives up after 10 consecutive rapid failures +4. **Health checks**: `GET http://localhost:7880/` verifies LiveKit is responding +5. **Graceful shutdown**: Stops the process when OwnCord shuts down (5s timeout before kill) + +If `livekit_binary` is empty, OwnCord assumes LiveKit is managed externally (e.g. Docker, systemd, or manual start). + +--- + +## 5. Token Flow + +How a client joins voice: + +``` +Client OwnCord Server LiveKit Server + | | | + |-- voice_join (channel_id)-->| | + | |-- check CONNECT_VOICE | + | |-- persist to voice_states | + | |-- GenerateToken() | + |<-- voice_token ------------| | + | { token, url, | | + | direct_url } | | + | | | + |-- connect with JWT --------|-------------------------->| + |<--- media streams ----------|--------------------------| +``` + +**Token details:** +- Room name: `"channel-{channelID}"` +- Identity: `"user-{userID}"` +- TTL: 24 hours (refresh at 23h) +- `canPublish` is derived from the `SPEAK_VOICE` permission +- `canSubscribe` is always true +- Client can request refresh via `voice_token_refresh` (rate limited to 1/60s) + +**Client connection paths:** +- **Proxy path** (`/livekit`): Client connects through OwnCord's HTTPS server. Avoids mixed-content issues. +- **Direct URL** (`ws://localhost:7880`): Used when the client is on localhost. + +--- + +## 6. Webhook Integration + +LiveKit sends webhooks to `POST /api/v1/livekit/webhook`. The endpoint verifies the JWT and handles `participant_left` to clean up ghost voice states when a user disconnects from LiveKit without sending a `voice_leave` message. + +--- + +## 7. Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| "voice not configured" error | LiveKit client failed to initialize | Check `livekit_api_key` and `livekit_api_secret` are set and secret is >= 32 chars | +| "failed to generate voice token" | API key/secret mismatch | Ensure `config.yaml` key/secret match what LiveKit is using | +| Voice connects but no audio | Firewall blocking UDP 50000-60000 | Open UDP port range in Windows Firewall | +| "backend unavailable" from `/livekit` proxy | LiveKit not running on port 7880 | Check `livekit_binary` path or start LiveKit manually | +| "too many rapid failures, giving up" in logs | LiveKit binary crashes on startup | Run `livekit-server --config data/livekit.yaml` manually to see errors | +| Mixed content / insecure WS error | Client using direct URL over HTTPS page | Client should use the `/livekit` proxy path | +| `GET /api/v1/livekit/health` returns degraded | LiveKit server not reachable | Verify LiveKit is running: `curl http://localhost:7880` | + +--- + +## 8. Production Checklist + +- [ ] Change `livekit_api_key` from `"devkey"` to a random string +- [ ] Change `livekit_api_secret` to a random 32+ character string +- [ ] Open firewall ports: 7880/TCP, 50000-60000/UDP +- [ ] If using ACME/manual TLS, ensure LiveKit proxy at `/livekit` is working +- [ ] Test voice by joining a voice channel from two clients +- [ ] Check `/api/v1/livekit/health` returns `{"status": "ok"}` diff --git a/docs/port-forwarding.md b/docs/port-forwarding.md new file mode 100644 index 00000000..ca9b5bf4 --- /dev/null +++ b/docs/port-forwarding.md @@ -0,0 +1,32 @@ +# Port Forwarding Guide + +How to make your OwnCord server accessible to friends outside your local network. + +## Why + +Friends outside your LAN need a way to reach your server. Port forwarding tells your router to send incoming traffic on a specific port to your server machine. + +## Steps + +1. **Find your router's admin page** -- usually `192.168.1.1` or `192.168.0.1`. Check your gateway IP with `ipconfig` (Windows) or `ip route` (Linux). +2. **Find the port forwarding section** -- may be listed under "NAT", "Virtual Servers", or "Firewall" depending on your router. +3. **Add a rule for the server:** + - External port: `8443` + - Internal IP: your server machine's local IP + - Internal port: `8443` + - Protocol: TCP +4. **Add a rule for voice chat** (if using voice/video): + - External port: `3478` + - Internal IP: your server machine's local IP + - Internal port: `3478` + - Protocol: UDP +5. **Find your public IP** at a site like `whatismyip.com`. +6. **Share your public IP and port** with friends: `your.public.ip:8443` + +## Troubleshooting + +Windows Firewall may block incoming connections. `chatserver.exe` should prompt on first run to allow access. If not, manually add a firewall rule for port 8443 (TCP) and 3478 (UDP). + +## Dynamic IP + +If your public IP changes frequently, consider a Dynamic DNS service (e.g., No-IP, DuckDNS) so friends can use a stable hostname instead of a raw IP address. diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 00000000..94a5c150 --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,838 @@ +# WebSocket Protocol Reference + +All client-server real-time communication happens over a single WebSocket connection. Messages are JSON with a `type` and `payload`. + +**Related docs:** +- [api.md](api.md) -- REST endpoints (message history, file uploads, etc.) +- [schema.md](schema.md) -- Database tables and permission bitfields + +--- + +## Table of Contents + +1. [Transport Layer](#transport-layer) +2. [Message Envelope](#message-envelope) +3. [Sequence Numbers](#sequence-numbers) +4. [Authentication Flow](#authentication-flow) +5. [Heartbeat and Connection Liveness](#heartbeat-and-connection-liveness) +6. [Reconnection with State Recovery](#reconnection-with-state-recovery) +7. [Initial State (ready)](#initial-state-ready) +8. [Chat Messages](#chat-messages) +9. [Reactions](#reactions) +10. [Typing Indicators](#typing-indicators) +11. [Presence](#presence) +12. [Channel Focus](#channel-focus) +13. [Channel Updates](#channel-updates) +14. [Member Updates](#member-updates) +15. [Voice Signaling](#voice-signaling) +16. [Direct Messages](#direct-messages) +17. [Server Restart](#server-restart) +18. [Error Handling](#error-handling) +19. [Rate Limits](#rate-limits) +20. [Message Type Reference Table](#message-type-reference-table) + +--- + +## Transport Layer + +### WebSocket Endpoint + +``` +wss://{host}/api/v1/ws +``` + +The client connects via the Tauri Rust backend's WS proxy rather than native WebView2 WebSocket. This is required because WebView2 rejects self-signed TLS certificates. The Rust proxy uses TOFU (Trust On First Use) certificate pinning. + +### Transport Limits + +| Limit | Value | +|-------|-------| +| Max read size | 1 MB | +| Max message content | 4000 runes | +| Write timeout | 10 seconds | +| Auth deadline | 10 seconds | +| Send buffer per client | 256 messages | + +--- + +## Message Envelope + +Every WebSocket message is a JSON object with these fields: + +```json +{ + "type": "message_type", + "id": "unique-request-id", + "payload": { }, + "seq": 42 +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Determines how `payload` is interpreted | +| `id` | string | Client messages only | Client-generated UUID for request/response correlation | +| `payload` | object | Yes | Contents vary by `type`. Must be present (can be `{}`). | +| `seq` | uint64 | Broadcast messages only | Monotonically increasing sequence number. Only present on server-to-client broadcast messages. | + +--- + +## Sequence Numbers + +The sequence number system enables reconnection with state recovery. + +1. The server maintains an atomic `uint64` counter. +2. Every broadcast message gets the next seq number. +3. The message is stored in a 1000-event replay ring buffer. +4. The client tracks `lastSeq` from every server broadcast. + +### Which Messages Get seq + +| Category | Has seq? | Examples | +|----------|----------|---------| +| Channel broadcasts | Yes | `chat_message`, `chat_edited`, `chat_deleted`, `reaction_update` | +| Global broadcasts | Yes | `presence`, `member_join`, `member_leave`, `member_update`, `member_ban`, `voice_state`, `voice_leave`, `channel_create`, `channel_update`, `channel_delete`, `server_restart` | +| Ephemeral | No | `typing` | +| DM messages | No | DM `chat_message`, `chat_edited`, `chat_deleted`, `reaction_update`, `dm_channel_open`, `dm_channel_close` | +| Direct responses | No | `auth_ok`, `auth_error`, `chat_send_ok`, `error`, `voice_config`, `voice_token`, `pong` | + +--- + +## Authentication Flow + +### Step 1: Client Sends auth + +After the WebSocket connection is established, the client sends the first message within 10 seconds: + +```json +{ + "type": "auth", + "payload": { + "token": "session-token-from-login", + "last_seq": 0 + } +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `token` | string | Yes | Session token obtained from `POST /api/v1/auth/login` | +| `last_seq` | uint64 | No | Last sequence number received. If > 0, server attempts replay. Default 0. | + +### Step 2: Success -- auth_ok + +```json +{ + "type": "auth_ok", + "payload": { + "user": { + "id": 1, + "username": "alex", + "avatar": "uuid.png", + "role": "admin" + }, + "server_name": "My Server", + "motd": "Welcome!" + } +} +``` + +### Step 3: Failure -- auth_error + +```json +{ + "type": "auth_error", + "payload": { + "message": "Invalid or expired token" + } +} +``` + +After sending `auth_error`, the server closes the connection. + +### Step 4: ready Payload + +After `auth_ok`, the server sends a `ready` message containing all initial state. + +### Step 5: Member Join + Presence + +The server broadcasts to all connected clients: + +```json +{ "type": "member_join", "payload": { "user": { "id": 1, "username": "alex", "avatar": "uuid.png", "role": "admin" } } } +{ "type": "presence", "payload": { "user_id": 1, "status": "online" } } +``` + +### Periodic Session Revalidation + +Every 10 messages, the server re-checks the session token against the database. If the session has been revoked, expired, or the user banned, the connection is closed immediately. + +--- + +## Heartbeat and Connection Liveness + +### Client Ping + +The client sends a JSON ping every 30 seconds: + +```json +{ "type": "ping", "payload": {} } +``` + +### Server Pong + +The server responds immediately: + +```json +{ "type": "pong" } +``` + +### Server Stale Client Sweep + +Every 30 seconds, the server checks all clients. Any client with no activity for 90 seconds is forcibly disconnected. Normal chat activity also keeps the connection alive. + +--- + +## Reconnection with State Recovery + +When a connection drops, the client automatically reconnects with exponential backoff (1s to 30s max) and sends `last_seq` in the `auth` message. + +| Condition | Server Behavior | +|-----------|-----------------| +| `last_seq == 0` | Full flow: `auth_ok` + `ready` + `member_join` + `presence` | +| `last_seq > 0` AND seq in buffer | Replay flow: `auth_ok` + missed events + `presence` (no `member_join`, no `ready`) | +| `last_seq > 0` AND seq NOT in buffer | Full flow (fallback): same as `last_seq == 0` | + +DM events are not stored in the ring buffer and are only recoverable via the full `ready` payload. + +--- + +## Initial State (ready) + +Sent once after `auth_ok` (fresh connection or replay fallback). + +```json +{ + "type": "ready", + "payload": { + "channels": [ ... ], + "dm_channels": [ ... ], + "members": [ ... ], + "voice_states": [ ... ], + "roles": [ ... ], + "server_name": "My Server", + "motd": "Welcome!" + } +} +``` + +### Payload Fields + +**channels[]:** `id`, `name`, `type` (`text`/`voice`/`announcement`), `category`, `position`, `unread_count` (text only), `last_message_id` (text only) + +**dm_channels[]:** `channel_id`, `recipient` (user object with `id`, `username`, `avatar`, `status`), `last_message_id`, `last_message`, `last_message_at`, `unread_count` + +**members[]:** All registered users with `id`, `username`, `avatar`, `role` (lowercase name), `status` + +**voice_states[]:** All users currently in any voice channel: `channel_id`, `user_id`, `muted`, `deafened` + +**roles[]:** All server roles with `id`, `name`, `color`, `permissions` (bitfield) + +--- + +## Chat Messages + +### chat_send (Client -> Server) + +```json +{ + "type": "chat_send", + "id": "550e8400-e29b-41d4-a716-446655440000", + "payload": { + "channel_id": 5, + "content": "Hello everyone!", + "reply_to": null, + "attachments": ["upload-uuid-1"] + } +} +``` + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| `channel_id` | number | Yes | Positive integer | +| `content` | string | Yes* | Max 4000 runes. HTML-sanitized. *Can be empty if `attachments` is non-empty. | +| `reply_to` | number or null | No | Message ID being replied to | +| `attachments` | string[] | No | Upload IDs from `POST /api/v1/uploads`. Requires `ATTACH_FILES` permission. | + +### chat_send_ok (Server -> Client) + +Direct response to sender (no seq): + +```json +{ + "type": "chat_send_ok", + "id": "550e8400-e29b-41d4-a716-446655440000", + "payload": { + "message_id": 1042, + "timestamp": "2026-03-14T10:30:00Z" + } +} +``` + +### chat_message (Server -> Client, broadcast) + +```json +{ + "seq": 42, + "type": "chat_message", + "payload": { + "id": 1042, + "channel_id": 5, + "user": { + "id": 1, + "username": "alex", + "avatar": "uuid.png", + "role": "admin" + }, + "content": "Hello everyone!", + "reply_to": null, + "timestamp": "2026-03-14T10:30:00Z", + "attachments": [], + "reactions": [], + "pinned": false + } +} +``` + +### chat_edit (Client -> Server) + +```json +{ + "type": "chat_edit", + "id": "req-uuid", + "payload": { + "message_id": 1042, + "content": "Hello everyone! (edited)" + } +} +``` + +Own messages only. Max 4000 runes. + +### chat_edited (Server -> Client, broadcast) + +```json +{ + "seq": 43, + "type": "chat_edited", + "payload": { + "message_id": 1042, + "channel_id": 5, + "content": "Hello everyone! (edited)", + "edited_at": "2026-03-14T10:31:00Z" + } +} +``` + +### chat_delete (Client -> Server) + +```json +{ + "type": "chat_delete", + "id": "req-uuid", + "payload": { + "message_id": 1042 + } +} +``` + +Moderators with `MANAGE_MESSAGES` can delete others' messages (non-DM channels only). + +### chat_deleted (Server -> Client, broadcast) + +```json +{ + "seq": 44, + "type": "chat_deleted", + "payload": { + "message_id": 1042, + "channel_id": 5 + } +} +``` + +--- + +## Reactions + +### reaction_add / reaction_remove (Client -> Server) + +```json +{ + "type": "reaction_add", + "payload": { + "message_id": 1042, + "emoji": "\ud83d\udc4d" + } +} +``` + +Rate limited at 5/sec. Requires `ADD_REACTIONS` permission (or DM participant). + +### reaction_update (Server -> Client, broadcast) + +```json +{ + "seq": 45, + "type": "reaction_update", + "payload": { + "message_id": 1042, + "channel_id": 5, + "emoji": "\ud83d\udc4d", + "user_id": 1, + "action": "add" + } +} +``` + +`action` is `"add"` or `"remove"`. + +--- + +## Typing Indicators + +### typing_start (Client -> Server) + +```json +{ "type": "typing_start", "payload": { "channel_id": 5 } } +``` + +Rate limited: 1 per 3 seconds per user per channel. Silently dropped when rate limited. + +### typing (Server -> Client, broadcast) + +```json +{ + "type": "typing", + "payload": { + "channel_id": 5, + "user_id": 1, + "username": "alex" + } +} +``` + +Typing broadcasts are ephemeral -- they are NOT stored in the replay ring buffer. + +--- + +## Presence + +### presence_update (Client -> Server) + +```json +{ "type": "presence_update", "payload": { "status": "online" } } +``` + +Valid values: `"online"`, `"idle"`, `"dnd"`, `"offline"`. Rate limited: 1 per 10 seconds. + +### presence (Server -> Client, broadcast) + +```json +{ + "seq": 50, + "type": "presence", + "payload": { + "user_id": 1, + "status": "online" + } +} +``` + +--- + +## Channel Focus + +### channel_focus (Client -> Server) + +```json +{ "type": "channel_focus", "payload": { "channel_id": 5 } } +``` + +Tells the server which channel the user is currently viewing. Affects broadcast delivery and unread tracking. + +--- + +## Channel Updates + +All channel update messages are broadcast to all connected clients. Triggered by REST API calls from admins. + +### channel_create (Server -> Client, broadcast) + +```json +{ + "seq": 60, + "type": "channel_create", + "payload": { + "id": 8, + "name": "gaming", + "type": "text", + "category": "Hangout", + "topic": "", + "position": 3 + } +} +``` + +### channel_update (Server -> Client, broadcast) + +Full channel object (all fields). + +### channel_delete (Server -> Client, broadcast) + +```json +{ + "seq": 62, + "type": "channel_delete", + "payload": { "id": 8 } +} +``` + +--- + +## Member Updates + +All member messages are broadcast to all connected clients. + +### member_join (Server -> Client, broadcast) + +Sent when a user first connects (fresh connection, not reconnect replay). + +```json +{ + "seq": 70, + "type": "member_join", + "payload": { + "user": { + "id": 5, + "username": "newuser", + "avatar": null, + "role": "member" + } + } +} +``` + +### member_update (Server -> Client, broadcast) + +Triggered when an admin changes a user's role. + +```json +{ + "seq": 71, + "type": "member_update", + "payload": { + "user_id": 5, + "role": "moderator" + } +} +``` + +### member_ban (Server -> Client, broadcast) + +```json +{ + "seq": 72, + "type": "member_ban", + "payload": { "user_id": 5 } +} +``` + +--- + +## Voice Signaling + +Voice uses LiveKit as the SFU. WebSocket messages handle signaling (join/leave/state) while the actual audio/video flows through LiveKit's own WebSocket connection. + +### voice_join (Client -> Server) + +```json +{ "type": "voice_join", "payload": { "channel_id": 10 } } +``` + +On success, server sends (in order): +1. `voice_token` -- LiveKit JWT + URL +2. `voice_state` broadcast -- joiner's state to all clients +3. Existing `voice_state` messages -- one per existing participant (to joiner only) +4. `voice_config` -- channel audio settings (to joiner only) + +### voice_token (Server -> Client, direct) + +```json +{ + "type": "voice_token", + "payload": { + "channel_id": 10, + "token": "eyJhbGciOiJIUzI1NiIs...", + "url": "/livekit", + "direct_url": "ws://localhost:7880" + } +} +``` + +### voice_config (Server -> Client, direct) + +```json +{ + "type": "voice_config", + "payload": { + "channel_id": 10, + "quality": "medium", + "bitrate": 64000, + "max_users": 50 + } +} +``` + +Quality presets: + +| Preset | Bitrate | +|--------|---------| +| `low` | 32,000 bps | +| `medium` | 64,000 bps | +| `high` | 128,000 bps | + +### voice_leave (Client -> Server) + +```json +{ "type": "voice_leave", "payload": {} } +``` + +### voice_leave (Server -> Client, broadcast) + +```json +{ + "seq": 80, + "type": "voice_leave", + "payload": { + "channel_id": 10, + "user_id": 1 + } +} +``` + +### voice_state (Server -> Client, broadcast) + +```json +{ + "seq": 81, + "type": "voice_state", + "payload": { + "channel_id": 10, + "user_id": 1, + "username": "alex", + "muted": false, + "deafened": false, + "speaking": false, + "camera": false, + "screenshare": false + } +} +``` + +### voice_mute / voice_deafen (Client -> Server) + +```json +{ "type": "voice_mute", "payload": { "muted": true } } +{ "type": "voice_deafen", "payload": { "deafened": true } } +``` + +### voice_camera (Client -> Server) + +```json +{ "type": "voice_camera", "payload": { "enabled": true } } +``` + +Rate limited: 2/sec. Requires `USE_VIDEO` permission. + +### voice_screenshare (Client -> Server) + +```json +{ "type": "voice_screenshare", "payload": { "enabled": true } } +``` + +Rate limited: 2/sec. Requires `SHARE_SCREEN` permission. + +### voice_token_refresh (Client -> Server) + +```json +{ "type": "voice_token_refresh", "payload": {} } +``` + +Rate limited: 1 per 60 seconds. Must be in a voice channel. + +--- + +## Direct Messages + +### dm_channel_open (Server -> Client) + +Sent when a DM is opened, created, or auto-reopened by an incoming message. + +```json +{ + "type": "dm_channel_open", + "payload": { + "channel_id": 100, + "recipient": { + "id": 2, + "username": "jordan", + "avatar": "uuid.png", + "status": "online" + } + } +} +``` + +### dm_channel_close (Server -> Client) + +```json +{ + "type": "dm_channel_close", + "payload": { "channel_id": 100 } +} +``` + +### DM Authorization + +All handlers that touch a channel check the channel type and branch to participant-based authorization for DMs instead of role-based permissions. This applies to: `chat_send`, `chat_edit`, `chat_delete`, `reaction_add`/`remove`, `typing_start`, `channel_focus`. + +--- + +## Server Restart + +### server_restart (Server -> Client, broadcast) + +```json +{ + "seq": 100, + "type": "server_restart", + "payload": { + "reason": "update", + "delay_seconds": 5 + } +} +``` + +--- + +## Error Handling + +### error (Server -> Client) + +```json +{ + "type": "error", + "id": "original-req-uuid", + "payload": { + "code": "FORBIDDEN", + "message": "No permission to post here" + } +} +``` + +### Error Codes + +| Code | Description | +|------|-------------| +| `BAD_REQUEST` | Invalid payload format or field values | +| `INTERNAL` | Server-side error | +| `NOT_FOUND` | Channel or message not found | +| `FORBIDDEN` | Missing required permission | +| `RATE_LIMITED` | Too many requests (includes `retry_after` in seconds) | +| `ALREADY_JOINED` | Already in this voice channel | +| `CHANNEL_FULL` | Voice channel at capacity | +| `VOICE_ERROR` | Voice-specific error | +| `VIDEO_LIMIT` | Maximum video streams reached | +| `BANNED` | User is banned | +| `INVALID_JSON` | Message is not valid JSON | +| `UNKNOWN_TYPE` | Unrecognized message type | +| `SLOW_MODE` | Channel has slow mode enabled | +| `CONFLICT` | Duplicate reaction or constraint violation | + +After 10 consecutive invalid JSON messages, the connection is forcibly closed. + +--- + +## Rate Limits + +All rate limits are enforced server-side using a token bucket rate limiter. + +| Action | Limit | Window | Error Response | +|--------|-------|--------|----------------| +| Chat send | 10 | 1 second | `RATE_LIMITED` error | +| Chat edit | 10 | 1 second | `RATE_LIMITED` error | +| Chat delete | 10 | 1 second | `RATE_LIMITED` error | +| Typing | 1 | 3 seconds | Silently dropped | +| Presence | 1 | 10 seconds | `RATE_LIMITED` error | +| Reactions | 5 | 1 second | `RATE_LIMITED` error | +| Voice camera | 2 | 1 second | `RATE_LIMITED` error | +| Voice screenshare | 2 | 1 second | `RATE_LIMITED` error | +| Voice token refresh | 1 | 60 seconds | `RATE_LIMITED` error | + +--- + +## Message Type Reference Table + +### Client -> Server (18 types) + +| Type | Rate Limit | Notes | +|------|-----------|-------| +| `auth` | N/A (first message) | Token + optional last_seq | +| `chat_send` | 10/sec | + slow mode per channel | +| `chat_edit` | 10/sec | Own messages only | +| `chat_delete` | 10/sec | Own or mod (non-DM) | +| `reaction_add` | 5/sec | | +| `reaction_remove` | 5/sec | | +| `typing_start` | 1/3sec/channel | Silently dropped | +| `channel_focus` | None | Updates read state | +| `presence_update` | 1/10sec | | +| `voice_join` | None | | +| `voice_leave` | None | Empty payload | +| `voice_mute` | None | | +| `voice_deafen` | None | | +| `voice_camera` | 2/sec | Requires USE_VIDEO | +| `voice_screenshare` | 2/sec | Requires SHARE_SCREEN | +| `voice_token_refresh` | 1/60sec | Must be in voice | +| `soundboard_play` | N/A | Not yet implemented server-side | +| `ping` | None | Heartbeat | + +### Server -> Client (25+ types) + +| Type | Has seq? | Delivery | +|------|----------|----------| +| `auth_ok` | No | Direct | +| `auth_error` | No | Direct (then close) | +| `ready` | No | Direct | +| `chat_message` | Non-DM only | Channel or DM participants | +| `chat_send_ok` | No | Direct to sender | +| `chat_edited` | Non-DM only | Channel or DM participants | +| `chat_deleted` | Non-DM only | Channel or DM participants | +| `reaction_update` | Non-DM only | Channel or DM participants | +| `typing` | No | Channel (excl. sender) or DM | +| `presence` | Yes | All clients | +| `channel_create` | Yes | All clients | +| `channel_update` | Yes | All clients | +| `channel_delete` | Yes | All clients | +| `voice_state` | Yes | All clients | +| `voice_leave` | Yes | All clients | +| `voice_config` | No | Direct to joiner | +| `voice_token` | No | Direct to joiner | +| `member_join` | Yes | All clients | +| `member_update` | Yes | All clients | +| `member_ban` | Yes | All clients | +| `dm_channel_open` | No | Direct to participant | +| `dm_channel_close` | No | Direct to participant | +| `server_restart` | Yes | All clients | +| `error` | No | Direct to requester | +| `pong` | No | Direct to pinger | diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 00000000..285c43ca --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,66 @@ +# Quick Start Guide + +Get OwnCord up and running in minutes. + +## Prerequisites + +- **Windows 10+** (x64) +- **Go 1.25+** (only if building the server from source) +- **Node.js 20+** (only if building the client from source) +- **Rust / Cargo** (only if building the Tauri client from source) +- **LiveKit Server** binary (optional, for voice/video) -- see [LiveKit Setup](livekit-setup.md) + +## Step 1: Download + +Get the latest release from GitHub Releases. Download `chatserver.exe` and the `OwnCord` installer. + +Or build from source: + +```bash +# Server +cd Server +go build -o chatserver.exe -ldflags "-s -w -X main.version=1.0.0" . + +# Client +cd Client/tauri-client +npm install +npm run tauri build +``` + +## Step 2: Run the Server + +Run `chatserver.exe`. On first run: + +1. `config.yaml` is created in the working directory with default settings +2. `data/` directory is created for the database, TLS certs, uploads, and backups +3. A self-signed TLS certificate is generated automatically +4. SQLite database is created and all migrations are applied +5. All user statuses are reset to offline (clean slate) + +The server starts on `https://0.0.0.0:8443`. + +See [Server Configuration](server-configuration.md) for the full config key reference and environment variable overrides. + +## Step 3: Admin Setup + +Open `https://localhost:8443/admin` in a browser. The first-run setup page will prompt you to create the Owner account (username + password). This user gets the Owner role with full server control. + +## Step 4: Create Invites + +In the admin panel, go to invite management and generate invite codes for your friends. + +## Step 5: Connect Clients + +Friends install OwnCord, enter your server address (IP or domain + port 8443), and redeem their invite code to register. + +The client uses TOFU (Trust On First Use) for self-signed certificates -- it will prompt to trust the server's certificate on first connection, then pin it for future sessions. + +## Networking + +If friends are outside your local network, see the [Port Forwarding Guide](port-forwarding.md) or use [Tailscale](tailscale.md) for zero-config networking. + +## Next Steps + +- [Server Configuration](server-configuration.md) -- customize ports, TLS, uploads, voice +- [Deployment Guide](deployment.md) -- production hardening, backups, monitoring, Windows service setup +- [LiveKit Setup](livekit-setup.md) -- enable voice and video chat diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 00000000..5e5fab04 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,394 @@ +# Database Schema Reference + +OwnCord uses a single SQLite database file (`data/chatserver.db`) with the pure-Go driver `modernc.org/sqlite` (no CGO). Migrations run automatically on startup. + +--- + +## Database Configuration + +| PRAGMA | Value | Purpose | +|--------|-------|---------| +| `journal_mode` | `WAL` | Write-Ahead Logging for concurrent readers | +| `foreign_keys` | `ON` | Enforces all `REFERENCES` constraints | +| `busy_timeout` | `5000` | Waits up to 5 seconds for the write lock | +| `synchronous` | `NORMAL` | Safe with WAL mode, reduces fsync calls | +| `temp_store` | `MEMORY` | Temporary tables stored in RAM | +| `mmap_size` | `268435456` | 256 MB memory-mapped I/O | +| `cache_size` | `-64000` | 64 MB page cache | + +SQLite only allows one writer at a time. The connection pool is pinned to a single connection. + +--- + +## Migration System + +Migrations are embedded `.sql` files applied in lexicographic order. Each migration runs in a transaction and is tracked in `schema_versions`. + +```sql +CREATE TABLE IF NOT EXISTS schema_versions ( + version TEXT PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) +); +``` + +### Migration History + +| File | Description | +|------|-------------| +| `001_initial_schema.sql` | All core tables, default roles and settings | +| `002_voice_states.sql` | Adds `voice_states` table | +| `003_audit_log.sql` | Recreates `audit_log` with renamed columns | +| `003_voice_optimization.sql` | Adds `camera`, `screenshare` to voice_states; voice settings to channels | +| `004_fix_member_permissions.sql` | Fixes Member role permissions | +| `005_channel_overrides_index.sql` | Adds composite index on channel_overrides | +| `006_member_video_permissions.sql` | Adds USE_VIDEO and SHARE_SCREEN to Member role | +| `007_attachment_dimensions.sql` | Adds `width` and `height` to attachments | +| `008_dm_tables.sql` | Adds `dm_participants` and `dm_open_state` tables | + +--- + +## Tables + +### roles + +Defines permission tiers. + +```sql +CREATE TABLE roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + color TEXT, + permissions INTEGER NOT NULL DEFAULT 0, + position INTEGER NOT NULL DEFAULT 0, + is_default INTEGER NOT NULL DEFAULT 0 +); +``` + +**Default roles:** + +| id | name | color | permissions | position | Notes | +|----|------|-------|-------------|----------|-------| +| 1 | Owner | `#E74C3C` | `0x7FFFFFFF` | 100 | All 31 permission bits set | +| 2 | Admin | `#F39C12` | `0x3FFFFFFF` | 80 | Everything except ADMINISTRATOR | +| 3 | Moderator | `#3498DB` | `0x000FFFFF` | 60 | All message + voice + moderation | +| 4 | Member | NULL | `0x1E63` | 40 | Send, read, attach, react, voice, video, screen share | + +--- + +### users + +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE COLLATE NOCASE, + password TEXT NOT NULL, + avatar TEXT, + role_id INTEGER NOT NULL DEFAULT 4 REFERENCES roles(id), + totp_secret TEXT, + status TEXT NOT NULL DEFAULT 'offline', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + last_seen TEXT, + banned INTEGER NOT NULL DEFAULT 0, + ban_reason TEXT, + ban_expires TEXT +); +``` + +Valid status values: `online`, `idle`, `dnd`, `offline`. All statuses are reset to `offline` on server startup. + +--- + +### sessions + +```sql +CREATE TABLE sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token TEXT NOT NULL UNIQUE, + device TEXT, + ip_address TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + last_used TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL +); +``` + +Session TTL: 30 days. Token is stored as SHA-256 hash. + +--- + +### channels + +```sql +CREATE TABLE channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL DEFAULT 'text', + category TEXT, + topic TEXT, + position INTEGER NOT NULL DEFAULT 0, + slow_mode INTEGER NOT NULL DEFAULT 0, + archived INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + voice_max_users INTEGER NOT NULL DEFAULT 0, + voice_quality TEXT, + mixing_threshold INTEGER, + voice_max_video INTEGER NOT NULL DEFAULT 25 +); +``` + +Channel types: `text`, `voice`, `announcement`, `dm`. + +--- + +### channel_overrides + +Per-channel permission overrides for specific roles. + +```sql +CREATE TABLE channel_overrides ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + allow INTEGER NOT NULL DEFAULT 0, + deny INTEGER NOT NULL DEFAULT 0, + UNIQUE(channel_id, role_id) +); +``` + +Effective permission calculation: `effective = (base_permissions & ~deny) | allow` + +--- + +### messages + +```sql +CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + reply_to INTEGER REFERENCES messages(id) ON DELETE SET NULL, + edited_at TEXT, + deleted INTEGER NOT NULL DEFAULT 0, + pinned INTEGER NOT NULL DEFAULT 0, + timestamp TEXT NOT NULL DEFAULT (datetime('now')) +); +``` + +Messages are soft-deleted (`deleted = 1`), never physically removed by user action. + +--- + +### messages_fts (FTS5 Virtual Table) + +Full-text search index synchronized via triggers. + +```sql +CREATE VIRTUAL TABLE messages_fts USING fts5( + content, + content='messages', + content_rowid='id' +); +``` + +Supports FTS5 query syntax: simple terms, phrase queries, prefix queries, boolean operators (`AND`, `OR`, `NOT`). + +--- + +### attachments + +```sql +CREATE TABLE attachments ( + id TEXT PRIMARY KEY, + message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + stored_as TEXT NOT NULL, + mime_type TEXT NOT NULL, + size INTEGER NOT NULL, + uploaded_at TEXT NOT NULL DEFAULT (datetime('now')), + width INTEGER, + height INTEGER +); +``` + +Uses UUID primary keys. `message_id` is NULL during upload, linked when the message is sent. + +--- + +### reactions + +```sql +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + emoji TEXT NOT NULL, + UNIQUE(message_id, user_id, emoji) +); +``` + +--- + +### invites + +```sql +CREATE TABLE invites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL UNIQUE, + created_by INTEGER NOT NULL REFERENCES users(id), + redeemed_by INTEGER REFERENCES users(id), + max_uses INTEGER, + use_count INTEGER NOT NULL DEFAULT 0, + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + revoked INTEGER NOT NULL DEFAULT 0 +); +``` + +Invite codes are 8 random bytes encoded as hex. Uses are validated and incremented atomically. + +--- + +### read_states + +```sql +CREATE TABLE read_states ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + last_message_id INTEGER NOT NULL DEFAULT 0, + mention_count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (user_id, channel_id) +); +``` + +--- + +### audit_log + +```sql +CREATE TABLE audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + actor_id INTEGER NOT NULL DEFAULT 0, + action TEXT NOT NULL, + target_type TEXT NOT NULL DEFAULT '', + target_id INTEGER NOT NULL DEFAULT 0, + detail TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +``` + +--- + +### voice_states + +Ephemeral -- all rows deleted on server startup. + +```sql +CREATE TABLE voice_states ( + user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + muted INTEGER NOT NULL DEFAULT 0, + deafened INTEGER NOT NULL DEFAULT 0, + speaking INTEGER NOT NULL DEFAULT 0, + camera INTEGER NOT NULL DEFAULT 0, + screenshare INTEGER NOT NULL DEFAULT 0, + joined_at TEXT NOT NULL DEFAULT (datetime('now')) +); +``` + +--- + +### dm_participants + +```sql +CREATE TABLE dm_participants ( + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (channel_id, user_id) +); +``` + +--- + +### dm_open_state + +```sql +CREATE TABLE dm_open_state ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + opened_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (user_id, channel_id) +); +``` + +--- + +## Indexes + +| Index Name | Table | Columns | Purpose | +|------------|-------|---------|---------| +| `idx_sessions_token` | sessions | `(token)` | Fast session lookup by token hash | +| `idx_sessions_user` | sessions | `(user_id)` | Fast deletion of all sessions for a user | +| `idx_messages_channel` | messages | `(channel_id, id DESC)` | Latest messages in channel query | +| `idx_messages_user` | messages | `(user_id)` | Filter by author | +| `idx_invites_code` | invites | `(code)` | Fast invite validation | +| `idx_audit_timestamp` | audit_log | `(created_at DESC)` | Pagination of audit log | +| `idx_audit_log_actor` | audit_log | `(actor_id)` | Filter by actor | +| `idx_login_ip` | login_attempts | `(ip_address, timestamp)` | Rate limiting queries | +| `idx_voice_states_channel` | voice_states | `(channel_id)` | All users in a voice channel | +| `idx_channel_overrides_channel_role` | channel_overrides | `(channel_id, role_id)` | Permission lookup | +| `idx_dm_participants_user` | dm_participants | `(user_id)` | DM channel lookup | + +--- + +## Permission Bitfield System + +Permissions are stored as an integer bitfield (31 bits used) in `roles.permissions`, `channel_overrides.allow`, and `channel_overrides.deny`. + +### Bit Map + +| Bit | Hex | Name | Description | +|-----|-----|------|-------------| +| 0 | `0x1` | `SEND_MESSAGES` | Post messages in text channels | +| 1 | `0x2` | `READ_MESSAGES` | View messages in text channels | +| 5 | `0x20` | `ATTACH_FILES` | Upload file attachments | +| 6 | `0x40` | `ADD_REACTIONS` | Add emoji reactions | +| 8 | `0x100` | `USE_SOUNDBOARD` | Play sounds in voice channels | +| 9 | `0x200` | `CONNECT_VOICE` | Join voice channels | +| 10 | `0x400` | `SPEAK_VOICE` | Transmit audio in voice channels | +| 11 | `0x800` | `USE_VIDEO` | Enable camera in voice channels | +| 12 | `0x1000` | `SHARE_SCREEN` | Share screen in voice channels | +| 16 | `0x10000` | `MANAGE_MESSAGES` | Delete others' messages, pin/unpin | +| 17 | `0x20000` | `MANAGE_CHANNELS` | Create, edit, delete channels | +| 18 | `0x40000` | `KICK_MEMBERS` | Kick users | +| 19 | `0x80000` | `BAN_MEMBERS` | Ban/unban users | +| 20 | `0x100000` | `MUTE_MEMBERS` | Server-side mute/deafen in voice | +| 24 | `0x1000000` | `MANAGE_ROLES` | Create, edit, delete roles | +| 25 | `0x2000000` | `MANAGE_SERVER` | Modify server settings | +| 26 | `0x4000000` | `MANAGE_INVITES` | Create and revoke invite codes | +| 27 | `0x8000000` | `VIEW_AUDIT_LOG` | View the audit log | +| 30 | `0x40000000` | `ADMINISTRATOR` | Bypasses ALL permission checks | + +Bits 2-4, 7, 13-15, 21-23, 28-29, 31 are reserved. + +### Permission Checking Logic + +``` +1. Get the user's role -> role.Permissions (base) +2. If (base & ADMINISTRATOR) != 0 -> ALLOW everything +3. Get channel_overrides for (channel_id, role_id) -> allow, deny +4. effective = (base | allow) & ~deny +5. Check: (effective & required_permission) != 0 +``` + +DM channels bypass role permissions entirely and use participant-based authorization instead. + +### Default Role Permission Values + +| Role | Hex | Permissions | +|------|-----|-------------| +| Owner | `0x7FFFFFFF` | Everything including ADMINISTRATOR | +| Admin | `0x3FFFFFFF` | Everything except ADMINISTRATOR | +| Moderator | `0x000FFFFF` | All message + voice + moderation | +| Member | `0x1E63` | Send, read, attach, react, voice, video, screen share | diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 00000000..46cf3f2f --- /dev/null +++ b/docs/security.md @@ -0,0 +1,41 @@ +# Security Policy + +Security guidelines and vulnerability reporting for OwnCord. + +## Reporting Vulnerabilities + +Use GitHub Security Advisories to report vulnerabilities: go to Settings > Security > Advisories and create a new advisory. + +**Do NOT open public issues for security bugs.** + +## Response Timeline + +- **Acknowledgment:** Within 48 hours +- **Critical fixes:** Within 7 days +- **Non-critical fixes:** Included in the next release + +## Two-Factor Authentication + +OwnCord supports TOTP-based 2FA: + +- Users enroll via Settings > Account (QR code + backup codes) +- Admins can enforce server-wide 2FA via the `require_2fa` setting in the admin panel +- `require_2fa` requires all users to have 2FA enabled and registration to be closed +- Login flow returns `requires_2fa: true` with a `partial_token` (10-min TTL, 5-attempt limit) +- Auth challenges are rate-limited to 10 req/min per IP + +## Known Limitations + +- No code signing yet -- binaries are verified via SHA256 checksums only + +## Security Hardening Checklist for Operators + +- [ ] Enable TLS (self-signed is the default; custom certs recommended for production) +- [ ] Keep invite-only registration enabled (default) +- [ ] Set a strong admin password +- [ ] Configure rate limits (defaults are sensible but review for your use case) +- [ ] Run regular backups via the admin panel +- [ ] Keep the server updated (admin panel shows available updates) +- [ ] Firewall: only expose port 8443 (HTTPS) and 7880 (LiveKit WebSocket for voice/video) +- [ ] Enable server-wide 2FA requirement once all users have enrolled +- [ ] Set `admin_allowed_cidrs` to restrict admin panel access to trusted networks diff --git a/docs/server-configuration.md b/docs/server-configuration.md new file mode 100644 index 00000000..295c7bb7 --- /dev/null +++ b/docs/server-configuration.md @@ -0,0 +1,141 @@ +# Server Configuration Reference + +Complete reference for all OwnCord server configuration options. + +## Overview + +OwnCord server reads configuration from `config.yaml` in the working directory. On first run, if the file does not exist, a default `config.yaml` is created automatically. + +Configuration is loaded in three layers (later layers override earlier ones): + +1. **Built-in defaults** (compiled into the binary) +2. **YAML file** (`config.yaml`) +3. **Environment variables** (prefix: `OWNCORD_`) + +## Config Key Reference + +### Server (`server`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `server.port` | int | `8443` | HTTP(S) listen port | +| `server.name` | string | `"OwnCord Server"` | Server display name (shown in `/api/v1/info` and admin panel) | +| `server.data_dir` | string | `"data"` | Directory for database, certs, uploads, backups | +| `server.allowed_origins` | string[] | `["*"]` | WebSocket CORS allowed origins; restrict in production | +| `server.trusted_proxies` | string[] | `[]` | CIDRs of trusted reverse proxies (for X-Forwarded-For) | +| `server.admin_allowed_cidrs` | string[] | private networks | CIDRs allowed to access `/admin` routes. Default: `127.0.0.0/8`, `::1/128`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7` | + +### TLS (`tls`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `tls.mode` | string | `"self_signed"` | TLS mode: `self_signed`, `acme`, `manual`, `off` | +| `tls.cert_file` | string | `"data/cert.pem"` | Path to TLS certificate (used by `manual` and `self_signed`) | +| `tls.key_file` | string | `"data/key.pem"` | Path to TLS private key | +| `tls.domain` | string | `""` | Domain for ACME/Let's Encrypt (required when `mode: acme`) | +| `tls.acme_cache_dir` | string | `"data/acme_certs"` | Directory for cached Let's Encrypt certificates | + +### Database (`database`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `database.path` | string | `"data/chatserver.db"` | Path to SQLite database file | + +### Uploads (`upload`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `upload.max_size_mb` | int | `100` | Maximum file upload size in megabytes | +| `upload.storage_dir` | string | `"data/uploads"` | Directory where uploaded files are stored | + +### Voice / LiveKit (`voice`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `voice.livekit_api_key` | string | *(random per run)* | LiveKit API key. Set a stable value for persistent voice tokens. | +| `voice.livekit_api_secret` | string | *(random per run)* | LiveKit API secret (min 32 chars). Set a stable value for persistent tokens. | +| `voice.livekit_url` | string | `"ws://localhost:7880"` | LiveKit server WebSocket URL | +| `voice.livekit_binary` | string | `""` | Path to `livekit-server` binary; empty = don't auto-start | +| `voice.node_ip` | string | `""` | Public IP for WebRTC ICE candidates; empty = auto-detect. Required for remote users behind NAT. | +| `voice.quality` | string | `"medium"` | Voice quality preset: `low`, `medium`, `high` | + +> **Warning:** If `livekit_api_key` or `livekit_api_secret` are left empty, random credentials are generated on each startup. This means voice tokens break on restart. Always set stable credentials in production. See [LiveKit Setup](livekit-setup.md) for details. + +### GitHub / Updates (`github`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `github.token` | string | `""` | Optional GitHub API token for higher rate limits on update checks (5000 req/hr vs 60) | + +## Environment Variable Overrides + +Every config key can be overridden via environment variables using the prefix `OWNCORD_`. + +**Format:** `OWNCORD_
_` + +| Environment Variable | Config Path | +|---------------------|-------------| +| `OWNCORD_SERVER_PORT` | `server.port` | +| `OWNCORD_SERVER_NAME` | `server.name` | +| `OWNCORD_SERVER_DATA_DIR` | `server.data_dir` | +| `OWNCORD_DATABASE_PATH` | `database.path` | +| `OWNCORD_TLS_MODE` | `tls.mode` | +| `OWNCORD_TLS_CERT_FILE` | `tls.cert_file` | +| `OWNCORD_TLS_DOMAIN` | `tls.domain` | +| `OWNCORD_UPLOAD_MAX_SIZE_MB` | `upload.max_size_mb` | +| `OWNCORD_UPLOAD_STORAGE_DIR` | `upload.storage_dir` | +| `OWNCORD_VOICE_LIVEKIT_API_KEY` | `voice.livekit_api_key` | +| `OWNCORD_VOICE_LIVEKIT_API_SECRET` | `voice.livekit_api_secret` | +| `OWNCORD_VOICE_LIVEKIT_URL` | `voice.livekit_url` | +| `OWNCORD_VOICE_NODE_IP` | `voice.node_ip` | +| `OWNCORD_VOICE_QUALITY` | `voice.quality` | +| `OWNCORD_GITHUB_TOKEN` | `github.token` | + +## Example config.yaml + +```yaml +# OwnCord Server Configuration +server: + port: 8443 + name: "OwnCord Server" + data_dir: "data" + allowed_origins: ["*"] # restrict in production + trusted_proxies: [] # e.g. ["10.0.0.0/8"] if behind a reverse proxy + admin_allowed_cidrs: + - "127.0.0.0/8" + - "::1/128" + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + +database: + path: "data/chatserver.db" + +tls: + mode: "self_signed" # self_signed | acme | manual | off + cert_file: "data/cert.pem" + key_file: "data/key.pem" + domain: "" # required for acme mode + acme_cache_dir: "data/acme_certs" + +upload: + max_size_mb: 100 + storage_dir: "data/uploads" + +voice: + livekit_api_key: "your-api-key" + livekit_api_secret: "your-secret-at-least-32-characters-long" + livekit_url: "ws://localhost:7880" + livekit_binary: "" # path to livekit-server binary + node_ip: "" # public IP for remote users behind NAT + quality: "medium" # low | medium | high + +github: + token: "" # optional GitHub PAT for update check rate limits +``` + +## See Also + +- [Deployment Guide](deployment.md) -- production deployment guide +- [LiveKit Setup](livekit-setup.md) -- voice/video setup +- [Quick Start](quick-start.md) -- getting started diff --git a/docs/tailscale.md b/docs/tailscale.md new file mode 100644 index 00000000..ed490956 --- /dev/null +++ b/docs/tailscale.md @@ -0,0 +1,23 @@ +# Tailscale Guide (Zero-Config Alternative) + +Use Tailscale for secure, zero-config networking without port forwarding. + +## What is Tailscale + +Tailscale is a mesh VPN that creates encrypted tunnels between your devices using WireGuard. No port forwarding, no dynamic DNS, and it works behind CGNAT. Free for personal use. + +## Setup + +1. **Install Tailscale** on the server machine and each client machine: https://tailscale.com/download +2. **Sign in** with the same Tailscale account (or share the machine using Tailscale's sharing feature) +3. **Find the server's Tailscale IP** -- shown in the Tailscale app, typically `100.x.y.z` +4. **Disable TLS in config** -- set `tls.mode` to `"off"` in `config.yaml` since Tailscale already encrypts all traffic with WireGuard +5. **Connect clients** using the Tailscale IP: `100.x.y.z:8443` + +## Benefits + +- No port forwarding needed +- Works behind CGNAT and strict firewalls +- Encrypted by default (WireGuard) +- Stable IPs that don't change +- Easy to add/remove friends via the Tailscale admin console