remove preact and create setup wizard
This commit is contained in:
@@ -8,6 +8,8 @@ Browser users authenticate with passkeys or password fallback. Successful login
|
||||
|
||||
Endpoints:
|
||||
|
||||
- `GET /setup`
|
||||
- `POST /api/setup`
|
||||
- `GET /login`
|
||||
- `GET /account`
|
||||
- `POST /api/auth/register/password`
|
||||
@@ -21,7 +23,12 @@ Endpoints:
|
||||
- `POST /api/auth/passkeys/login/begin`
|
||||
- `POST /api/auth/passkeys/login/finish?challengeId=...`
|
||||
|
||||
The first registered user bootstraps as `admin`. Later public registrations become `viewer`; role changes are admin-managed from the account screen. Public registration endpoints only create new accounts. Adding or changing credentials on an existing account requires an authenticated browser session.
|
||||
On an empty database, the one-time `/setup` wizard creates the first `admin`
|
||||
account and records whether public signup is enabled. Later public
|
||||
registrations are accepted only when that setting is enabled and always become
|
||||
`viewer`; role changes are admin-managed from the account screen. Public
|
||||
registration endpoints only create new accounts. Adding or changing
|
||||
credentials on an existing account requires an authenticated browser session.
|
||||
|
||||
## API Tokens
|
||||
|
||||
@@ -64,7 +71,8 @@ This is intentionally inspired by OAuth device authorization, but it is not a ge
|
||||
|
||||
## Route Protection
|
||||
|
||||
- Public: health, static assets, document reads, login/account entry pages, password/passkey begin-login/register endpoints, device start/token polling.
|
||||
- Setup-only until configured: initial setup page and endpoint.
|
||||
- Public: health, static assets, document reads, login/account entry pages, password/passkey begin-login/register endpoints when signups are enabled, device start/token polling.
|
||||
- Session or bearer token required: auth profile/logout, edit/save APIs, uploads, sync/content APIs, device approval.
|
||||
- Admin role required: admin APIs and API token management.
|
||||
|
||||
|
||||
@@ -4,269 +4,120 @@
|
||||
|
||||
- Docker 24.0+
|
||||
- Docker Compose 2.20+
|
||||
- 1GB RAM minimum (2GB recommended)
|
||||
- 10GB disk space
|
||||
- TLS certificate (Let's Encrypt or custom)
|
||||
- A reverse proxy that terminates TLS for the public hostname
|
||||
- A writable checkout directory for `./data` and `./content`
|
||||
|
||||
## Quick Start
|
||||
## Fresh Deployment
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/your-org/cairnquire.git
|
||||
cd cairnquire
|
||||
|
||||
# Copy and edit configuration
|
||||
cp .env.example .env
|
||||
# Edit .env with your domain, email, Postmark API key
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Verify health
|
||||
curl https://your-domain.com/health
|
||||
```
|
||||
|
||||
## Configuration
|
||||
Edit `.env` and set the HTTPS origin that users will open in their browser:
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `DOMAIN` | Yes | — | Public domain name |
|
||||
| `DB_PATH` | No | `/data/db.sqlite` | Database file path |
|
||||
| `FILESTORE_PATH` | No | `/data/files` | Content-addressed file storage |
|
||||
| `NOTES_PATH` | No | `/notes` | Watched markdown directory |
|
||||
| `POSTMARK_API_KEY` | Yes | — | Postmark server API token |
|
||||
| `POSTMARK_FROM` | No | `notifications@DOMAIN` | From email address |
|
||||
| `SESSION_SECRET` | Yes | — | 32-byte hex for session signing |
|
||||
| `ENV` | No | `production` | `development` or `production` |
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./notes:/notes:ro
|
||||
environment:
|
||||
- DOMAIN=${DOMAIN}
|
||||
- DB_PATH=/data/db.sqlite
|
||||
- FILESTORE_PATH=/data/files
|
||||
- NOTES_PATH=/notes
|
||||
- POSTMARK_API_KEY=${POSTMARK_API_KEY}
|
||||
- SESSION_SECRET=${SESSION_SECRET}
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:noexec,nosuid,size=100m
|
||||
|
||||
# Optional: Traefik for auto-HTTPS
|
||||
traefik:
|
||||
image: traefik:v3.0
|
||||
command:
|
||||
- "--api.insecure=false"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./letsencrypt:/letsencrypt
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
restart: unless-stopped
|
||||
```dotenv
|
||||
CAIRNQUIRE_PUBLIC_ORIGIN=https://cairnquire.example.com
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
- [ ] Change default `SESSION_SECRET` (generate: `openssl rand -hex 32`)
|
||||
- [ ] Configure `POSTMARK_API_KEY` with valid token
|
||||
- [ ] Set `DOMAIN` to public hostname
|
||||
- [ ] Configure DNS A/AAAA records
|
||||
- [ ] Set up SPF and DKIM for email domain
|
||||
- [ ] Review `.env` file — no default secrets
|
||||
|
||||
### Security
|
||||
- [ ] TLS 1.3 enforced (Traefik or reverse proxy)
|
||||
- [ ] HSTS header configured
|
||||
- [ ] Firewall rules: only 80/443 open
|
||||
- [ ] Fail2ban or rate limiting on SSH
|
||||
- [ ] Automatic security updates enabled
|
||||
- [ ] Backup encryption key stored securely
|
||||
|
||||
### Monitoring
|
||||
- [ ] Health check endpoint accessible
|
||||
- [ ] Log rotation configured
|
||||
- [ ] Disk space alerting (>80%)
|
||||
- [ ] Memory usage alerting
|
||||
- [ ] Uptime monitoring (external)
|
||||
|
||||
## Backup Procedures
|
||||
|
||||
### Automated Backup
|
||||
Start the application and verify the local container health endpoint:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
BACKUP_DIR="/backups"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/md-hub-$TIMESTAMP.tar.gz"
|
||||
|
||||
# Create backup
|
||||
tar czf "$BACKUP_FILE" \
|
||||
-C /data \
|
||||
db.sqlite \
|
||||
files/
|
||||
|
||||
# Encrypt (optional)
|
||||
gpg --symmetric --cipher-algo AES256 "$BACKUP_FILE"
|
||||
rm "$BACKUP_FILE"
|
||||
|
||||
# Keep only last 30 days
|
||||
find "$BACKUP_DIR" -name "md-hub-*.tar.gz.gpg" -mtime +30 -delete
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
**Cron**: `0 2 * * * /opt/cairnquire/backup.sh`
|
||||
Then open `https://cairnquire.example.com/setup`. On an empty database the
|
||||
first-run wizard:
|
||||
|
||||
### Restore from Backup
|
||||
1. Creates the initial administrator account.
|
||||
2. Sets its password.
|
||||
3. Records whether visitors may create their own accounts.
|
||||
4. Signs the administrator in.
|
||||
|
||||
The setup endpoint is only available until the first administrator has been
|
||||
created. The signup setting can be changed later from the administrator's
|
||||
account page.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The checked-in Compose file sets the container paths. For a normal single-host
|
||||
deployment, only `CAIRNQUIRE_PUBLIC_ORIGIN` must be supplied in `.env`.
|
||||
|
||||
| Variable | Required | Compose default | Description |
|
||||
|----------|----------|-----------------|-------------|
|
||||
| `CAIRNQUIRE_PUBLIC_ORIGIN` | Yes | None | Exact external HTTPS origin. Passkeys and device-flow URLs depend on this value. |
|
||||
| `CAIRNQUIRE_EMAIL_SMTP_HOST` | No | `mailpit` | SMTP server used for notifications. |
|
||||
| `CAIRNQUIRE_EMAIL_SMTP_PORT` | No | `1025` | SMTP server port. |
|
||||
| `CAIRNQUIRE_EMAIL_FROM` | No | `notifications@cairnquire.local` | Notification sender address. |
|
||||
| `CAIRNQUIRE_LOG_LEVEL` | No | `INFO` | Application log level. |
|
||||
|
||||
The server also accepts these lower-level overrides when it is run outside the
|
||||
checked-in Compose deployment:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CAIRNQUIRE_SERVER_ADDR` | `:8080` | Listen address. |
|
||||
| `CAIRNQUIRE_DATABASE_PATH` | `../../data/db.sqlite` | Local SQLite/libsql database path. |
|
||||
| `CAIRNQUIRE_DATABASE_PRIMARY_URL` | Empty | Optional remote libsql primary URL for embedded-replica mode. |
|
||||
| `CAIRNQUIRE_DATABASE_AUTH_TOKEN` | Empty | Optional auth token for the remote libsql primary. |
|
||||
| `CAIRNQUIRE_CONTENT_SOURCE_DIR` | `../../content` | Writable Markdown source directory. |
|
||||
| `CAIRNQUIRE_CONTENT_STORE_DIR` | `../../data/files` | Content-addressed attachment store. |
|
||||
| `CAIRNQUIRE_CONFIG` | Empty | Optional JSON configuration file. Environment variables override it. |
|
||||
| `CAIRNQUIRE_DEV_MODE` | `false` | Local-only auth shortcut. Never enable this on a deployed server. |
|
||||
|
||||
## Persistent Data
|
||||
|
||||
Compose mounts:
|
||||
|
||||
- `./data:/workspace/data` for the database and attachments
|
||||
- `./content:/workspace/content` for editable Markdown source
|
||||
|
||||
Both mounts must remain writable because uploads, browser edits, and sync
|
||||
writebacks modify them.
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
Expose container port `8080` through a TLS-terminating reverse proxy. Preserve
|
||||
the original hostname and forwarded client address headers. The configured
|
||||
`CAIRNQUIRE_PUBLIC_ORIGIN` must exactly match the external origin, including
|
||||
`https://`.
|
||||
|
||||
## Backup And Restore
|
||||
|
||||
Stop writes or stop the app container before copying a fully consistent local
|
||||
database backup:
|
||||
|
||||
```bash
|
||||
# Stop application
|
||||
docker-compose stop app
|
||||
|
||||
# Extract backup
|
||||
tar xzf md-hub-20240115_020000.tar.gz -C /data
|
||||
|
||||
# Restart
|
||||
docker-compose up -d app
|
||||
|
||||
# Verify
|
||||
curl https://your-domain.com/health
|
||||
docker compose stop app
|
||||
tar czf cairnquire-backup.tgz data content
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
## Upgrade Procedures
|
||||
Restore by stopping the app, replacing `data` and `content` from the archive,
|
||||
and starting the app again.
|
||||
|
||||
## Upgrade
|
||||
|
||||
### Minor Update (patch version)
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker compose stop app
|
||||
tar czf cairnquire-backup-before-upgrade.tgz data content
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
### Major Update
|
||||
1. Read release notes for breaking changes
|
||||
2. Create backup: `./backup.sh`
|
||||
3. Pull new image: `docker-compose pull`
|
||||
4. Run database migrations (auto on startup)
|
||||
5. Update configuration if needed
|
||||
6. Deploy: `docker-compose up -d`
|
||||
7. Verify health and functionality
|
||||
8. Rollback if issues: restore from backup
|
||||
Database migrations run automatically on startup.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application Won't Start
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose logs -f app
|
||||
|
||||
# Verify configuration
|
||||
docker-compose exec app /server -check-config
|
||||
|
||||
# Test database
|
||||
docker-compose exec app sqlite3 /data/db.sqlite ".tables"
|
||||
docker compose logs -f app
|
||||
docker compose ps
|
||||
curl http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
### Sync Not Working
|
||||
```bash
|
||||
# Check file watcher
|
||||
docker-compose exec app ls -la /notes
|
||||
|
||||
# Verify WebSocket
|
||||
curl -i -N \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
|
||||
-H "Sec-WebSocket-Version: 13" \
|
||||
https://your-domain.com/ws
|
||||
```
|
||||
|
||||
### Email Not Sending
|
||||
```bash
|
||||
# Test Postmark connection
|
||||
curl -X POST \
|
||||
https://api.postmarkapp.com/server \
|
||||
-H "X-Postmark-Server-Token: YOUR_TOKEN" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
# Check email queue
|
||||
docker-compose exec app sqlite3 /data/db.sqlite \
|
||||
"SELECT * FROM email_queue WHERE status='failed'"
|
||||
```
|
||||
|
||||
## Scaling
|
||||
|
||||
### Vertical Scaling
|
||||
- Increase container memory/CPU limits
|
||||
- SQLite WAL mode handles concurrent reads well
|
||||
- For heavy write loads, consider libsql replication
|
||||
|
||||
### Horizontal Scaling (Advanced)
|
||||
```yaml
|
||||
# Requires shared storage and load balancer
|
||||
services:
|
||||
app1:
|
||||
# ... same config ...
|
||||
environment:
|
||||
- DB_PATH=/shared/db.sqlite
|
||||
- FILESTORE_PATH=/shared/files
|
||||
|
||||
app2:
|
||||
# ... same config ...
|
||||
environment:
|
||||
- DB_PATH=/shared/db.sqlite
|
||||
- FILESTORE_PATH=/shared/files
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
ports:
|
||||
- "80:80"
|
||||
```
|
||||
|
||||
**Note**: Horizontal scaling requires:
|
||||
- Shared filesystem (NFS, EFS)
|
||||
- libsql replication or external database
|
||||
- Sticky sessions for WebSocket
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: `/docs` route (when authenticated)
|
||||
- **Health Check**: `GET /health`
|
||||
- **Metrics**: `GET /metrics` (Prometheus format)
|
||||
- **Logs**: `docker-compose logs -f app`
|
||||
- **Issues**: https://github.com/your-org/cairnquire/issues
|
||||
If passkey creation fails after deployment, confirm that the browser URL and
|
||||
`CAIRNQUIRE_PUBLIC_ORIGIN` are the same HTTPS origin.
|
||||
|
||||
@@ -408,7 +408,6 @@ RUN addgroup -g 1000 appgroup && \
|
||||
|
||||
# Read-only root filesystem
|
||||
COPY --from=builder /app/server /server
|
||||
COPY --from=builder /app/web/dist /web
|
||||
|
||||
# Data directory (writable)
|
||||
RUN mkdir /data && chown appuser:appgroup /data
|
||||
|
||||
Reference in New Issue
Block a user