diff --git a/README.md b/README.md index 4ba7b4c..04d019f 100644 --- a/README.md +++ b/README.md @@ -1158,6 +1158,55 @@ curl -s http://localhost:8080/api/v1/channels/general/members \ -H "Authorization: Bearer $TOKEN" | jq . ``` +### POST /api/v1/logout — Logout + +Destroy the current client's auth token. If no other clients remain on the +session, the user is fully cleaned up: parted from all channels (with QUIT +broadcast to members), session deleted, nick released. + +**Request:** No body. Requires auth. + +**Response:** `200 OK` +```json +{"status": "ok"} +``` + +**Errors:** + +| Status | Error | When | +|--------|-------|------| +| 401 | `unauthorized` | Missing or invalid auth token | + +**curl example:** +```bash +curl -s -X POST http://localhost:8080/api/v1/logout \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### GET /api/v1/users/me — Current User Info + +Return the current user's session state. This is an alias for +`GET /api/v1/state`. + +**Request:** No body. Requires auth. + +**Response:** `200 OK` +```json +{ + "id": 1, + "nick": "alice", + "channels": [ + {"id": 1, "name": "#general", "topic": "Welcome!"} + ] +} +``` + +**curl example:** +```bash +curl -s http://localhost:8080/api/v1/users/me \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + ### GET /api/v1/server — Server Info Return server metadata. No authentication required. @@ -1166,10 +1215,17 @@ Return server metadata. No authentication required. ```json { "name": "My Chat Server", - "motd": "Welcome! Be nice." + "motd": "Welcome! Be nice.", + "users": 42 } ``` +| Field | Type | Description | +|---------|---------|-------------| +| `name` | string | Server display name | +| `motd` | string | Message of the day | +| `users` | integer | Number of currently active user sessions | + ### GET /.well-known/healthcheck.json — Health Check Standard health check endpoint. No authentication required. @@ -1572,8 +1628,10 @@ skew issues) and simpler than UUIDs (integer comparison vs. string comparison). - **Queue entries**: Stored until pruned. Pruning by `QUEUE_MAX_AGE` is planned. - **Channels**: Deleted when the last member leaves (ephemeral). -- **Users/sessions**: Deleted on `QUIT`. Session expiry by `SESSION_TIMEOUT` - is planned. +- **Users/sessions**: Deleted on `QUIT` or `POST /api/v1/logout`. Idle + sessions are automatically expired after `SESSION_IDLE_TIMEOUT` (default + 24h) — the server runs a background cleanup loop that parts idle users + from all channels, broadcasts QUIT, and releases their nicks. --- @@ -1590,7 +1648,7 @@ directory is also loaded automatically via | `DBURL` | string | `file:./data.db?_journal_mode=WAL` | SQLite connection string. For file-based: `file:./path.db?_journal_mode=WAL`. For in-memory (testing): `file::memory:?cache=shared`. | | `DEBUG` | bool | `false` | Enable debug logging (verbose request/response logging) | | `MAX_HISTORY` | int | `10000` | Maximum messages retained per channel before rotation (planned) | -| `SESSION_TIMEOUT` | int | `86400` | Session idle timeout in seconds (planned). Sessions with no activity for this long are expired and the nick is released. | +| `SESSION_IDLE_TIMEOUT` | string | `24h` | Session idle timeout as a Go duration string (e.g. `24h`, `30m`). Sessions with no activity for this long are expired and the nick is released. | | `QUEUE_MAX_AGE` | int | `172800` | Maximum age of client queue entries in seconds (48h). Entries older than this are pruned (planned). | | `MAX_MESSAGE_SIZE` | int | `4096` | Maximum message body size in bytes (planned enforcement) | | `LONG_POLL_TIMEOUT`| int | `15` | Default long-poll timeout in seconds (client can override via query param, server caps at 30) | @@ -1610,7 +1668,7 @@ SERVER_NAME=My Chat Server MOTD=Welcome! Be excellent to each other. DEBUG=false DBURL=file:./data.db?_journal_mode=WAL -SESSION_TIMEOUT=86400 +SESSION_IDLE_TIMEOUT=24h ``` --- @@ -2008,11 +2066,14 @@ GET /api/v1/challenge - [x] Docker deployment - [x] Prometheus metrics endpoint - [x] Health check endpoint +- [x] Session expiry — auto-expire idle sessions, release nicks +- [x] Logout endpoint (`POST /api/v1/logout`) +- [x] Current user endpoint (`GET /api/v1/users/me`) +- [x] User count in server info (`GET /api/v1/server`) ### Post-MVP (Planned) - [ ] **Hashcash proof-of-work** for session creation (abuse prevention) -- [ ] **Session expiry** — auto-expire idle sessions, release nicks - [ ] **Queue pruning** — delete old queue entries per `QUEUE_MAX_AGE` - [ ] **Message rotation** — enforce `MAX_HISTORY` per channel - [ ] **Channel modes** — enforce `+i`, `+m`, `+s`, `+t`, `+n` diff --git a/internal/config/config.go b/internal/config/config.go index 3117e02..bdf5835 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,7 +31,6 @@ type Config struct { Port int SentryDSN string MaxHistory int - SessionTimeout int MaxMessageSize int MOTD string ServerName string @@ -62,7 +61,6 @@ func New( viper.SetDefault("METRICS_USERNAME", "") viper.SetDefault("METRICS_PASSWORD", "") viper.SetDefault("MAX_HISTORY", "10000") - viper.SetDefault("SESSION_TIMEOUT", "86400") viper.SetDefault("MAX_MESSAGE_SIZE", "4096") viper.SetDefault("MOTD", "") viper.SetDefault("SERVER_NAME", "") @@ -87,7 +85,6 @@ func New( MetricsUsername: viper.GetString("METRICS_USERNAME"), MetricsPassword: viper.GetString("METRICS_PASSWORD"), MaxHistory: viper.GetInt("MAX_HISTORY"), - SessionTimeout: viper.GetInt("SESSION_TIMEOUT"), MaxMessageSize: viper.GetInt("MAX_MESSAGE_SIZE"), MOTD: viper.GetString("MOTD"), ServerName: viper.GetString("SERVER_NAME"),