Add MkDocs documentation: architecture, deployment, development, decisions
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# Authentication & Invites
|
||||
|
||||
## Shared database
|
||||
|
||||
BincioWiki shares its user database with `bincio_activity`. Both services read from and write to the same SQLite file at `/var/bincio/data/instance.db`. There is no separate user store for the wiki.
|
||||
|
||||
The relevant tables:
|
||||
|
||||
### `users`
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
handle TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
wiki_access INTEGER NOT NULL DEFAULT 1,
|
||||
activity_access INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
`wiki_access = 1` grants access to BincioWiki. `activity_access = 1` grants access to BincioActivity. The two flags are independent; a user can have one, both, or neither.
|
||||
|
||||
### `sessions`
|
||||
|
||||
```sql
|
||||
CREATE TABLE sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
handle TEXT NOT NULL REFERENCES users(handle) ON DELETE CASCADE,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Sessions expire after **30 days**. The cookie is `bincio_session`, HTTP-only, `SameSite=Lax`, domain `.bincio.org` (shared across all subdomains).
|
||||
|
||||
### `invites`
|
||||
|
||||
```sql
|
||||
CREATE TABLE invites (
|
||||
code TEXT PRIMARY KEY,
|
||||
created_by TEXT NOT NULL REFERENCES users(handle) ON DELETE CASCADE,
|
||||
used_by TEXT REFERENCES users(handle) ON DELETE SET NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
used_at INTEGER,
|
||||
grants_activity INTEGER NOT NULL DEFAULT 0,
|
||||
grants_wiki INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
Wiki invites are created with `grants_wiki = 1, grants_activity = 0`. The `grants_wiki` column was added by a migration in `edit/server.py` (`_migrate_db()` runs at sidecar startup).
|
||||
|
||||
## Session flow
|
||||
|
||||
```
|
||||
POST /api/auth/login → bcrypt.checkpw → INSERT sessions → Set-Cookie: bincio_session
|
||||
GET /api/me → SELECT sessions JOIN users → 200 + user JSON | 401
|
||||
POST /api/auth/logout → DELETE sessions → Clear cookie
|
||||
```
|
||||
|
||||
Every protected Astro page calls `GET /api/me` on load via inline JavaScript. A 401 or 403 response triggers an immediate `window.location.replace('/login/')`.
|
||||
|
||||
## Security model
|
||||
|
||||
This is **client-side enforcement only**. The static HTML is served publicly by nginx without any HTTP-level authentication. A user without JavaScript, or one who crafts direct HTTP requests, can read the raw HTML.
|
||||
|
||||
This is an intentional, documented tradeoff. The content is community memories (cycling routes, gear notes, trip reports) — not financial or medical data. The goal is keeping search engine crawlers and casual visitors out, not resisting determined attackers. All members of the community are trusted.
|
||||
|
||||
For future pages that require stricter enforcement, the architecture supports adding an Astro SSR mode with server-side cookie validation before any HTML is rendered. This would require migrating from `output: "static"` to `output: "server"`.
|
||||
|
||||
## Invite system
|
||||
|
||||
### Creating an invite
|
||||
|
||||
Any wiki user can generate an invite link from `/invites/`. The sidecar creates a single-use token:
|
||||
|
||||
```
|
||||
POST /api/invites
|
||||
→ { "code": "abc123def456ghi7" }
|
||||
```
|
||||
|
||||
The invite URL is `https://wiki.bincio.org/join/?code=<token>`.
|
||||
|
||||
### Registration
|
||||
|
||||
The invitee visits the URL and fills in a handle, optional display name, and password. On submit:
|
||||
|
||||
```
|
||||
POST /api/auth/register
|
||||
{ code, handle, display_name, password }
|
||||
```
|
||||
|
||||
The sidecar:
|
||||
|
||||
1. Validates the code exists and `used_by` is null.
|
||||
2. Checks `grants_wiki = 1`.
|
||||
3. Validates the handle (lowercase, 2–20 chars, `[a-z][a-z0-9_-]{1,19}`).
|
||||
4. Validates password length ≥ 8 characters.
|
||||
5. Checks handle uniqueness.
|
||||
6. Inserts the user (`wiki_access=1, activity_access=0`) and marks the invite as used — in a single transaction.
|
||||
7. Creates a session and sets the cookie. The user is immediately logged in.
|
||||
|
||||
### Revoking an invite
|
||||
|
||||
A user can revoke any of their own unused invites via the `/invites/` page. Used invites cannot be revoked.
|
||||
|
||||
### User limits
|
||||
|
||||
The `settings` table stores a `max_wiki_users` value (currently `100`). The `/api/auth/register` endpoint reads this value and rejects registration if the current wiki user count has reached the limit.
|
||||
|
||||
```sql
|
||||
SELECT value FROM settings WHERE key = 'max_wiki_users';
|
||||
-- 100
|
||||
```
|
||||
Reference in New Issue
Block a user