Private
Public Access
Rename project from KeepItTalking to DS Chat
Renames the app's display name everywhere (page titles, PWA manifest, TopBar, email subject lines, HMAC signature header) and its internal technical slug from chatapp to ds-chat/ds_chat: the Python package name and console script, the systemd unit and its user/group/paths, the deploy scripts, the Docker container names, and the Postgres database name. The live dev Postgres role stays "chatapp" -- renaming a role requires disconnecting the session using it, which needed a temporary superuser role Claude's auto-mode classifier correctly declined to create unsupervised. Functionally invisible (it's just a login credential), but worth knowing about if this ever needs fully cleaning up by hand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+12
-12
@@ -205,19 +205,19 @@ Runs the FastAPI app and Nginx; serves the built PWA static files.
|
|||||||
starting point, managed by a systemd unit:
|
starting point, managed by a systemd unit:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# /etc/systemd/system/chatapp.service
|
# /etc/systemd/system/ds-chat.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Chat service app server
|
Description=Chat service app server
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=chatapp
|
User=ds-chat
|
||||||
WorkingDirectory=/srv/chatapp
|
WorkingDirectory=/srv/ds-chat
|
||||||
EnvironmentFile=/etc/chatapp/env
|
EnvironmentFile=/etc/ds-chat/env
|
||||||
ExecStart=/srv/chatapp/venv/bin/gunicorn app.main:app \
|
ExecStart=/srv/ds-chat/venv/bin/gunicorn app.main:app \
|
||||||
-k uvicorn.workers.UvicornWorker \
|
-k uvicorn.workers.UvicornWorker \
|
||||||
--workers 4 \
|
--workers 4 \
|
||||||
--bind unix:/run/chatapp/chatapp.sock
|
--bind unix:/run/ds-chat/ds-chat.sock
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
@@ -233,16 +233,16 @@ server {
|
|||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name chat.example.com;
|
server_name chat.example.com;
|
||||||
|
|
||||||
root /srv/chatapp/frontend/dist;
|
root /srv/ds-chat/frontend/dist;
|
||||||
try_files $uri /index.html;
|
try_files $uri /index.html;
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://unix:/run/chatapp/chatapp.sock;
|
proxy_pass http://unix:/run/ds-chat/ds-chat.sock;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws/ {
|
location /ws/ {
|
||||||
proxy_pass http://unix:/run/chatapp/chatapp.sock;
|
proxy_pass http://unix:/run/ds-chat/ds-chat.sock;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -252,12 +252,12 @@ server {
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Secrets (database URL pointing at the data server's private IP, Redis URL,
|
- Secrets (database URL pointing at the data server's private IP, Redis URL,
|
||||||
VAPID keys, session secret) live in `/etc/chatapp/env`, loaded via
|
VAPID keys, session secret) live in `/etc/ds-chat/env`, loaded via
|
||||||
`EnvironmentFile=`, never committed to the repository.
|
`EnvironmentFile=`, never committed to the repository.
|
||||||
- Deploy process: `git pull`, install/update dependencies, `alembic upgrade
|
- Deploy process: `git pull`, install/update dependencies, `alembic upgrade
|
||||||
head`, build the frontend, `systemctl restart chatapp`, `nginx -s reload` if
|
head`, build the frontend, `systemctl restart ds-chat`, `nginx -s reload` if
|
||||||
the Nginx config changed.
|
the Nginx config changed.
|
||||||
- Logs: `journalctl -u chatapp`, rotated by systemd/journald defaults; add
|
- Logs: `journalctl -u ds-chat`, rotated by systemd/journald defaults; add
|
||||||
`logrotate` if the app also writes its own log files.
|
`logrotate` if the app also writes its own log files.
|
||||||
|
|
||||||
## 10. Security considerations
|
## 10. Security considerations
|
||||||
|
|||||||
+53
-53
@@ -1,4 +1,4 @@
|
|||||||
# Deploying KeepItTalking
|
# Deploying DS Chat
|
||||||
|
|
||||||
Two Debian 13 servers, no containers, matching [ARCHITECTURE.md §9](ARCHITECTURE.md#9-deployment-architecture--two-linux-servers-no-docker):
|
Two Debian 13 servers, no containers, matching [ARCHITECTURE.md §9](ARCHITECTURE.md#9-deployment-architecture--two-linux-servers-no-docker):
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ sudo apt install -y postgresql redis-server
|
|||||||
**PostgreSQL** — create the role and database:
|
**PostgreSQL** — create the role and database:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u postgres psql -c "CREATE ROLE chatapp WITH LOGIN PASSWORD '<DB_PASSWORD>';"
|
sudo -u postgres psql -c "CREATE ROLE ds_chat WITH LOGIN PASSWORD '<DB_PASSWORD>';"
|
||||||
sudo -u postgres psql -c "CREATE DATABASE chatapp OWNER chatapp;"
|
sudo -u postgres psql -c "CREATE DATABASE ds_chat OWNER ds_chat;"
|
||||||
```
|
```
|
||||||
|
|
||||||
Bind it to the private interface only (find the exact config path with
|
Bind it to the private interface only (find the exact config path with
|
||||||
@@ -67,7 +67,7 @@ enough (a single `/32`) that it won't collide with Debian's default
|
|||||||
`127.0.0.1`/`::1`-only entries, so appending is fine:
|
`127.0.0.1`/`::1`-only entries, so appending is fine:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "host chatapp chatapp <APP_SERVER_PRIVATE_IP>/32 scram-sha-256" \
|
echo "host ds_chat ds_chat <APP_SERVER_PRIVATE_IP>/32 scram-sha-256" \
|
||||||
| sudo tee -a /etc/postgresql/17/main/pg_hba.conf
|
| sudo tee -a /etc/postgresql/17/main/pg_hba.conf
|
||||||
sudo systemctl restart postgresql
|
sudo systemctl restart postgresql
|
||||||
```
|
```
|
||||||
@@ -95,35 +95,35 @@ sudo ufw enable
|
|||||||
install steps (copy it to `/usr/local/bin/`, cron entry). Off-box shipping
|
install steps (copy it to `/usr/local/bin/`, cron entry). Off-box shipping
|
||||||
is left as a placeholder in that script — see §8 below.
|
is left as a placeholder in that script — see §8 below.
|
||||||
|
|
||||||
## 3. App server: Python, Node.js, the `chatapp` user, and the app itself
|
## 3. App server: Python, Node.js, the `ds-chat` user, and the app itself
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y python3 python3-venv nodejs npm git
|
sudo apt install -y python3 python3-venv nodejs npm git
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3a. The `chatapp` system user and directory
|
### 3a. The `ds-chat` system user and directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo useradd --system --shell /usr/sbin/nologin --home-dir /srv/chatapp --create-home chatapp
|
sudo useradd --system --shell /usr/sbin/nologin --home-dir /srv/ds-chat --create-home ds-chat
|
||||||
sudo chown chatapp:chatapp /srv/chatapp
|
sudo chown ds-chat:ds-chat /srv/ds-chat
|
||||||
sudo -u chatapp mkdir -p /srv/chatapp/uploads
|
sudo -u ds-chat mkdir -p /srv/ds-chat/uploads
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3b. Clone the repo (deploy key, not a personal token)
|
### 3b. Clone the repo (deploy key, not a personal token)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp mkdir -p /srv/chatapp/.ssh
|
sudo -u ds-chat mkdir -p /srv/ds-chat/.ssh
|
||||||
sudo -u chatapp ssh-keygen -t ed25519 -f /srv/chatapp/.ssh/id_ed25519 -N ""
|
sudo -u ds-chat ssh-keygen -t ed25519 -f /srv/ds-chat/.ssh/id_ed25519 -N ""
|
||||||
sudo cat /srv/chatapp/.ssh/id_ed25519.pub
|
sudo cat /srv/ds-chat/.ssh/id_ed25519.pub
|
||||||
```
|
```
|
||||||
|
|
||||||
Add that public key as a **read-only deploy key** on the Gitea repo
|
Add that public key as a **read-only deploy key** on the Gitea repo
|
||||||
(Settings → Deploy Keys), then:
|
(Settings → Deploy Keys), then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp ssh-keyscan git.darksingularity.org >> /srv/chatapp/.ssh/known_hosts
|
sudo -u ds-chat ssh-keyscan git.darksingularity.org >> /srv/ds-chat/.ssh/known_hosts
|
||||||
sudo -u chatapp git clone git@git.darksingularity.org:DarkSingularity/KeepItTalking.git /srv/chatapp
|
sudo -u ds-chat git clone git@git.darksingularity.org:DarkSingularity/ds-chat.git /srv/ds-chat
|
||||||
```
|
```
|
||||||
|
|
||||||
(If your Gitea's SSH is on a non-default port, adjust the clone URL and
|
(If your Gitea's SSH is on a non-default port, adjust the clone URL and
|
||||||
@@ -132,16 +132,16 @@ sudo -u chatapp git clone git@git.darksingularity.org:DarkSingularity/KeepItTalk
|
|||||||
### 3c. Backend: venv, env file, migrations, first admin
|
### 3c. Backend: venv, env file, migrations, first admin
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp python3 -m venv /srv/chatapp/backend/.venv
|
sudo -u ds-chat python3 -m venv /srv/ds-chat/backend/.venv
|
||||||
sudo -u chatapp /srv/chatapp/backend/.venv/bin/pip install -e /srv/chatapp/backend
|
sudo -u ds-chat /srv/ds-chat/backend/.venv/bin/pip install -e /srv/ds-chat/backend
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /etc/chatapp
|
sudo mkdir -p /etc/ds-chat
|
||||||
sudo cp /srv/chatapp/deploy/chatapp.env.example /etc/chatapp/env
|
sudo cp /srv/ds-chat/deploy/ds-chat.env.example /etc/ds-chat/env
|
||||||
sudo chown root:chatapp /etc/chatapp/env
|
sudo chown root:ds-chat /etc/ds-chat/env
|
||||||
sudo chmod 0640 /etc/chatapp/env
|
sudo chmod 0640 /etc/ds-chat/env
|
||||||
sudo -e /etc/chatapp/env # fill in DATABASE_URL, REDIS_URL, SESSION_SECRET (see below)
|
sudo -e /etc/ds-chat/env # fill in DATABASE_URL, REDIS_URL, SESSION_SECRET (see below)
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate `SESSION_SECRET`:
|
Generate `SESSION_SECRET`:
|
||||||
@@ -150,23 +150,23 @@ Generate `SESSION_SECRET`:
|
|||||||
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
```
|
```
|
||||||
|
|
||||||
Run migrations and create the first admin account (as `chatapp`, with the
|
Run migrations and create the first admin account (as `ds-chat`, with the
|
||||||
env file sourced so `DATABASE_URL` is set):
|
env file sourced so `DATABASE_URL` is set):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp bash -c 'set -a; source /etc/chatapp/env; set +a; \
|
sudo -u ds-chat bash -c 'set -a; source /etc/ds-chat/env; set +a; \
|
||||||
cd /srv/chatapp/backend && .venv/bin/alembic upgrade head'
|
cd /srv/ds-chat/backend && .venv/bin/alembic upgrade head'
|
||||||
|
|
||||||
sudo -u chatapp bash -c 'set -a; source /etc/chatapp/env; set +a; \
|
sudo -u ds-chat bash -c 'set -a; source /etc/ds-chat/env; set +a; \
|
||||||
cd /srv/chatapp/backend && .venv/bin/python -m app.cli create-user <ADMIN_USERNAME> <ADMIN_EMAIL> "<ADMIN_PASSWORD>" --admin'
|
cd /srv/ds-chat/backend && .venv/bin/python -m app.cli create-user <ADMIN_USERNAME> <ADMIN_EMAIL> "<ADMIN_PASSWORD>" --admin'
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional: push notifications. Skipped silently if `VAPID_PUBLIC_KEY`/
|
Optional: push notifications. Skipped silently if `VAPID_PUBLIC_KEY`/
|
||||||
`VAPID_PRIVATE_KEY` are left unset in `/etc/chatapp/env`. To enable:
|
`VAPID_PRIVATE_KEY` are left unset in `/etc/ds-chat/env`. To enable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp /srv/chatapp/backend/.venv/bin/python -m app.cli generate-vapid-keys
|
sudo -u ds-chat /srv/ds-chat/backend/.venv/bin/python -m app.cli generate-vapid-keys
|
||||||
# paste the three printed lines into /etc/chatapp/env
|
# paste the three printed lines into /etc/ds-chat/env
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional: outgoing email (admin-invited signups, room membership notifications).
|
Optional: outgoing email (admin-invited signups, room membership notifications).
|
||||||
@@ -182,16 +182,16 @@ admin sets it up.
|
|||||||
Manager forward the whole domain to one port with no custom path routing.
|
Manager forward the whole domain to one port with no custom path routing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp bash -c 'cd /srv/chatapp/frontend && npm ci && npm run build'
|
sudo -u ds-chat bash -c 'cd /srv/ds-chat/frontend && npm ci && npm run build'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3e. systemd unit
|
### 3e. systemd unit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp /srv/chatapp/deploy/systemd/chatapp.service /etc/systemd/system/
|
sudo cp /srv/ds-chat/deploy/systemd/ds-chat.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now chatapp
|
sudo systemctl enable --now ds-chat
|
||||||
sudo systemctl status chatapp --no-pager
|
sudo systemctl status ds-chat --no-pager
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm it's actually up before continuing:
|
Confirm it's actually up before continuing:
|
||||||
@@ -200,13 +200,13 @@ Confirm it's actually up before continuing:
|
|||||||
curl -s http://127.0.0.1:8000/api/health # expect {"status":"ok"}
|
curl -s http://127.0.0.1:8000/api/health # expect {"status":"ok"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3f. Let `chatapp` restart its own service (needed for `deploy/upgrade.sh`)
|
### 3f. Let `ds-chat` restart its own service (needed for `deploy/upgrade.sh`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo 'chatapp ALL=(root) NOPASSWD: /usr/bin/systemctl restart chatapp, /usr/bin/systemctl status chatapp' \
|
echo 'ds-chat ALL=(root) NOPASSWD: /usr/bin/systemctl restart ds-chat, /usr/bin/systemctl status ds-chat' \
|
||||||
| sudo tee /etc/sudoers.d/chatapp
|
| sudo tee /etc/sudoers.d/ds-chat
|
||||||
sudo chmod 0440 /etc/sudoers.d/chatapp
|
sudo chmod 0440 /etc/sudoers.d/ds-chat
|
||||||
sudo visudo -cf /etc/sudoers.d/chatapp # validates syntax before it's live
|
sudo visudo -cf /etc/sudoers.d/ds-chat # validates syntax before it's live
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3g. Firewall
|
### 3g. Firewall
|
||||||
@@ -222,7 +222,7 @@ sudo ufw enable
|
|||||||
|
|
||||||
If NPM reaches this box over the same private network the data server
|
If NPM reaches this box over the same private network the data server
|
||||||
uses, bind gunicorn to that private IP instead of `0.0.0.0` in
|
uses, bind gunicorn to that private IP instead of `0.0.0.0` in
|
||||||
`deploy/systemd/chatapp.service` for defense in depth on top of the
|
`deploy/systemd/ds-chat.service` for defense in depth on top of the
|
||||||
firewall rule (edit `--bind`, then `daemon-reload` + `restart`).
|
firewall rule (edit `--bind`, then `daemon-reload` + `restart`).
|
||||||
|
|
||||||
## 4. Configuring Nginx Proxy Manager
|
## 4. Configuring Nginx Proxy Manager
|
||||||
@@ -251,17 +251,17 @@ This is config in NPM's own UI/database, not a file this repo ships:
|
|||||||
- Open `https://chat.example.com` in a browser, log in with the admin
|
- Open `https://chat.example.com` in a browser, log in with the admin
|
||||||
account from §3c, create a room, send a message, confirm it appears
|
account from §3c, create a room, send a message, confirm it appears
|
||||||
live (WebSocket working).
|
live (WebSocket working).
|
||||||
- `sudo journalctl -u chatapp -f` on the app server while doing the above —
|
- `sudo journalctl -u ds-chat -f` on the app server while doing the above —
|
||||||
should show request logs, no tracebacks.
|
should show request logs, no tracebacks.
|
||||||
|
|
||||||
## 6. Upgrades
|
## 6. Upgrades
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u chatapp /srv/chatapp/deploy/upgrade.sh
|
sudo -u ds-chat /srv/ds-chat/deploy/upgrade.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Pulls latest `main`, reinstalls backend deps, runs `alembic upgrade head`,
|
Pulls latest `main`, reinstalls backend deps, runs `alembic upgrade head`,
|
||||||
rebuilds the frontend, restarts `chatapp`, and curls `/api/health` to
|
rebuilds the frontend, restarts `ds-chat`, and curls `/api/health` to
|
||||||
confirm it came back up. Fails loudly (`set -euo pipefail`) and stops
|
confirm it came back up. Fails loudly (`set -euo pipefail`) and stops
|
||||||
before restarting anything if an earlier step — most importantly a failed
|
before restarting anything if an earlier step — most importantly a failed
|
||||||
migration — errors out, so a bad deploy doesn't take down the previously
|
migration — errors out, so a bad deploy doesn't take down the previously
|
||||||
@@ -281,30 +281,30 @@ in practice, downgrades written and tested by hand if one is ever needed).
|
|||||||
## 7. Backups
|
## 7. Backups
|
||||||
|
|
||||||
`deploy/backup-postgres.sh` (installed in §2) runs nightly via cron,
|
`deploy/backup-postgres.sh` (installed in §2) runs nightly via cron,
|
||||||
producing a gzipped `pg_dump` in `/var/backups/chatapp/` with 14-day local
|
producing a gzipped `pg_dump` in `/var/backups/ds-chat/` with 14-day local
|
||||||
rotation. Off-box shipping is a placeholder in that script (commented-out
|
rotation. Off-box shipping is a placeholder in that script (commented-out
|
||||||
rsync/S3 examples) — decide where those need to go and fill it in.
|
rsync/S3 examples) — decide where those need to go and fill it in.
|
||||||
|
|
||||||
That script covers Postgres only. Uploaded chat images live on the **app**
|
That script covers Postgres only. Uploaded chat images live on the **app**
|
||||||
server's disk (`/srv/chatapp/uploads`, created in §3a) — a separate machine
|
server's disk (`/srv/ds-chat/uploads`, created in §3a) — a separate machine
|
||||||
from the data server this script runs on — and currently have no backup
|
from the data server this script runs on — and currently have no backup
|
||||||
mechanism at all. Whatever off-box destination you pick above, include
|
mechanism at all. Whatever off-box destination you pick above, include
|
||||||
`/srv/chatapp/uploads` in it too (e.g. a second `rsync` line run from the
|
`/srv/ds-chat/uploads` in it too (e.g. a second `rsync` line run from the
|
||||||
app server).
|
app server).
|
||||||
|
|
||||||
**Test a restore** (against a scratch database, never directly onto
|
**Test a restore** (against a scratch database, never directly onto
|
||||||
`chatapp`):
|
`ds_chat`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -u postgres createdb chatapp_restore_test
|
sudo -u postgres createdb ds_chat_restore_test
|
||||||
gunzip -c /var/backups/chatapp/chatapp-<TIMESTAMP>.sql.gz | sudo -u postgres psql chatapp_restore_test
|
gunzip -c /var/backups/ds-chat/ds-chat-<TIMESTAMP>.sql.gz | sudo -u postgres psql ds_chat_restore_test
|
||||||
sudo -u postgres dropdb chatapp_restore_test
|
sudo -u postgres dropdb ds_chat_restore_test
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. Troubleshooting
|
## 8. Troubleshooting
|
||||||
|
|
||||||
- **`chatapp` service won't start**: `sudo journalctl -u chatapp -n 50`.
|
- **`ds-chat` service won't start**: `sudo journalctl -u ds-chat -n 50`.
|
||||||
Common causes: `/etc/chatapp/env` missing/malformed (gunicorn workers
|
Common causes: `/etc/ds-chat/env` missing/malformed (gunicorn workers
|
||||||
crash-loop on `pydantic-settings` validation errors), or Postgres/Redis
|
crash-loop on `pydantic-settings` validation errors), or Postgres/Redis
|
||||||
unreachable (check the data-server firewall rules in §2 actually match
|
unreachable (check the data-server firewall rules in §2 actually match
|
||||||
the app server's real private IP).
|
the app server's real private IP).
|
||||||
@@ -313,7 +313,7 @@ sudo -u postgres dropdb chatapp_restore_test
|
|||||||
(isolates "app is down" from "NPM can't reach it") — then check §3g's
|
(isolates "app is down" from "NPM can't reach it") — then check §3g's
|
||||||
`ufw` rule matches NPM's actual source IP.
|
`ufw` rule matches NPM's actual source IP.
|
||||||
- **Migration fails mid-`upgrade.sh`**: the script stops before restarting
|
- **Migration fails mid-`upgrade.sh`**: the script stops before restarting
|
||||||
`chatapp`, so the previous (still-migrated-to-its-old-schema) code keeps
|
`ds-chat`, so the previous (still-migrated-to-its-old-schema) code keeps
|
||||||
running. Fix the migration, re-run the script.
|
running. Fix the migration, re-run the script.
|
||||||
- **Chat works but disconnects after ~a minute of inactivity, then
|
- **Chat works but disconnects after ~a minute of inactivity, then
|
||||||
reconnects**: expected under the current design (§4's NPM timeout note) —
|
reconnects**: expected under the current design (§4's NPM timeout note) —
|
||||||
@@ -334,7 +334,7 @@ scope decisions" for the full detail on each):
|
|||||||
re-validated per delivery (DNS-rebinding gap).
|
re-validated per delivery (DNS-rebinding gap).
|
||||||
- Backup off-box shipping is a placeholder — decide a destination and fill
|
- Backup off-box shipping is a placeholder — decide a destination and fill
|
||||||
in `deploy/backup-postgres.sh`.
|
in `deploy/backup-postgres.sh`.
|
||||||
- Uploaded chat images (`/srv/chatapp/uploads` on the app server) have no
|
- Uploaded chat images (`/srv/ds-chat/uploads` on the app server) have no
|
||||||
backup coverage at all yet, on-box or off — see §7.
|
backup coverage at all yet, on-box or off — see §7.
|
||||||
- Uploaded-but-never-sent images (a user attaches a file, then never hits
|
- Uploaded-but-never-sent images (a user attaches a file, then never hits
|
||||||
Send) leak an orphaned file on disk — no cleanup job for this yet. Not a
|
Send) leak an orphaned file on disk — no cleanup job for this yet. Not a
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# KeepItTalking
|
# DS Chat
|
||||||
|
|
||||||
A web-based team chat service (Mattermost-style, no threaded conversations),
|
A web-based team chat service (Mattermost-style, no threaded conversations),
|
||||||
invite-only. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full system design
|
invite-only. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full system design
|
||||||
@@ -25,8 +25,8 @@ prioritized.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Postgres (see backend/README.md for details)
|
# 1. Postgres (see backend/README.md for details)
|
||||||
docker run -d --name chatapp-postgres \
|
docker run -d --name ds-chat-postgres \
|
||||||
-e POSTGRES_USER=chatapp -e POSTGRES_PASSWORD=chatapp -e POSTGRES_DB=chatapp \
|
-e POSTGRES_USER=ds_chat -e POSTGRES_PASSWORD=ds_chat -e POSTGRES_DB=ds_chat \
|
||||||
-p 5432:5432 postgres:16-alpine
|
-p 5432:5432 postgres:16-alpine
|
||||||
|
|
||||||
# 2. Backend
|
# 2. Backend
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
DATABASE_URL=postgresql+asyncpg://chatapp:chatapp@localhost:5432/chatapp
|
DATABASE_URL=postgresql+asyncpg://ds_chat:ds_chat@localhost:5432/ds_chat
|
||||||
SESSION_SECRET=change-me-to-a-long-random-string
|
SESSION_SECRET=change-me-to-a-long-random-string
|
||||||
SESSION_HTTPS_ONLY=false
|
SESSION_HTTPS_ONLY=false
|
||||||
REDIS_URL=redis://localhost:6379/0
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|||||||
+8
-8
@@ -1,4 +1,4 @@
|
|||||||
# KeepItTalking backend (Phase 1 + 2 + 4 + 5 + 6 + 7 + 8, image uploads, file attachments, admin-configurable upload size limits, emoji & reactions, user profiles, site invites & email, password reset)
|
# DS Chat backend (Phase 1 + 2 + 4 + 5 + 6 + 7 + 8, image uploads, file attachments, admin-configurable upload size limits, emoji & reactions, user profiles, site invites & email, password reset)
|
||||||
|
|
||||||
FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL + Redis. Implements auth, room
|
FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL + Redis. Implements auth, room
|
||||||
CRUD (open and private), room roles (owner/admin/member) and direct
|
CRUD (open and private), room roles (owner/admin/member) and direct
|
||||||
@@ -24,15 +24,15 @@ Accounts are created by an operator on the app server — see step 4 below.
|
|||||||
Any local Postgres 14+ works. The quickest option is a container:
|
Any local Postgres 14+ works. The quickest option is a container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name chatapp-postgres \
|
docker run -d --name ds-chat-postgres \
|
||||||
-e POSTGRES_USER=chatapp -e POSTGRES_PASSWORD=chatapp -e POSTGRES_DB=chatapp \
|
-e POSTGRES_USER=ds_chat -e POSTGRES_PASSWORD=ds_chat -e POSTGRES_DB=ds_chat \
|
||||||
-p 5432:5432 postgres:16-alpine
|
-p 5432:5432 postgres:16-alpine
|
||||||
```
|
```
|
||||||
|
|
||||||
Then create the test database (used by the test suite, kept separate from dev data):
|
Then create the test database (used by the test suite, kept separate from dev data):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec chatapp-postgres psql -U chatapp -d chatapp -c "CREATE DATABASE chatapp_test;"
|
docker exec ds-chat-postgres psql -U ds_chat -d ds_chat -c "CREATE DATABASE ds_chat_test;"
|
||||||
```
|
```
|
||||||
|
|
||||||
(Docker here is purely a local-dev convenience for standing up Postgres quickly —
|
(Docker here is purely a local-dev convenience for standing up Postgres quickly —
|
||||||
@@ -44,7 +44,7 @@ Used for cross-instance WebSocket fan-out and presence (see the section
|
|||||||
below). Required — there's no in-memory fallback.
|
below). Required — there's no in-memory fallback.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name chatapp-redis -p 6379:6379 redis:7-alpine
|
docker run -d --name ds-chat-redis -p 6379:6379 redis:7-alpine
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Python environment
|
### 3. Python environment
|
||||||
@@ -99,13 +99,13 @@ connected to the other, purely via Redis.
|
|||||||
|
|
||||||
### 8. Run tests
|
### 8. Run tests
|
||||||
|
|
||||||
Tests run against a real Postgres database (`chatapp_test` by default — native
|
Tests run against a real Postgres database (`ds_chat_test` by default — native
|
||||||
`ENUM`/`UUID` types aren't faithfully reproduced by SQLite) and a real Redis
|
`ENUM`/`UUID` types aren't faithfully reproduced by SQLite) and a real Redis
|
||||||
(db 15 by default, kept separate from dev use of db 0), with each test
|
(db 15 by default, kept separate from dev use of db 0), with each test
|
||||||
wrapped in a transaction that's rolled back afterward:
|
wrapped in a transaction that's rolled back afterward:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL=postgresql+asyncpg://chatapp:chatapp@localhost:5432/chatapp_test .venv/bin/pytest
|
DATABASE_URL=postgresql+asyncpg://ds_chat:ds_chat@localhost:5432/ds_chat_test .venv/bin/pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
@@ -247,7 +247,7 @@ call sites rather than duplicated).
|
|||||||
**Outgoing webhooks / event subscriptions** (`POST
|
**Outgoing webhooks / event subscriptions** (`POST
|
||||||
/api/rooms/{id}/event-subscriptions`, room-admin managed; room-scoped or
|
/api/rooms/{id}/event-subscriptions`, room-admin managed; room-scoped or
|
||||||
global via `room_id=null`): fires an HMAC-SHA256-signed POST
|
global via `room_id=null`): fires an HMAC-SHA256-signed POST
|
||||||
(`X-KeepItTalking-Signature: sha256=...`) on `message.created`/
|
(`X-DS-Chat-Signature: sha256=...`) on `message.created`/
|
||||||
`message.updated`, delivered via a backgrounded `asyncio.create_task`
|
`message.updated`, delivered via a backgrounded `asyncio.create_task`
|
||||||
(`app/services/webhook_delivery.py`) — safe to background here, unlike the
|
(`app/services/webhook_delivery.py`) — safe to background here, unlike the
|
||||||
Phase 4 push lesson, since there's no DB session involved, just the
|
Phase 4 push lesson, since there's no DB session involved, just the
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ path_separator = os
|
|||||||
# database URL. This is consumed by the user-maintained env.py script only.
|
# database URL. This is consumed by the user-maintained env.py script only.
|
||||||
# other means of configuring database URLs may be customized within the env.py
|
# other means of configuring database URLs may be customized within the env.py
|
||||||
# file.
|
# file.
|
||||||
sqlalchemy.url = postgresql+asyncpg://chatapp:chatapp@localhost:5432/chatapp
|
sqlalchemy.url = postgresql+asyncpg://chatapp:chatapp@localhost:5432/ds_chat
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|||||||
+3
-3
@@ -18,8 +18,8 @@ from app.ws.connection_manager import ConnectionManager
|
|||||||
from app.ws.presence import Presence
|
from app.ws.presence import Presence
|
||||||
|
|
||||||
# backend/app/main.py -> backend/ -> repo root -- matches both the local
|
# backend/app/main.py -> backend/ -> repo root -- matches both the local
|
||||||
# monorepo layout and the production layout (/srv/chatapp/backend,
|
# monorepo layout and the production layout (/srv/ds-chat/backend,
|
||||||
# /srv/chatapp/frontend/dist), which is the same relative shape.
|
# /srv/ds-chat/frontend/dist), which is the same relative shape.
|
||||||
FRONTEND_DIST = pathlib.Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
FRONTEND_DIST = pathlib.Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
app = FastAPI(title="KeepItTalking", lifespan=lifespan)
|
app = FastAPI(title="DS Chat", lifespan=lifespan)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
SessionMiddleware,
|
SessionMiddleware,
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ async def send_test_email(db: AsyncSession, to_address: str) -> None:
|
|||||||
await _deliver(
|
await _deliver(
|
||||||
cfg,
|
cfg,
|
||||||
to_address,
|
to_address,
|
||||||
"KeepItTalking test email",
|
"DS Chat test email",
|
||||||
"This is a test email from KeepItTalking to confirm your SMTP settings are working.",
|
"This is a test email from DS Chat to confirm your SMTP settings are working.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async def request_password_reset(db: AsyncSession, email: str, base_url: str) ->
|
|||||||
await send_email(
|
await send_email(
|
||||||
db,
|
db,
|
||||||
email,
|
email,
|
||||||
"Reset your KeepItTalking password",
|
"Reset your DS Chat password",
|
||||||
f"Someone requested a password reset for this account.\n\n"
|
f"Someone requested a password reset for this account.\n\n"
|
||||||
f"Reset it here:\n{reset_link}\n\n"
|
f"Reset it here:\n{reset_link}\n\n"
|
||||||
f"This link expires in 15 minutes. If you didn't request this, "
|
f"This link expires in 15 minutes. If you didn't request this, "
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ async def add_member(
|
|||||||
db,
|
db,
|
||||||
target.email,
|
target.email,
|
||||||
f"You've been added to #{room.name}",
|
f"You've been added to #{room.name}",
|
||||||
f"You've been added to the #{room.name} room on KeepItTalking.\n\n"
|
f"You've been added to the #{room.name} room on DS Chat.\n\n"
|
||||||
f"Open the app: {base_url.rstrip('/')}",
|
f"Open the app: {base_url.rstrip('/')}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ async def create_site_invite(
|
|||||||
await send_email(
|
await send_email(
|
||||||
db,
|
db,
|
||||||
email,
|
email,
|
||||||
"You're invited to join KeepItTalking",
|
"You're invited to join DS Chat",
|
||||||
f"You've been invited to join KeepItTalking by {actor.username}.\n\n"
|
f"You've been invited to join DS Chat by {actor.username}.\n\n"
|
||||||
f"Set up your account here:\n{signup_link}\n\n"
|
f"Set up your account here:\n{signup_link}\n\n"
|
||||||
f"This link expires in 7 days.",
|
f"This link expires in 7 days.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async def deliver_event(subscription: EventSubscription, event_type: str, payloa
|
|||||||
content=body,
|
content=body,
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-KeepItTalking-Signature": f"sha256={signature}",
|
"X-DS-Chat-Signature": f"sha256={signature}",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from PIL import Image, UnidentifiedImageError
|
|||||||
|
|
||||||
# backend/app/storage.py -> backend/ -> repo root -- same
|
# backend/app/storage.py -> backend/ -> repo root -- same
|
||||||
# resolve-relative-to-file convention FRONTEND_DIST uses in app/main.py, so
|
# resolve-relative-to-file convention FRONTEND_DIST uses in app/main.py, so
|
||||||
# this lands in the right place in both local dev and the /srv/chatapp
|
# this lands in the right place in both local dev and the /srv/ds-chat
|
||||||
# production layout with zero new config.
|
# production layout with zero new config.
|
||||||
UPLOADS_DIR = pathlib.Path(__file__).resolve().parent.parent.parent / "uploads"
|
UPLOADS_DIR = pathlib.Path(__file__).resolve().parent.parent.parent / "uploads"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "chatapp"
|
name = "ds-chat"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "KeepItTalking chat service backend"
|
description = "DS Chat backend service"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.115",
|
"fastapi>=0.115",
|
||||||
@@ -25,7 +25,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
chatapp-create-user = "app.cli:main"
|
ds-chat-create-user = "app.cli:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
"DATABASE_URL", "postgresql+asyncpg://chatapp:chatapp@localhost:5432/chatapp_test"
|
"DATABASE_URL", "postgresql+asyncpg://chatapp:chatapp@localhost:5432/ds_chat_test"
|
||||||
)
|
)
|
||||||
os.environ.setdefault("SESSION_SECRET", "test-secret")
|
os.environ.setdefault("SESSION_SECRET", "test-secret")
|
||||||
os.environ.setdefault("SESSION_HTTPS_ONLY", "false")
|
os.environ.setdefault("SESSION_HTTPS_ONLY", "false")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async def test_subscribe_creates_row(client, db_session):
|
|||||||
resp = await client.post("/api/push/subscribe", json=payload)
|
resp = await client.post("/api/push/subscribe", json=payload)
|
||||||
assert resp.status_code == 204
|
assert resp.status_code == 204
|
||||||
|
|
||||||
# The chatapp_test database is shared across the whole suite and the
|
# The ds_chat_test database is shared across the whole suite and the
|
||||||
# ws_client-based tests below intentionally don't roll back (see
|
# ws_client-based tests below intentionally don't roll back (see
|
||||||
# conftest.ws_client), so a unique endpoint keeps this test independent
|
# conftest.ws_client), so a unique endpoint keeps this test independent
|
||||||
# of leftover rows from those instead of asserting on the total count.
|
# of leftover rows from those instead of asserting on the total count.
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ async def test_outgoing_webhook_delivers_signed_payload(client, db_session, monk
|
|||||||
assert len(posts) == 1
|
assert len(posts) == 1
|
||||||
body = posts[0]["content"]
|
body = posts[0]["content"]
|
||||||
expected_signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
expected_signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||||
assert posts[0]["headers"]["X-KeepItTalking-Signature"] == f"sha256={expected_signature}"
|
assert posts[0]["headers"]["X-DS-Chat-Signature"] == f"sha256={expected_signature}"
|
||||||
payload = json.loads(body)
|
payload = json.loads(body)
|
||||||
assert payload["event"] == "message.created"
|
assert payload["event"] == "message.created"
|
||||||
assert payload["data"]["content"] == "ping"
|
assert payload["data"]["content"] == "ping"
|
||||||
|
|||||||
+13
-13
@@ -1,27 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Nightly Postgres backup for the KeepItTalking data server.
|
# Nightly Postgres backup for the DS Chat data server.
|
||||||
#
|
#
|
||||||
# Install (as root, on the data server):
|
# Install (as root, on the data server):
|
||||||
# sudo cp deploy/backup-postgres.sh /usr/local/bin/chatapp-backup-postgres.sh
|
# sudo cp deploy/backup-postgres.sh /usr/local/bin/ds-chat-backup-postgres.sh
|
||||||
# sudo chmod 0700 /usr/local/bin/chatapp-backup-postgres.sh
|
# sudo chmod 0700 /usr/local/bin/ds-chat-backup-postgres.sh
|
||||||
# sudo crontab -e
|
# sudo crontab -e
|
||||||
# # add:
|
# # add:
|
||||||
# 0 3 * * * /usr/local/bin/chatapp-backup-postgres.sh
|
# 0 3 * * * /usr/local/bin/ds-chat-backup-postgres.sh
|
||||||
#
|
#
|
||||||
# See ../DEPLOYMENT.md for the full data-server setup this fits into.
|
# See ../DEPLOYMENT.md for the full data-server setup this fits into.
|
||||||
#
|
#
|
||||||
# Covers Postgres only. Uploaded chat images live on the app server's disk
|
# Covers Postgres only. Uploaded chat images live on the app server's disk
|
||||||
# (/srv/chatapp/uploads, see app/storage.py), not here -- see DEPLOYMENT.md
|
# (/srv/ds-chat/uploads, see app/storage.py), not here -- see DEPLOYMENT.md
|
||||||
# §7 for that gap.
|
# §7 for that gap.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DB_NAME="chatapp"
|
DB_NAME="ds_chat"
|
||||||
DB_USER="chatapp"
|
DB_USER="ds_chat"
|
||||||
BACKUP_DIR="/var/backups/chatapp"
|
BACKUP_DIR="/var/backups/ds-chat"
|
||||||
RETENTION_DAYS=14
|
RETENTION_DAYS=14
|
||||||
TIMESTAMP="$(date +%F-%H%M%S)"
|
TIMESTAMP="$(date +%F-%H%M%S)"
|
||||||
DEST="${BACKUP_DIR}/chatapp-${TIMESTAMP}.sql.gz"
|
DEST="${BACKUP_DIR}/ds-chat-${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ echo "Backed up ${DB_NAME} to ${DEST}"
|
|||||||
|
|
||||||
# Local rotation -- keep RETENTION_DAYS days on this box regardless of
|
# Local rotation -- keep RETENTION_DAYS days on this box regardless of
|
||||||
# whether off-box shipping (below) is configured yet.
|
# whether off-box shipping (below) is configured yet.
|
||||||
find "$BACKUP_DIR" -name 'chatapp-*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete
|
find "$BACKUP_DIR" -name 'ds-chat-*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete
|
||||||
|
|
||||||
# --- Off-box shipping -------------------------------------------------
|
# --- Off-box shipping -------------------------------------------------
|
||||||
# Not configured yet -- destination wasn't decided as of this script being
|
# Not configured yet -- destination wasn't decided as of this script being
|
||||||
@@ -44,9 +44,9 @@ find "$BACKUP_DIR" -name 'chatapp-*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete
|
|||||||
#
|
#
|
||||||
# rsync (to a second host reachable by the data server, e.g. over the same
|
# rsync (to a second host reachable by the data server, e.g. over the same
|
||||||
# private network / a WireGuard tunnel used for anything else):
|
# private network / a WireGuard tunnel used for anything else):
|
||||||
# rsync -a "$DEST" backup-user@backup-host:/path/to/chatapp-backups/
|
# rsync -a "$DEST" backup-user@backup-host:/path/to/ds-chat-backups/
|
||||||
#
|
#
|
||||||
# S3-compatible object storage (needs `aws configure` or rclone set up
|
# S3-compatible object storage (needs `aws configure` or rclone set up
|
||||||
# separately first):
|
# separately first):
|
||||||
# aws s3 cp "$DEST" s3://your-bucket/chatapp-backups/
|
# aws s3 cp "$DEST" s3://your-bucket/ds-chat-backups/
|
||||||
# # or: rclone copy "$DEST" remote:chatapp-backups/
|
# # or: rclone copy "$DEST" remote:ds-chat-backups/
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# /etc/chatapp/env (production)
|
# /etc/ds-chat/env (production)
|
||||||
#
|
#
|
||||||
# This file is loaded by systemd's EnvironmentFile= (see
|
# This file is loaded by systemd's EnvironmentFile= (see
|
||||||
# deploy/systemd/chatapp.service) directly into the app process's
|
# deploy/systemd/ds-chat.service) directly into the app process's
|
||||||
# environment -- it is NOT a dotenv file Python reads from a working
|
# environment -- it is NOT a dotenv file Python reads from a working
|
||||||
# directory, and it must never be committed to the repository.
|
# directory, and it must never be committed to the repository.
|
||||||
#
|
#
|
||||||
# Install:
|
# Install:
|
||||||
# sudo mkdir -p /etc/chatapp
|
# sudo mkdir -p /etc/ds-chat
|
||||||
# sudo cp deploy/chatapp.env.example /etc/chatapp/env
|
# sudo cp deploy/ds-chat.env.example /etc/ds-chat/env
|
||||||
# sudo chown root:chatapp /etc/chatapp/env
|
# sudo chown root:ds-chat /etc/ds-chat/env
|
||||||
# sudo chmod 0640 /etc/chatapp/env
|
# sudo chmod 0640 /etc/ds-chat/env
|
||||||
# # then edit in the real values below
|
# # then edit in the real values below
|
||||||
#
|
#
|
||||||
# See ../DEPLOYMENT.md for how each value is generated.
|
# See ../DEPLOYMENT.md for how each value is generated.
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
# Points at the data server's PRIVATE address -- never the public one.
|
# Points at the data server's PRIVATE address -- never the public one.
|
||||||
# The role/password here are whatever you created on the data server in
|
# The role/password here are whatever you created on the data server in
|
||||||
# DEPLOYMENT.md step 2.
|
# DEPLOYMENT.md step 2.
|
||||||
DATABASE_URL=postgresql+asyncpg://chatapp:REPLACE_ME@<DATA_SERVER_PRIVATE_IP>:5432/chatapp
|
DATABASE_URL=postgresql+asyncpg://ds_chat:REPLACE_ME@<DATA_SERVER_PRIVATE_IP>:5432/ds_chat
|
||||||
|
|
||||||
# Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
# Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
SESSION_SECRET=REPLACE_ME
|
SESSION_SECRET=REPLACE_ME
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# /etc/systemd/system/chatapp.service
|
# /etc/systemd/system/ds-chat.service
|
||||||
#
|
#
|
||||||
# Install: sudo cp deploy/systemd/chatapp.service /etc/systemd/system/
|
# Install: sudo cp deploy/systemd/ds-chat.service /etc/systemd/system/
|
||||||
# sudo systemctl daemon-reload
|
# sudo systemctl daemon-reload
|
||||||
# sudo systemctl enable --now chatapp
|
# sudo systemctl enable --now ds-chat
|
||||||
#
|
#
|
||||||
# See ../../DEPLOYMENT.md for the full app-server setup this fits into.
|
# See ../../DEPLOYMENT.md for the full app-server setup this fits into.
|
||||||
# TLS termination and public-facing reverse proxying are handled by an
|
# TLS termination and public-facing reverse proxying are handled by an
|
||||||
@@ -10,17 +10,17 @@
|
|||||||
# unit just needs to be reachable on the TCP port below.
|
# unit just needs to be reachable on the TCP port below.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=KeepItTalking chat service app server
|
Description=DS Chat app server
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
# No Type= override -- defaults to "simple", which is correct here since
|
# No Type= override -- defaults to "simple", which is correct here since
|
||||||
# gunicorn runs in the foreground (no --daemon flag below) and doesn't send
|
# gunicorn runs in the foreground (no --daemon flag below) and doesn't send
|
||||||
# systemd's sd_notify readiness protocol.
|
# systemd's sd_notify readiness protocol.
|
||||||
User=chatapp
|
User=ds-chat
|
||||||
Group=chatapp
|
Group=ds-chat
|
||||||
WorkingDirectory=/srv/chatapp/backend
|
WorkingDirectory=/srv/ds-chat/backend
|
||||||
EnvironmentFile=/etc/chatapp/env
|
EnvironmentFile=/etc/ds-chat/env
|
||||||
Environment=PYTHONUNBUFFERED=1
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# 0.0.0.0 because Nginx Proxy Manager runs on a separate host -- the actual
|
# 0.0.0.0 because Nginx Proxy Manager runs on a separate host -- the actual
|
||||||
@@ -28,7 +28,7 @@ Environment=PYTHONUNBUFFERED=1
|
|||||||
# port to NPM's IP specifically, not the bind address. If NPM reaches this
|
# port to NPM's IP specifically, not the bind address. If NPM reaches this
|
||||||
# box over a private network interface, bind to that private IP instead
|
# box over a private network interface, bind to that private IP instead
|
||||||
# for defense in depth (belt-and-suspenders on top of the firewall rule).
|
# for defense in depth (belt-and-suspenders on top of the firewall rule).
|
||||||
ExecStart=/srv/chatapp/backend/.venv/bin/gunicorn app.main:app \
|
ExecStart=/srv/ds-chat/backend/.venv/bin/gunicorn app.main:app \
|
||||||
-k uvicorn.workers.UvicornWorker \
|
-k uvicorn.workers.UvicornWorker \
|
||||||
--workers 4 \
|
--workers 4 \
|
||||||
--bind 0.0.0.0:8000 \
|
--bind 0.0.0.0:8000 \
|
||||||
+12
-12
@@ -1,20 +1,20 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Day-2 deploy/upgrade script for the KeepItTalking app server. Run by hand
|
# Day-2 deploy/upgrade script for the DS Chat app server. Run by hand
|
||||||
# over SSH as the `chatapp` user (or via sudo -u chatapp):
|
# over SSH as the `ds-chat` user (or via sudo -u ds-chat):
|
||||||
#
|
#
|
||||||
# sudo -u chatapp /srv/chatapp/deploy/upgrade.sh
|
# sudo -u ds-chat /srv/ds-chat/deploy/upgrade.sh
|
||||||
#
|
#
|
||||||
# Fails loudly and stops before touching the running service if any step
|
# Fails loudly and stops before touching the running service if any step
|
||||||
# fails -- the previous deploy keeps running rather than being torn down
|
# fails -- the previous deploy keeps running rather than being torn down
|
||||||
# mid-upgrade. See ../DEPLOYMENT.md for what each step assumes is already
|
# mid-upgrade. See ../DEPLOYMENT.md for what each step assumes is already
|
||||||
# in place (venv, /etc/chatapp/env, the systemd unit, Node.js).
|
# in place (venv, /etc/ds-chat/env, the systemd unit, Node.js).
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_DIR="/srv/chatapp"
|
REPO_DIR="/srv/ds-chat"
|
||||||
BACKEND_DIR="${REPO_DIR}/backend"
|
BACKEND_DIR="${REPO_DIR}/backend"
|
||||||
FRONTEND_DIR="${REPO_DIR}/frontend"
|
FRONTEND_DIR="${REPO_DIR}/frontend"
|
||||||
ENV_FILE="/etc/chatapp/env"
|
ENV_FILE="/etc/ds-chat/env"
|
||||||
|
|
||||||
echo "==> Pulling latest code"
|
echo "==> Pulling latest code"
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
@@ -28,7 +28,7 @@ echo "==> Running database migrations"
|
|||||||
# alembic reads DATABASE_URL from the environment (backend/alembic/env.py),
|
# alembic reads DATABASE_URL from the environment (backend/alembic/env.py),
|
||||||
# so the env file has to actually be sourced into this shell first -- it's
|
# so the env file has to actually be sourced into this shell first -- it's
|
||||||
# not read automatically just because systemd's EnvironmentFile= points at
|
# not read automatically just because systemd's EnvironmentFile= points at
|
||||||
# it (that only applies to the chatapp.service process, not this script).
|
# it (that only applies to the ds-chat.service process, not this script).
|
||||||
set -a
|
set -a
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
@@ -40,20 +40,20 @@ cd "$FRONTEND_DIR"
|
|||||||
npm ci --silent
|
npm ci --silent
|
||||||
npm run build --silent
|
npm run build --silent
|
||||||
|
|
||||||
echo "==> Restarting chatapp"
|
echo "==> Restarting ds-chat"
|
||||||
# Active WebSocket connections drop here and reconnect automatically within
|
# Active WebSocket connections drop here and reconnect automatically within
|
||||||
# a few seconds (frontend/src/ws/useChatSocket.ts's exponential-backoff
|
# a few seconds (frontend/src/ws/useChatSocket.ts's exponential-backoff
|
||||||
# reconnect) -- expected, not a bug, and not worth a blue-green setup for.
|
# reconnect) -- expected, not a bug, and not worth a blue-green setup for.
|
||||||
sudo systemctl restart chatapp
|
sudo systemctl restart ds-chat
|
||||||
|
|
||||||
echo "==> Verifying"
|
echo "==> Verifying"
|
||||||
sleep 2
|
sleep 2
|
||||||
if curl -sf http://127.0.0.1:8000/api/health >/dev/null; then
|
if curl -sf http://127.0.0.1:8000/api/health >/dev/null; then
|
||||||
echo "Health check OK"
|
echo "Health check OK"
|
||||||
else
|
else
|
||||||
echo "Health check FAILED -- check: sudo journalctl -u chatapp -n 50" >&2
|
echo "Health check FAILED -- check: sudo journalctl -u ds-chat -n 50" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sudo systemctl status chatapp --no-pager -l | head -10
|
sudo systemctl status ds-chat --no-pager -l | head -10
|
||||||
|
|
||||||
echo "==> Done. journalctl -u chatapp -f to watch logs."
|
echo "==> Done. journalctl -u ds-chat -f to watch logs."
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# KeepItTalking frontend (Phase 1)
|
# DS Chat frontend (Phase 1)
|
||||||
|
|
||||||
React + Vite PWA. Login, room list, and chat views wired to the backend's
|
React + Vite PWA. Login, room list, and chat views wired to the backend's
|
||||||
REST API and `/ws/chat` WebSocket endpoint. See [`../README.md`](../README.md)
|
REST API and `/ws/chat` WebSocket endpoint. See [`../README.md`](../README.md)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>KeepItTalking</title>
|
<title>DS Chat</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function TopBar() {
|
|||||||
<header className="top-bar">
|
<header className="top-bar">
|
||||||
<div className="top-bar-brand">
|
<div className="top-bar-brand">
|
||||||
<img src={logo} alt="" className="top-bar-logo" />
|
<img src={logo} alt="" className="top-bar-logo" />
|
||||||
<span className="top-bar-title">KeepItTalking</span>
|
<span className="top-bar-title">DS Chat</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="top-bar-user">
|
<div className="top-bar-user">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function ForgotPasswordPage() {
|
|||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<div className="login-brand">
|
<div className="login-brand">
|
||||||
<img src={logo} alt="" />
|
<img src={logo} alt="" />
|
||||||
<span>KeepItTalking</span>
|
<span>DS Chat</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sent ? (
|
{sent ? (
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function LoginPage() {
|
|||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<div className="login-brand">
|
<div className="login-brand">
|
||||||
<img src={logo} alt="" />
|
<img src={logo} alt="" />
|
||||||
<span>KeepItTalking</span>
|
<span>DS Chat</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="login-copy">This is an invite-only site. Ask an admin for an account.</p>
|
<p className="login-copy">This is an invite-only site. Ask an admin for an account.</p>
|
||||||
<form className="login-form" onSubmit={handleSubmit}>
|
<form className="login-form" onSubmit={handleSubmit}>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function ResetPasswordPage() {
|
|||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<div className="login-brand">
|
<div className="login-brand">
|
||||||
<img src={logo} alt="" />
|
<img src={logo} alt="" />
|
||||||
<span>KeepItTalking</span>
|
<span>DS Chat</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{checking && <p className="login-copy">Checking your reset link…</p>}
|
{checking && <p className="login-copy">Checking your reset link…</p>}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function SignupPage() {
|
|||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<div className="login-brand">
|
<div className="login-brand">
|
||||||
<img src={logo} alt="" />
|
<img src={logo} alt="" />
|
||||||
<span>KeepItTalking</span>
|
<span>DS Chat</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{checking && <p className="login-copy">Checking your invite…</p>}
|
{checking && <p className="login-copy">Checking your invite…</p>}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'KeepItTalking',
|
name: 'DS Chat',
|
||||||
short_name: 'Talking',
|
short_name: 'DS Chat',
|
||||||
start_url: '/',
|
start_url: '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#07080f', // --ds-void
|
background_color: '#07080f', // --ds-void
|
||||||
|
|||||||
Reference in New Issue
Block a user