Compare commits
9 Commits
d79839b438
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f7f7eb9d2f | |||
| b527e24705 | |||
| 1af039ab0a | |||
| 03496884d5 | |||
| c87b5f0304 | |||
| b1ceb7cccb | |||
| 3a86385038 | |||
| 70750890e3 | |||
| 8f5310754c |
34
.env
34
.env
@@ -1,29 +1,28 @@
|
|||||||
#** Required
|
#** Required
|
||||||
DB_PASSWORD=C@nuck2024
|
DB_PASSWORD=r0sterCh!rp2026
|
||||||
JWT_SECRET=changemesupersecretjwtkey
|
JWT_SECRET=changemesupersecretjwtkey
|
||||||
|
|
||||||
#** App identity
|
#** App identity
|
||||||
PROJECT_NAME=jama
|
PROJECT_NAME=rosterchirp
|
||||||
APP_NAME=RosterChirp
|
APP_NAME=RosterChirp
|
||||||
DEFCHAT_NAME=General Chat
|
DEFCHAT_NAME=General Chat
|
||||||
ADMIN_NAME=Admin User
|
ADMIN_NAME=Admin User
|
||||||
ADMIN_EMAIL=admin@rosterchirp.local
|
ADMIN_EMAIL=admin@yourdomain.com
|
||||||
ADMIN_PASS=Admin@1234
|
ADMIN_PASS=Admin@1234
|
||||||
ADMPW_RESET=false
|
ADMPW_RESET=false
|
||||||
|
|
||||||
#** Database
|
#** Database
|
||||||
# DB names intentionally kept as 'jama' — matches the existing live database
|
DB_NAME=rosterchirp
|
||||||
DB_NAME=jama
|
DB_USER=rosterchirp
|
||||||
DB_USER=jama
|
|
||||||
# DB_HOST and DB_PORT are set automatically in docker-compose (host=db, port=5432)
|
# DB_HOST and DB_PORT are set automatically in docker-compose (host=db, port=5432)
|
||||||
|
|
||||||
#** Tenancy mode
|
#** Tenancy mode
|
||||||
# selfhost = single tenant (RosterChirp-Chat / RosterChirp-Brand / RosterChirp-Team)
|
# selfhost = single tenant (RosterChirp-Chat / RosterChirp-Brand / RosterChirp-Team)
|
||||||
# host = multi-tenant (RosterChirp-Host only)
|
# host = multi-tenant (RosterChirp-Host only)
|
||||||
APP_TYPE=host
|
APP_TYPE=selfhost
|
||||||
|
|
||||||
#** RosterChirp-Host only (ignored in selfhost mode)
|
#** RosterChirp-Host only (ignored in selfhost mode)
|
||||||
HOST_DOMAIN=jamahost.stretchy.ca
|
HOST_DOMAIN=yourdomain.com
|
||||||
HOST_ADMIN_KEY=VBGFHETSTTGRDDWAASJKH
|
HOST_ADMIN_KEY=VBGFHETSTTGRDDWAASJKH
|
||||||
|
|
||||||
#** Optional
|
#** Optional
|
||||||
@@ -32,15 +31,16 @@ TZ=America/Toronto
|
|||||||
|
|
||||||
#** Firebase Cloud Messaging (FCM) — Android background push
|
#** Firebase Cloud Messaging (FCM) — Android background push
|
||||||
# Web app config — from Firebase Console → Project Settings → General → Your apps
|
# Web app config — from Firebase Console → Project Settings → General → Your apps
|
||||||
FIREBASE_API_KEY=AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA
|
FIREBASE_API_KEY=
|
||||||
FIREBASE_PROJECT_ID=fcmtest-push
|
FIREBASE_PROJECT_ID=
|
||||||
FIREBASE_MESSAGING_SENDER_ID=439263996034
|
FIREBASE_MESSAGING_SENDER_ID=
|
||||||
FIREBASE_APP_ID=1:439263996034:web:62a6a6b0afdbad99fdec9b
|
FIREBASE_APP_ID=
|
||||||
# VAPID key — from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
|
# VAPID key — from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
|
||||||
FIREBASE_VAPID_KEY=BKUioOWptwKIfQJV9udX5P0VsIxLn3LC-Bj2eAenUNSZ5CoFmls3lQWxu03rcO9XZcXA-aYaGuD-jWNH3fOybN8
|
FIREBASE_VAPID_KEY=
|
||||||
# Service account — from Firebase Console → Project Settings → Service accounts → Generate new private key
|
# Service account — from Firebase Console → Project Settings → Service accounts → Generate new private key
|
||||||
FIREBASE_SERVICE_ACCOUNT={"type": "service_account","project_id": "fcmtest-push","private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad","private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\s+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com", "client_id": "103917424542871804597", "auth_uri":"https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com", "universe_domain": "googleapis.com"}
|
FIREBASE_SERVICE_ACCOUNT=
|
||||||
|
|
||||||
VAPID_SUBJECT=mailto:webpush@rosterchirp.com
|
#Required for iOS notifications (create here: https://vapidkeys.com/ with valid email address)
|
||||||
VAPID_PUBLIC=BOGwB-erNPVGQqSkZFl8uBaWcoTaU8fVdjXwGlHdq0JAmaKVjde5NE5upqktsvwIW_c19O8Fv0hLMC44skdjkVY
|
VAPID_SUBJECT=mailto:webpush@yourdomain.com
|
||||||
VAPID_PRIVATE=ZqCDnzcbpoUyvzMQpT9gcPV7h-GXn5BPTSvWBtlNmxQ
|
VAPID_PUBLIC=
|
||||||
|
VAPID_PRIVATE=
|
||||||
22
.env.example
22
.env.example
@@ -22,22 +22,32 @@ DB_USER=rosterchirp
|
|||||||
APP_TYPE=selfhost
|
APP_TYPE=selfhost
|
||||||
|
|
||||||
# ── RosterChirp-Host only (ignored in selfhost mode) ─────────────────────────────────
|
# ── RosterChirp-Host only (ignored in selfhost mode) ─────────────────────────────────
|
||||||
# HOST_DOMAIN=rosterchirp.com
|
# APP_DOMAIN=example.com
|
||||||
|
# HOST_SLUG=chathost
|
||||||
# HOST_ADMIN_KEY=change_me_host_admin_secret
|
# HOST_ADMIN_KEY=change_me_host_admin_secret
|
||||||
|
# To access the tenant host it would be http|https://HOST_SLUG.APP_DOMAIN (ie: http|https://chathost.example.com)
|
||||||
|
|
||||||
# ── Optional ──────────────────────────────────────────────────────────────────
|
# ── Optional ──────────────────────────────────────────────────────────────────
|
||||||
PORT=3000
|
PORT=3000
|
||||||
TZ=UTC
|
TZ=UTC
|
||||||
|
|
||||||
# ── Firebase Cloud Messaging (FCM) — Android background push ──────────────────
|
# ── Firebase Cloud Messaging (FCM) https://firebase.google.com/ — Android background push ──────────────────
|
||||||
# Required for push notifications to work on Android when the app is backgrounded.
|
# Required for push notifications to work on Android when the app is backgrounded.
|
||||||
# Get these from: Firebase Console → Project Settings → General → Your web app
|
# -- Get these from: Firebase Console → Project Settings → General → Your web app
|
||||||
# FIREBASE_API_KEY=
|
# FIREBASE_API_KEY=
|
||||||
# FIREBASE_PROJECT_ID=
|
# FIREBASE_PROJECT_ID=
|
||||||
# FIREBASE_MESSAGING_SENDER_ID=
|
# FIREBASE_MESSAGING_SENDER_ID=
|
||||||
# FIREBASE_APP_ID=
|
# FIREBASE_APP_ID=
|
||||||
# Get VAPID key from: Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
|
# -- Get VAPID key from: Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
|
||||||
# FIREBASE_VAPID_KEY=
|
# FIREBASE_VAPID_KEY=
|
||||||
# Get service account JSON from: Firebase Console → Project Settings → Service accounts → Generate new private key
|
# -- Get service account JSON from: Firebase Console → Project Settings → Service accounts → Generate new private key
|
||||||
# Paste the entire JSON content as a single-line string:
|
# -- Paste the entire JSON content as a single-line string (include curlybracket to curlybracket):
|
||||||
# FIREBASE_SERVICE_ACCOUNT={"type":"service_account","project_id":"..."}
|
# FIREBASE_SERVICE_ACCOUNT={"type":"service_account","project_id":"..."}
|
||||||
|
|
||||||
|
# ── iOS (iPhone) background push ──────────────────
|
||||||
|
# Required for push notifications to work on iOS when the app is backgrounded.
|
||||||
|
# -- Get these from: https://vapidkeys.com/
|
||||||
|
# -- The subject requires the "mailto:yourvalid@email.com" without quotes
|
||||||
|
# VAPID_SUBJECT=
|
||||||
|
# VAPID_PUBLIC=
|
||||||
|
# FVAPID_PRIVATE=
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
#** Required
|
|
||||||
DB_PASSWORD=C@nuck2024
|
|
||||||
JWT_SECRET=changemesupersecretjwtkey
|
|
||||||
|
|
||||||
#** App identity
|
|
||||||
PROJECT_NAME=rosterchirp-dev
|
|
||||||
APP_NAME=RosterChirp
|
|
||||||
DEFCHAT_NAME=General Chat
|
|
||||||
ADMIN_NAME=Admin User
|
|
||||||
ADMIN_EMAIL=admin@rosterchirp.local
|
|
||||||
ADMIN_PASS=Admin@1234
|
|
||||||
ADMPW_RESET=false
|
|
||||||
|
|
||||||
#** Database
|
|
||||||
# DB names intentionally kept as 'rosterchirp' — matches the existing live database
|
|
||||||
DB_NAME=rosterchirp
|
|
||||||
DB_USER=rosterchirp
|
|
||||||
# DB_HOST and DB_PORT are set automatically in docker-compose (host=db, port=5432)
|
|
||||||
|
|
||||||
#** Tenancy mode
|
|
||||||
# selfhost = single tenant (RosterChirp-Chat / RosterChirp-Brand / RosterChirp-Team)
|
|
||||||
# host = multi-tenant (RosterChirp-Host only)
|
|
||||||
APP_TYPE=host
|
|
||||||
|
|
||||||
#** RosterChirp-Host only (ignored in selfhost mode)
|
|
||||||
HOST_DOMAIN=rosterchirp.com
|
|
||||||
HOST_ADMIN_KEY=VBGFHEANTTGRDDWAASJKH
|
|
||||||
|
|
||||||
#** Optional
|
|
||||||
PORT=3244
|
|
||||||
TZ=America/Toronto
|
|
||||||
|
|
||||||
#** Firebase Cloud Messaging (FCM) — Android background push
|
|
||||||
# Web app config — from Firebase Console → Project Settings → General → Your apps
|
|
||||||
FIREBASE_API_KEY=AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA
|
|
||||||
FIREBASE_PROJECT_ID=fcmtest-push
|
|
||||||
FIREBASE_MESSAGING_SENDER_ID=439263996034
|
|
||||||
FIREBASE_APP_ID=1:439263996034:web:62a6a6b0afdbad99fdec9b
|
|
||||||
# VAPID key — from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
|
|
||||||
FIREBASE_VAPID_KEY=BE6hPKkbf-h0lUQ1tYo249pBOdZFFcWQn9suwg3NDwSE8C_hv8hk1dUY9zxHBQEChO_IAqyFZplF_SUb5c4Ofrw
|
|
||||||
# Service account — from Firebase Console → Project Settings → Service accounts → Generate new private key
|
|
||||||
FIREBASE_SERVICE_ACCOUNT={"type": "service_account", "project_id": "fcmtest-push", "private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\ns+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLwTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n","client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com", "client_id": "103917424542871804597", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token","auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com", "universe_domain": "googleapis.com" }
|
|
||||||
|
|
||||||
RC_VERSION=latest
|
|
||||||
|
|
||||||
VAPID_SUBJECT=mailto:devpush@rosterchirp.com
|
|
||||||
VAPID_PUBLIC=BGYS6vMY7zlx31UKRN9QcaOwfomoDJ50_MtfTcfE84q5bhTLq0rM1zSa6uzBTRBxZuFW1kMQP7ardN_jog3T14Y
|
|
||||||
VAPID_PRIVATE=8SnDSEy_gs2jNwXLtOchZfHW0ppy_RG8wtvjSjYGA48
|
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Environment — never commit real credentials
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
backend/node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
data/
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# Docker local volume mounts
|
||||||
|
postgres-data/
|
||||||
|
|
||||||
|
# OS / editor artefacts
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Private reference / scratch docs
|
||||||
|
ReferenceDocs/
|
||||||
@@ -12,18 +12,17 @@
|
|||||||
# CF_API_TOKEN=your_cloudflare_token (or equivalent)
|
# CF_API_TOKEN=your_cloudflare_token (or equivalent)
|
||||||
#
|
#
|
||||||
# 3. Add a wildcard DNS record in your DNS provider:
|
# 3. Add a wildcard DNS record in your DNS provider:
|
||||||
# *.rosterchirp.com → your server IP
|
# *.example.com → your server IP
|
||||||
# rosterchirp.com → your server IP
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# Copy this file to /etc/caddy/Caddyfile (or wherever Caddy reads it)
|
# Copy this file to /etc/caddy/Caddyfile (or wherever Caddy reads it)
|
||||||
# Reload: caddy reload
|
# Reload: caddy reload
|
||||||
|
|
||||||
# ── Wildcard subdomain ────────────────────────────────────────────────────────
|
# ── Wildcard subdomain ────────────────────────────────────────────────────────
|
||||||
# Handles team1.rosterchirp.com, teamB.rosterchirp.com, etc.
|
# Handles mychat.example.com, teamB.example.com, chathost.example.com, etc.
|
||||||
# Replace rosterchirp.com with your actual HOST_DOMAIN.
|
# Replace example.com with your actual APP_DOMAIN.
|
||||||
|
|
||||||
*.rosterchirp.com {
|
*.example.com {
|
||||||
tls {
|
tls {
|
||||||
dns cloudflare {env.CF_API_TOKEN}
|
dns cloudflare {env.CF_API_TOKEN}
|
||||||
}
|
}
|
||||||
@@ -47,20 +46,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Base domain (host admin panel) ───────────────────────────────────────────
|
|
||||||
rosterchirp.com {
|
|
||||||
reverse_proxy localhost:3000
|
|
||||||
header {
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
||||||
X-Content-Type-Options nosniff
|
|
||||||
-Server
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Custom tenant domains ─────────────────────────────────────────────────────
|
# ── Custom tenant domains ─────────────────────────────────────────────────────
|
||||||
# When a tenant sets up a custom domain (e.g. chat.theircompany.com):
|
# When a tenant sets up a custom domain (e.g. chat.theircompany.com):
|
||||||
#
|
#
|
||||||
# 1. They add a DNS CNAME: chat.theircompany.com → rosterchirp.com
|
# 1. They add a DNS CNAME: chat.theircompany.com → your server IP
|
||||||
#
|
#
|
||||||
# 2. You add a block here and reload Caddy.
|
# 2. You add a block here and reload Caddy.
|
||||||
# Caddy will automatically obtain and renew the SSL cert.
|
# Caddy will automatically obtain and renew the SSL cert.
|
||||||
@@ -80,7 +69,7 @@ rosterchirp.com {
|
|||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# *.rosterchirp.com, rosterchirp.com {
|
# *.example.com {
|
||||||
# tls { on_demand }
|
# tls { on_demand }
|
||||||
# reverse_proxy localhost:3000
|
# reverse_proxy localhost:3000
|
||||||
# }
|
# }
|
||||||
|
|||||||
297
README.md
297
README.md
@@ -1,7 +1,16 @@
|
|||||||
# jama 💬
|
<<<<<<< HEAD
|
||||||
### *just another messaging app*
|
# RosterChirp
|
||||||
|
|
||||||
A modern, self-hosted team messaging Progressive Web App (PWA) built for small to medium teams. jama runs entirely in a single Docker container with no external database dependencies — all data is stored locally using SQLite.
|
A modern, self-hosted team messaging Progressive Web App (PWA) built for small to medium teams. RosterChirp runs via Docker Compose with PostgreSQL and supports both single-tenant (self-hosted) and multi-tenant (hosted) deployments.
|
||||||
|
|
||||||
|
Development was vibe-coded using Claude.ai.
|
||||||
|
|
||||||
|
**Current version:** 0.13.1
|
||||||
|
=======
|
||||||
|
# rosterchirp
|
||||||
|
|
||||||
|
A modern, self-hosted team messaging Progressive Web App (PWA) built for small to medium teams. rosterchirp runs entirely in a single Docker container with no external database dependencies — all data is stored locally using SQLite.
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,6 +38,17 @@ A modern, self-hosted team messaging Progressive Web App (PWA) built for small t
|
|||||||
- **Read-only channels** — Admin-configurable announcement-style channels; only admins can post
|
- **Read-only channels** — Admin-configurable announcement-style channels; only admins can post
|
||||||
- **Support group** — A private admin-only group that receives submissions from the login page contact form
|
- **Support group** — A private admin-only group that receives submissions from the login page contact form
|
||||||
- **Custom group names** — Each user can set a personal display name for any group, visible only to them
|
- **Custom group names** — Each user can set a personal display name for any group, visible only to them
|
||||||
|
- **Group Messages** — Managed private groups (created and controlled by admins via Group Manager) appear in a separate "Private Group Messages" section in the sidebar
|
||||||
|
|
||||||
|
### Schedule
|
||||||
|
- **Team schedule** — Full calendar view for creating and managing team events (Team plan)
|
||||||
|
- **Desktop & mobile views** — Dedicated layout for each; desktop shows a full monthly grid, mobile shows a scrollable event list
|
||||||
|
- **Event types** — Colour-coded event categories (configurable by admins)
|
||||||
|
- **Recurring events** — Create daily, weekly, or custom-interval recurring events; only future occurrences are shown
|
||||||
|
- **Availability** — Users can mark their availability per event
|
||||||
|
- **Keyword filter** — Search events by keyword with word-boundary matching; quoted terms match exactly
|
||||||
|
- **Type filter** — Filter events by event type across the current month (including past events, shown greyed)
|
||||||
|
- **Past event protection** — New events cannot be created with a start date/time in the past
|
||||||
|
|
||||||
### Users & Profiles
|
### Users & Profiles
|
||||||
- **Authentication** — Email/password login with optional Remember Me (30-day session)
|
- **Authentication** — Email/password login with optional Remember Me (30-day session)
|
||||||
@@ -36,19 +56,29 @@ A modern, self-hosted team messaging Progressive Web App (PWA) built for small t
|
|||||||
- **User profiles** — Custom display name, avatar upload, About Me text
|
- **User profiles** — Custom display name, avatar upload, About Me text
|
||||||
- **Profile popup** — Click any user's avatar in chat to view their profile card
|
- **Profile popup** — Click any user's avatar in chat to view their profile card
|
||||||
- **Admin badge** — Admins display a role badge; can be hidden per-user in Profile settings
|
- **Admin badge** — Admins display a role badge; can be hidden per-user in Profile settings
|
||||||
|
- **Online presence** — Real-time online/offline status tracked per user
|
||||||
|
- **Last seen** — Users' last online timestamp updated on disconnect
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
- **In-app notifications** — Mention alerts with toast notifications
|
- **In-app notifications** — Mention alerts with toast notifications
|
||||||
- **Unread indicators** — Private groups with new unread messages are highlighted and bolded in the sidebar
|
- **Unread indicators** — Private groups with new unread messages are highlighted and bolded in the sidebar
|
||||||
- **Web Push notifications** — Badge and push notifications for mentions and new private messages when the app is backgrounded or closed (requires HTTPS)
|
- **Push notifications** — Firebase Cloud Messaging (FCM) push notifications for mentions and new private messages when the app is backgrounded or closed (Android PWA; requires HTTPS and Firebase setup)
|
||||||
|
|
||||||
### Admin & Settings
|
### Admin & Settings
|
||||||
- **User Manager** — Create, suspend, activate, delete users; reset passwords; change roles
|
- **User Manager** — Create, suspend, activate, delete users; reset passwords; change roles
|
||||||
- **Bulk CSV import** — Import multiple users at once from a CSV file
|
- **Bulk CSV import** — Import multiple users at once from a CSV file
|
||||||
- **App branding** — Customize app name and logo via the Settings panel
|
- **Group Manager** — Create and manage private groups and their membership centrally (Team plan)
|
||||||
|
- **App branding** — Customize app name, logo, and icons via the Settings panel (Brand+ plan)
|
||||||
- **Reset to defaults** — One-click reset of all branding customizations
|
- **Reset to defaults** — One-click reset of all branding customizations
|
||||||
- **Version display** — Current app version shown in the Settings panel
|
- **Version display** — Current app version shown in the Settings panel
|
||||||
- **Default user password** — Configurable via `USER_PASS` env var; shown live in User Manager
|
- **Default user password** — Configurable via `USER_PASS` env var; shown live in User Manager
|
||||||
|
- **Feature flags** — Plan-gated features (branding, group manager, schedule manager) controlled via settings
|
||||||
|
|
||||||
|
### User Deletion
|
||||||
|
- Deleting a user scrubs their email, name, and avatar immediately
|
||||||
|
- Their messages are marked deleted (content removed); direct message threads become read-only
|
||||||
|
- Group memberships, sessions, push subscriptions, and notifications are purged
|
||||||
|
- Suspended users retain all data and can be re-activated
|
||||||
|
|
||||||
### Help & Onboarding
|
### Help & Onboarding
|
||||||
- **Getting Started modal** — Appears automatically on first login; users can dismiss permanently with "Do not show again"
|
- **Getting Started modal** — Appears automatically on first login; users can dismiss permanently with "Do not show again"
|
||||||
@@ -67,18 +97,39 @@ A modern, self-hosted team messaging Progressive Web App (PWA) built for small t
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Deployment Modes
|
||||||
|
|
||||||
|
| Mode | Description |
|
||||||
|
|---|---|
|
||||||
|
| `selfhost` | Single tenant — one team, one database schema. Default. |
|
||||||
|
| `host` | Multi-tenant — one schema per tenant, provisioned via subdomains. Requires `APP_DOMAIN`, `HOST_SLUG`, and `HOST_ADMIN_KEY`. |
|
||||||
|
|
||||||
|
Set `APP_TYPE=selfhost` or `APP_TYPE=host` in `.env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plans & Feature Flags
|
||||||
|
|
||||||
|
| Plan | Features |
|
||||||
|
|---|---|
|
||||||
|
| **RosterChirp-Chat** | Messaging, channels, DMs, profiles, push notifications |
|
||||||
|
| **RosterChirp-Brand** | Everything in Chat + custom branding (logo, app name, icons) |
|
||||||
|
| **RosterChirp-Team** | Everything in Brand + Group Manager + Schedule Manager |
|
||||||
|
|
||||||
|
Feature flags are stored in the database `settings` table and can be toggled by the admin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Backend | Node.js, Express, Socket.io |
|
| Backend | Node.js, Express, Socket.io |
|
||||||
| Database | SQLite (better-sqlite3) |
|
| Database | PostgreSQL 16 (via `pg`) |
|
||||||
| Frontend | React 18, Vite |
|
| Frontend | React 18, Vite |
|
||||||
| Markdown rendering | marked |
|
| Push notifications | Firebase Cloud Messaging (FCM) |
|
||||||
| Emoji picker | emoji-mart |
|
|
||||||
| Image processing | sharp |
|
| Image processing | sharp |
|
||||||
| Push notifications | web-push (VAPID) |
|
| Containerization | Docker, Docker Compose v2 |
|
||||||
| Containerization | Docker, Docker Compose |
|
|
||||||
| Reverse proxy / SSL | Caddy (recommended) |
|
| Reverse proxy / SSL | Caddy (recommended) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -86,8 +137,9 @@ A modern, self-hosted team messaging Progressive Web App (PWA) built for small t
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **Docker** and **Docker Compose v2**
|
- **Docker** and **Docker Compose v2**
|
||||||
- A domain name with DNS pointed at your server (required for HTTPS and Web Push notifications)
|
- A domain name with DNS pointed at your server (required for HTTPS and push notifications)
|
||||||
- Ports **80** and **443** open on your server firewall (if using Caddy for SSL)
|
- Ports **80** and **443** open on your server firewall (if using Caddy for SSL)
|
||||||
|
- (Optional) A Firebase project for push notifications
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -102,7 +154,7 @@ All builds use `build.sh`. No host Node.js installation is required.
|
|||||||
./build.sh
|
./build.sh
|
||||||
|
|
||||||
# Build and tag as a specific version
|
# Build and tag as a specific version
|
||||||
./build.sh 1.0.0
|
./build.sh 0.13.1
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -112,14 +164,18 @@ All builds use `build.sh`. No host Node.js installation is required.
|
|||||||
### 1. Clone the repository
|
### 1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://your-gitea/youruser/jama.git
|
<<<<<<< HEAD
|
||||||
cd jama
|
git clone https://your-git/youruser/rosterchirp.git
|
||||||
|
=======
|
||||||
|
git clone https://your-gitea/youruser/rosterchirp.git
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
cd rosterchirp
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Build the Docker image
|
### 2. Build the Docker image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh 1.0.0
|
./build.sh 0.13.1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Configure environment
|
### 3. Configure environment
|
||||||
@@ -129,13 +185,13 @@ cp .env.example .env
|
|||||||
nano .env
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
At minimum, change `ADMIN_EMAIL`, `ADMIN_PASS`, and `JWT_SECRET`.
|
At minimum, set `ADMIN_EMAIL`, `ADMIN_PASS`, `ADMIN_NAME`, `JWT_SECRET`, and `DB_PASSWORD`.
|
||||||
|
|
||||||
### 4. Start the container
|
### 4. Start the services
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
docker compose logs -f jama
|
docker compose logs -f rosterchirp
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Log in
|
### 5. Log in
|
||||||
@@ -146,40 +202,79 @@ Open `http://your-server:3000`, log in with your `ADMIN_EMAIL` and `ADMIN_PASS`,
|
|||||||
|
|
||||||
## HTTPS & SSL
|
## HTTPS & SSL
|
||||||
|
|
||||||
jama does not manage SSL itself. Use **Caddy** as a reverse proxy.
|
<<<<<<< HEAD
|
||||||
|
RosterChirp does not manage SSL itself. Use **Caddy** as a reverse proxy.
|
||||||
|
=======
|
||||||
|
rosterchirp does not manage SSL itself. Use **Caddy** as a reverse proxy.
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
|
||||||
### Caddyfile
|
### Caddyfile
|
||||||
|
|
||||||
```
|
```
|
||||||
chat.yourdomain.com {
|
chat.yourdomain.com {
|
||||||
reverse_proxy jama:3000
|
reverse_proxy rosterchirp:3000
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### docker-compose.yaml (with Caddy)
|
### docker-compose.yaml (with Caddy)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
|
||||||
services:
|
services:
|
||||||
jama:
|
rosterchirp:
|
||||||
image: jama:${JAMA_VERSION:-latest}
|
<<<<<<< HEAD
|
||||||
container_name: jama
|
image: rosterchirp:${ROSTERCHIRP_VERSION:-latest}
|
||||||
|
=======
|
||||||
|
image: rosterchirp:${rosterchirp_VERSION:-latest}
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
container_name: rosterchirp
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
- APP_TYPE=${APP_TYPE:-selfhost}
|
||||||
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
|
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@jama.local}
|
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@rosterchirp.local}
|
||||||
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
|
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
|
||||||
- USER_PASS=${USER_PASS:-user@1234}
|
- USER_PASS=${USER_PASS:-user@1234}
|
||||||
- ADMPW_RESET=${ADMPW_RESET:-false}
|
- ADMPW_RESET=${ADMPW_RESET:-false}
|
||||||
- JWT_SECRET=${JWT_SECRET:-changeme}
|
- JWT_SECRET=${JWT_SECRET:-changeme}
|
||||||
- APP_NAME=${APP_NAME:-jama}
|
<<<<<<< HEAD
|
||||||
- JAMA_VERSION=${JAMA_VERSION:-latest}
|
- APP_NAME=${APP_NAME:-RosterChirp}
|
||||||
|
- DEFCHAT_NAME=${DEFCHAT_NAME:-General Chat}
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_NAME=${DB_NAME:-rosterchirp}
|
||||||
|
- DB_USER=${DB_USER:-rosterchirp}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- ROSTERCHIRP_VERSION=${ROSTERCHIRP_VERSION:-latest}
|
||||||
volumes:
|
volumes:
|
||||||
- jama_db:/app/data
|
- rosterchirp_uploads:/app/uploads
|
||||||
- jama_uploads:/app/uploads
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: rosterchirp_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${DB_NAME:-rosterchirp}
|
||||||
|
- POSTGRES_USER=${DB_USER:-rosterchirp}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- rosterchirp_db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-rosterchirp}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
=======
|
||||||
|
- APP_NAME=${APP_NAME:-rosterchirp}
|
||||||
|
- rosterchirp_VERSION=${rosterchirp_VERSION:-latest}
|
||||||
|
volumes:
|
||||||
|
- rosterchirp_db:/app/data
|
||||||
|
- rosterchirp_uploads:/app/uploads
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:alpine
|
image: caddy:alpine
|
||||||
@@ -194,11 +289,11 @@ services:
|
|||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_certs:/config
|
- caddy_certs:/config
|
||||||
depends_on:
|
depends_on:
|
||||||
- jama
|
- rosterchirp
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
jama_db:
|
rosterchirp_db:
|
||||||
jama_uploads:
|
rosterchirp_uploads:
|
||||||
caddy_data:
|
caddy_data:
|
||||||
caddy_certs:
|
caddy_certs:
|
||||||
```
|
```
|
||||||
@@ -209,24 +304,57 @@ volumes:
|
|||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `JAMA_VERSION` | `latest` | Docker image tag to run |
|
<<<<<<< HEAD
|
||||||
|
| `APP_TYPE` | `selfhost` | Deployment mode: `selfhost` (single tenant) or `host` (multi-tenant) |
|
||||||
|
| `ROSTERCHIRP_VERSION` | `latest` | Docker image tag to run |
|
||||||
|
=======
|
||||||
|
| `rosterchirp_VERSION` | `latest` | Docker image tag to run |
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
| `TZ` | `UTC` | Container timezone (e.g. `America/Toronto`) |
|
| `TZ` | `UTC` | Container timezone (e.g. `America/Toronto`) |
|
||||||
| `ADMIN_NAME` | `Admin User` | Display name of the default admin account |
|
| `ADMIN_NAME` | `Admin User` | Display name of the default admin account |
|
||||||
| `ADMIN_EMAIL` | `admin@jama.local` | Login email for the default admin account |
|
| `ADMIN_EMAIL` | `admin@rosterchirp.local` | Login email for the default admin account |
|
||||||
| `ADMIN_PASS` | `Admin@1234` | Initial password for the default admin account |
|
| `ADMIN_PASS` | `Admin@1234` | Initial password for the default admin account |
|
||||||
| `USER_PASS` | `user@1234` | Default temporary password for bulk-imported users when no password is specified in CSV |
|
| `USER_PASS` | `user@1234` | Default temporary password for bulk-imported users when no password is specified in CSV |
|
||||||
| `ADMPW_RESET` | `false` | If `true`, resets the **admin** password to `ADMIN_PASS` on every restart. Emergency access recovery only. Shows a warning banner when active. |
|
| `ADMPW_RESET` | `false` | If `true`, resets the admin password to `ADMIN_PASS` on every restart. Emergency recovery only. |
|
||||||
| `JWT_SECRET` | *(insecure default)* | Secret used to sign auth tokens. **Must be changed in production.** |
|
| `JWT_SECRET` | *(insecure default)* | Secret used to sign auth tokens. **Must be changed in production.** |
|
||||||
|
<<<<<<< HEAD
|
||||||
|
| `APP_NAME` | `RosterChirp` | Initial application name (can also be changed in Settings UI) |
|
||||||
|
| `DEFCHAT_NAME` | `General Chat` | Name of the default public channel created on first run |
|
||||||
|
| `DB_HOST` | `db` | PostgreSQL hostname |
|
||||||
|
| `DB_NAME` | `rosterchirp` | PostgreSQL database name |
|
||||||
|
| `DB_USER` | `rosterchirp` | PostgreSQL username |
|
||||||
|
| `DB_PASSWORD` | *(required)* | PostgreSQL password. **Avoid `!` — shell interpolation issue with Docker Compose.** |
|
||||||
|
| `APP_DOMAIN` | — | Base domain for multi-tenant host mode (e.g. `example.com`) |
|
||||||
|
| `HOST_SLUG` | — | Subdomain slug for the host control panel (e.g. `chathost` → `chathost.example.com`) |
|
||||||
|
| `HOST_ADMIN_KEY` | — | Secret key for the host control plane API |
|
||||||
|
=======
|
||||||
| `PORT` | `3000` | Host port to bind (without Caddy) |
|
| `PORT` | `3000` | Host port to bind (without Caddy) |
|
||||||
| `APP_NAME` | `jama` | Initial application name (can also be changed in Settings UI) |
|
| `APP_NAME` | `rosterchirp` | Initial application name (can also be changed in Settings UI) |
|
||||||
| `DEFCHAT_NAME` | `General Chat` | Name of the default public group created on first run |
|
| `DEFCHAT_NAME` | `General Chat` | Name of the default public group created on first run |
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
|
||||||
> `ADMIN_EMAIL` and `ADMIN_PASS` are only used on the **first run**. Once the database exists they are ignored — unless `ADMPW_RESET=true`.
|
### Firebase Push Notification Variables (optional)
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|---|---|
|
||||||
|
| `FIREBASE_API_KEY` | Firebase web app API key |
|
||||||
|
| `FIREBASE_PROJECT_ID` | Firebase project ID |
|
||||||
|
| `FIREBASE_MESSAGING_SENDER_ID` | Firebase messaging sender ID |
|
||||||
|
| `FIREBASE_APP_ID` | Firebase web app ID |
|
||||||
|
| `FIREBASE_VAPID_KEY` | Web Push certificate public key (from Firebase Cloud Messaging tab) |
|
||||||
|
| `FIREBASE_SERVICE_ACCOUNT` | Full service account JSON, stringified (remove all newlines) |
|
||||||
|
|
||||||
|
> `ADMIN_EMAIL` and `ADMIN_PASS` are only used on the **first run**. Once the database is seeded they are ignored — unless `ADMPW_RESET=true`.
|
||||||
|
|
||||||
### Example `.env`
|
### Example `.env`
|
||||||
|
|
||||||
```env
|
```env
|
||||||
JAMA_VERSION=1.0.0
|
<<<<<<< HEAD
|
||||||
|
ROSTERCHIRP_VERSION=0.13.1
|
||||||
|
APP_TYPE=selfhost
|
||||||
|
=======
|
||||||
|
rosterchirp_VERSION=1.0.0
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
TZ=America/Toronto
|
TZ=America/Toronto
|
||||||
|
|
||||||
ADMIN_NAME=Your Name
|
ADMIN_NAME=Your Name
|
||||||
@@ -238,9 +366,17 @@ ADMPW_RESET=false
|
|||||||
|
|
||||||
JWT_SECRET=replace-this-with-a-long-random-string-at-least-32-chars
|
JWT_SECRET=replace-this-with-a-long-random-string-at-least-32-chars
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
APP_NAME=RosterChirp
|
||||||
|
=======
|
||||||
PORT=3000
|
PORT=3000
|
||||||
APP_NAME=jama
|
APP_NAME=rosterchirp
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
DEFCHAT_NAME=General Chat
|
DEFCHAT_NAME=General Chat
|
||||||
|
|
||||||
|
DB_NAME=rosterchirp
|
||||||
|
DB_USER=rosterchirp
|
||||||
|
DB_PASSWORD=a-strong-db-password
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -266,7 +402,7 @@ Accessible from the bottom-left menu (admin only).
|
|||||||
| Reset password | User is forced to set a new password on next login |
|
| Reset password | User is forced to set a new password on next login |
|
||||||
| Suspend | Blocks login; messages are preserved |
|
| Suspend | Blocks login; messages are preserved |
|
||||||
| Activate | Re-enables a suspended account |
|
| Activate | Re-enables a suspended account |
|
||||||
| Delete | Removes account; messages remain attributed to user |
|
| Delete | Scrubs account data; messages are removed; threads become read-only |
|
||||||
| Change role | Promote member → admin or demote admin → member |
|
| Change role | Promote member → admin or demote admin → member |
|
||||||
|
|
||||||
### CSV Import Format
|
### CSV Import Format
|
||||||
@@ -292,6 +428,7 @@ Jane Smith,jane@example.com,,admin
|
|||||||
| Rename | Admin only | Owner only | ❌ Not allowed |
|
| Rename | Admin only | Owner only | ❌ Not allowed |
|
||||||
| Read-only mode | ✅ Optional | ❌ N/A | ❌ N/A |
|
| Read-only mode | ✅ Optional | ❌ N/A | ❌ N/A |
|
||||||
| Duplicate prevention | N/A | ✅ Redirects to existing | ✅ Redirects to existing |
|
| Duplicate prevention | N/A | ✅ Redirects to existing | ✅ Redirects to existing |
|
||||||
|
| Managed (Group Manager) | ❌ | ✅ Optional | ❌ |
|
||||||
|
|
||||||
### @Mention Scoping
|
### @Mention Scoping
|
||||||
|
|
||||||
@@ -313,13 +450,45 @@ Any user can set a personal display name for any group:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Schedule
|
||||||
|
|
||||||
|
The Schedule page (Team plan) provides a full team calendar:
|
||||||
|
|
||||||
|
- **Desktop view** — Monthly grid with event cards per day
|
||||||
|
- **Mobile view** — Scrollable event list with a date picker
|
||||||
|
- **Event types** — Colour-coded categories created by admins
|
||||||
|
- **Recurring events** — Set daily, weekly, or custom recurrence intervals
|
||||||
|
- **Availability** — Members can mark availability per event
|
||||||
|
- **Keyword search** — Unquoted terms match word prefixes; quoted terms match whole words exactly
|
||||||
|
- **Type filter** — Filter by event type across the full current month
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Push Notifications
|
||||||
|
|
||||||
|
RosterChirp uses **Firebase Cloud Messaging (FCM)** for push notifications. HTTPS is required.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Create a Firebase project at [console.firebase.google.com](https://console.firebase.google.com)
|
||||||
|
2. Add a **Web app** → copy the config values into `.env`
|
||||||
|
3. Go to **Project Settings → Cloud Messaging → Web Push certificates** → generate a key pair → copy the public key as `FIREBASE_VAPID_KEY`
|
||||||
|
4. Go to **Project Settings → Service accounts → Generate new private key** → download the JSON → stringify it (remove all newlines) → set as `FIREBASE_SERVICE_ACCOUNT`
|
||||||
|
|
||||||
|
Push notifications are sent for:
|
||||||
|
- New messages in private groups (to all members except the sender)
|
||||||
|
- New messages in public channels (to all subscribers except the sender)
|
||||||
|
- Image messages show as `📷 Image`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Help Content
|
## Help Content
|
||||||
|
|
||||||
The Getting Started guide is sourced from `data/help.md`. Edit before running `build.sh` — it is baked into the image at build time.
|
The Getting Started guide is sourced from `data/help.md`. Edit before running `build.sh` — it is baked into the image at build time.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nano data/help.md
|
nano data/help.md
|
||||||
./build.sh 1.0.0
|
./build.sh 0.13.1
|
||||||
```
|
```
|
||||||
|
|
||||||
Users can access the guide at any time via **User menu → Help**.
|
Users can access the guide at any time via **User menu → Help**.
|
||||||
@@ -330,37 +499,59 @@ Users can access the guide at any time via **User menu → Help**.
|
|||||||
|
|
||||||
| Volume | Container path | Contents |
|
| Volume | Container path | Contents |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `jama_db` | `/app/data` | SQLite database (`jama.db`), `help.md` |
|
<<<<<<< HEAD
|
||||||
| `jama_uploads` | `/app/uploads` | Avatars, logos, PWA icons, message images |
|
| `rosterchirp_db` | `/var/lib/postgresql/data` | PostgreSQL data directory |
|
||||||
|
=======
|
||||||
|
| `rosterchirp_db` | `/app/data` | SQLite database (`rosterchirp.db`), `help.md` |
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
| `rosterchirp_uploads` | `/app/uploads` | Avatars, logos, PWA icons, message images |
|
||||||
|
|
||||||
### Backup
|
### Backup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Backup database
|
# Backup database
|
||||||
|
<<<<<<< HEAD
|
||||||
|
docker compose exec db pg_dump -U rosterchirp rosterchirp | gzip > rosterchirp_db_$(date +%Y%m%d).sql.gz
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
gunzip -c rosterchirp_db_20240101.sql.gz | docker compose exec -T db psql -U rosterchirp rosterchirp
|
||||||
|
=======
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v jama_db:/data \
|
-v rosterchirp_db:/data \
|
||||||
-v $(pwd):/backup alpine \
|
-v $(pwd):/backup alpine \
|
||||||
tar czf /backup/jama_db_$(date +%Y%m%d).tar.gz -C /data .
|
tar czf /backup/rosterchirp_db_$(date +%Y%m%d).tar.gz -C /data .
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
|
|
||||||
# Backup uploads
|
# Backup uploads
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v jama_uploads:/data \
|
-v rosterchirp_uploads:/data \
|
||||||
-v $(pwd):/backup alpine \
|
-v $(pwd):/backup alpine \
|
||||||
tar czf /backup/jama_uploads_$(date +%Y%m%d).tar.gz -C /data .
|
tar czf /backup/rosterchirp_uploads_$(date +%Y%m%d).tar.gz -C /data .
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Upgrades & Rollbacks
|
## Upgrades & Rollbacks
|
||||||
|
|
||||||
|
Database migrations run automatically on startup. There is no manual migration step.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Upgrade
|
# Upgrade
|
||||||
./build.sh 1.1.0
|
<<<<<<< HEAD
|
||||||
# Set JAMA_VERSION=1.1.0 in .env
|
./build.sh 0.13.1
|
||||||
|
# Set ROSTERCHIRP_VERSION=0.13.1 in .env
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
# Rollback
|
# Rollback
|
||||||
# Set JAMA_VERSION=1.0.0 in .env
|
# Set ROSTERCHIRP_VERSION=0.12.x in .env
|
||||||
|
=======
|
||||||
|
./build.sh 1.1.0
|
||||||
|
# Set rosterchirp_VERSION=1.1.0 in .env
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
# Set rosterchirp_VERSION=1.0.0 in .env
|
||||||
|
>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -372,7 +563,7 @@ Data volumes are untouched in both cases.
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `icon-192.png` / `icon-512.png` | Standard icons — PC PWA shortcuts (`purpose: any`) |
|
| `icon-192.png` / `icon-512.png` | Standard icons — desktop PWA shortcuts (`purpose: any`) |
|
||||||
| `icon-192-maskable.png` / `icon-512-maskable.png` | Adaptive icons — Android home screen (`purpose: maskable`); logo at 75% scale on solid background |
|
| `icon-192-maskable.png` / `icon-512-maskable.png` | Adaptive icons — Android home screen (`purpose: maskable`); logo at 75% scale on solid background |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -403,10 +594,10 @@ cd backend && npm install && npm run dev
|
|||||||
cd frontend && npm install && npm run dev
|
cd frontend && npm install && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The Vite dev server proxies all `/api` and `/socket.io` requests to the backend automatically.
|
The Vite dev server proxies all `/api` and `/socket.io` requests to the backend automatically. You will need a running PostgreSQL instance and a `.env` file in the project root.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
Proprietary — all rights reserved.
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
// APP_TYPE validation — host mode requires HOST_DOMAIN and HOST_ADMIN_KEY.
|
// APP_TYPE validation — host mode requires APP_DOMAIN, HOST_SLUG, and HOST_ADMIN_KEY.
|
||||||
// If either is missing, fall back to selfhost and warn rather than silently
|
// If any are missing, fall back to selfhost and warn rather than silently
|
||||||
// exposing a broken or insecure host control plane.
|
// exposing a broken or insecure host control plane.
|
||||||
let APP_TYPE = (process.env.APP_TYPE || 'selfhost').toLowerCase().trim();
|
let APP_TYPE = (process.env.APP_TYPE || 'selfhost').toLowerCase().trim();
|
||||||
if (APP_TYPE === 'host') {
|
if (APP_TYPE === 'host') {
|
||||||
if (!process.env.HOST_DOMAIN || !process.env.HOST_ADMIN_KEY) {
|
if (!process.env.APP_DOMAIN || !process.env.HOST_SLUG || !process.env.HOST_ADMIN_KEY) {
|
||||||
console.warn('[DB] WARNING: APP_TYPE=host requires HOST_DOMAIN and HOST_ADMIN_KEY to be set.');
|
console.warn('[DB] WARNING: APP_TYPE=host requires APP_DOMAIN, HOST_SLUG, and HOST_ADMIN_KEY to be set.');
|
||||||
console.warn('[DB] WARNING: Falling back to APP_TYPE=selfhost for safety.');
|
console.warn('[DB] WARNING: Falling back to APP_TYPE=selfhost for safety.');
|
||||||
APP_TYPE = 'selfhost';
|
APP_TYPE = 'selfhost';
|
||||||
}
|
}
|
||||||
@@ -52,12 +52,17 @@ function resolveSchema(req) {
|
|||||||
if (APP_TYPE === 'selfhost') return 'public';
|
if (APP_TYPE === 'selfhost') return 'public';
|
||||||
|
|
||||||
const host = (req.headers.host || '').toLowerCase().split(':')[0];
|
const host = (req.headers.host || '').toLowerCase().split(':')[0];
|
||||||
const baseDomain = (process.env.HOST_DOMAIN || 'rosterchirp.com').toLowerCase();
|
const baseDomain = (process.env.APP_DOMAIN || 'rosterchirp.com').toLowerCase();
|
||||||
|
const hostSlug = (process.env.HOST_SLUG || 'host').toLowerCase();
|
||||||
|
const hostControlDomain = `${hostSlug}.${baseDomain}`;
|
||||||
|
|
||||||
// Internal requests (Docker health checks, localhost) → public schema
|
// Internal requests (Docker health checks, localhost) → public schema
|
||||||
if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return 'public';
|
if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return 'public';
|
||||||
|
|
||||||
// Subdomain: team1.rosterchirp.com → tenant_team1
|
// Host control panel subdomain: chathost.example.com → public schema
|
||||||
|
if (host === hostControlDomain) return 'public';
|
||||||
|
|
||||||
|
// Tenant subdomain: mychat.example.com → tenant_mychat
|
||||||
if (host.endsWith(`.${baseDomain}`)) {
|
if (host.endsWith(`.${baseDomain}`)) {
|
||||||
const slug = host.slice(0, -(baseDomain.length + 1));
|
const slug = host.slice(0, -(baseDomain.length + 1));
|
||||||
if (!slug || slug === 'www') throw new Error(`Invalid tenant slug: ${slug}`);
|
if (!slug || slug === 'www') throw new Error(`Invalid tenant slug: ${slug}`);
|
||||||
@@ -67,9 +72,6 @@ function resolveSchema(req) {
|
|||||||
// Custom domain lookup (populated from host admin DB)
|
// Custom domain lookup (populated from host admin DB)
|
||||||
if (tenantDomainCache.has(host)) return tenantDomainCache.get(host);
|
if (tenantDomainCache.has(host)) return tenantDomainCache.get(host);
|
||||||
|
|
||||||
// Base domain → public schema (host admin panel)
|
|
||||||
if (host === baseDomain || host === `www.${baseDomain}`) return 'public';
|
|
||||||
|
|
||||||
throw new Error(`Unknown tenant for host: ${host}`);
|
throw new Error(`Unknown tenant for host: ${host}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ router.post('/tenants', async (req, res) => {
|
|||||||
// 7. Reload domain cache
|
// 7. Reload domain cache
|
||||||
await reloadTenantCache();
|
await reloadTenantCache();
|
||||||
|
|
||||||
const baseDomain = process.env.HOST_DOMAIN || 'rosterchirp.com';
|
const baseDomain = process.env.APP_DOMAIN || 'rosterchirp.com';
|
||||||
const tenant = tr.rows[0];
|
const tenant = tr.rows[0];
|
||||||
tenant.url = `https://${slug}.${baseDomain}`;
|
tenant.url = `https://${slug}.${baseDomain}`;
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ router.get('/status', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const tenantCount = await queryOne('public', 'SELECT COUNT(*) AS count FROM tenants');
|
const tenantCount = await queryOne('public', 'SELECT COUNT(*) AS count FROM tenants');
|
||||||
const active = await queryOne('public', "SELECT COUNT(*) AS count FROM tenants WHERE status='active'");
|
const active = await queryOne('public', "SELECT COUNT(*) AS count FROM tenants WHERE status='active'");
|
||||||
const baseDomain = process.env.HOST_DOMAIN || 'rosterchirp.com';
|
const baseDomain = process.env.APP_DOMAIN || 'rosterchirp.com';
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
appType: process.env.APP_TYPE || 'selfhost',
|
appType: process.env.APP_TYPE || 'selfhost',
|
||||||
|
|||||||
@@ -39,14 +39,16 @@ router.get('/', async (req, res) => {
|
|||||||
if (admin) obj.admin_email = admin.email;
|
if (admin) obj.admin_email = admin.email;
|
||||||
obj.app_version = process.env.ROSTERCHIRP_VERSION || 'dev';
|
obj.app_version = process.env.ROSTERCHIRP_VERSION || 'dev';
|
||||||
obj.user_pass = process.env.USER_PASS || 'user@1234';
|
obj.user_pass = process.env.USER_PASS || 'user@1234';
|
||||||
// Tell the frontend whether this request came from the HOST_DOMAIN.
|
// Tell the frontend whether this request came from the host control panel subdomain.
|
||||||
// Used to show/hide the Control Panel menu item — only visible on the host's own domain.
|
// Used to show/hide the Control Panel menu item — only visible on the host's own subdomain.
|
||||||
const reqHost = (req.headers.host || '').toLowerCase().split(':')[0];
|
const reqHost = (req.headers.host || '').toLowerCase().split(':')[0];
|
||||||
const hostDomain = (process.env.HOST_DOMAIN || '').toLowerCase();
|
const appDomain = (process.env.APP_DOMAIN || '').toLowerCase();
|
||||||
|
const hostSlug = (process.env.HOST_SLUG || 'host').toLowerCase();
|
||||||
|
const hostControlDomain = appDomain ? `${hostSlug}.${appDomain}` : '';
|
||||||
obj.is_host_domain = (
|
obj.is_host_domain = (
|
||||||
process.env.APP_TYPE === 'host' &&
|
process.env.APP_TYPE === 'host' &&
|
||||||
!!hostDomain &&
|
!!hostControlDomain &&
|
||||||
(reqHost === hostDomain || reqHost === `www.${hostDomain}` || reqHost === 'localhost')
|
(reqHost === hostControlDomain || reqHost === 'localhost')
|
||||||
) ? 'true' : 'false';
|
) ? 'true' : 'false';
|
||||||
res.json({ settings: obj });
|
res.json({ settings: obj });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
services:
|
|
||||||
rosterchirp:
|
|
||||||
image: rosterchirp:${RC_VERSION:-latest}
|
|
||||||
container_name: ${PROJECT_NAME:-rosterchirp}
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${PORT:-3000}:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- TZ=${TZ:-UTC}
|
|
||||||
- APP_TYPE=${APP_TYPE:-selfhost}
|
|
||||||
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
|
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@rosterchirp.local}
|
|
||||||
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
|
|
||||||
- ADMPW_RESET=${ADMPW_RESET:-false}
|
|
||||||
- JWT_SECRET=${JWT_SECRET:-changeme_super_secret_jwt_key_2024}
|
|
||||||
- APP_NAME=${APP_NAME:-rosterchirp}
|
|
||||||
- DEFCHAT_NAME=${DEFCHAT_NAME:-General Chat}
|
|
||||||
- DB_HOST=db
|
|
||||||
- DB_PORT=5432
|
|
||||||
- DB_NAME=${DB_NAME:-rosterchirp}
|
|
||||||
- DB_USER=${DB_USER:-rosterchirp}
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
|
||||||
- HOST_DOMAIN=${HOST_DOMAIN:-}
|
|
||||||
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:-}
|
|
||||||
- FIREBASE_API_KEY=${FIREBASE_API_KEY:-}
|
|
||||||
- FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-}
|
|
||||||
- FIREBASE_MESSAGING_SENDER_ID=${FIREBASE_MESSAGING_SENDER_ID:-}
|
|
||||||
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
|
|
||||||
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
|
|
||||||
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
|
|
||||||
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:webpush@rosterchirp.com}
|
|
||||||
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
|
|
||||||
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
|
|
||||||
volumes:
|
|
||||||
- jama_uploads:/app/uploads
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: ${PROJECT_NAME:-rosterchirp}_db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=${DB_NAME:-rosterchirp}
|
|
||||||
- POSTGRES_USER=${DB_USER:-rosterchirp}
|
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
|
||||||
volumes:
|
|
||||||
- jama_db:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-rosterchirp} -d ${DB_NAME:-rosterchirp}"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
jama_db:
|
|
||||||
driver: local
|
|
||||||
jama_uploads:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
#
|
#
|
||||||
# Required .env additions for host mode:
|
# Required .env additions for host mode:
|
||||||
# APP_TYPE=host
|
# APP_TYPE=host
|
||||||
# HOST_DOMAIN=rosterchirp.com
|
# APP_DOMAIN=example.com
|
||||||
|
# HOST_SLUG=chathost
|
||||||
# HOST_ADMIN_KEY=your_secret_host_admin_key
|
# HOST_ADMIN_KEY=your_secret_host_admin_key
|
||||||
# CF_API_TOKEN=your_cloudflare_dns_api_token (or equivalent for your DNS provider)
|
# CF_API_TOKEN=your_cloudflare_dns_api_token (or equivalent for your DNS provider)
|
||||||
|
|
||||||
@@ -36,7 +37,8 @@ services:
|
|||||||
- DB_NAME=${DB_NAME:-rosterchirp}
|
- DB_NAME=${DB_NAME:-rosterchirp}
|
||||||
- DB_USER=${DB_USER:-rosterchirp}
|
- DB_USER=${DB_USER:-rosterchirp}
|
||||||
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
||||||
- HOST_DOMAIN=${HOST_DOMAIN:?HOST_DOMAIN is required in host mode}
|
- APP_DOMAIN=${APP_DOMAIN:?APP_DOMAIN is required in host mode}
|
||||||
|
- HOST_SLUG=${HOST_SLUG:?HOST_SLUG is required in host mode}
|
||||||
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:?HOST_ADMIN_KEY is required in host mode}
|
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:?HOST_ADMIN_KEY is required in host mode}
|
||||||
volumes:
|
volumes:
|
||||||
- rosterchirp_uploads:/app/uploads
|
- rosterchirp_uploads:/app/uploads
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ services:
|
|||||||
- DB_NAME=${DB_NAME:-rosterchirp}
|
- DB_NAME=${DB_NAME:-rosterchirp}
|
||||||
- DB_USER=${DB_USER:-rosterchirp}
|
- DB_USER=${DB_USER:-rosterchirp}
|
||||||
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
||||||
- HOST_DOMAIN=${HOST_DOMAIN:-}
|
- APP_DOMAIN=${APP_DOMAIN:-}
|
||||||
|
- HOST_SLUG=${HOST_SLUG:-}
|
||||||
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:-}
|
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:-}
|
||||||
- FIREBASE_API_KEY=${FIREBASE_API_KEY:-}
|
- FIREBASE_API_KEY=${FIREBASE_API_KEY:-}
|
||||||
- FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-}
|
- FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "service_account",
|
|
||||||
"project_id": "fcmtest-push",
|
|
||||||
"private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad",
|
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\ns+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLwTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n",
|
|
||||||
"client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com",
|
|
||||||
"client_id": "103917424542871804597",
|
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
|
||||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
||||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com",
|
|
||||||
"universe_domain": "googleapis.com"
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,9 @@
|
|||||||
// ── Firebase Messaging (background push for Android PWA) ──────────────────────
|
// ── Service Worker — RosterChirp ───────────────────────────────────────────────
|
||||||
// Config must be hardcoded here — the SW is woken by push events before any
|
// Push notifications are handled via the standard W3C Push API (`push` event).
|
||||||
// async fetch can resolve, so Firebase must be initialised synchronously.
|
// The Firebase SDK is not initialised here — FCM delivers the payload via the
|
||||||
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js');
|
// standard push event and event.data.json() is sufficient to read it.
|
||||||
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js');
|
// Firebase SDK initialisation (for getToken) happens in the main thread (Chat.jsx),
|
||||||
|
// where the config is fetched at runtime from /api/push/firebase-config.
|
||||||
const FIREBASE_CONFIG = {
|
|
||||||
apiKey: "AIzaSyDx191unzXFT4WA1OvkdbrIY_c57kgruAU",
|
|
||||||
authDomain: "rosterchirp-push.firebaseapp.com",
|
|
||||||
projectId: "rosterchirp-push",
|
|
||||||
storageBucket: "rosterchirp-push.firebasestorage.app",
|
|
||||||
messagingSenderId: "126479377334",
|
|
||||||
appId: "1:126479377334:web:280abdd135cf7e0c50d717"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialise Firebase synchronously so the push listener is ready immediately.
|
|
||||||
// Skip on iOS — iOS PWAs use standard W3C WebPush (VAPID), not FCM. Initialising
|
|
||||||
// firebase.messaging() on iOS registers a second internal push listener alongside
|
|
||||||
// the custom one below, causing every notification to appear twice.
|
|
||||||
const isIOS = /iPhone|iPad|iPod/.test(self.navigator?.userAgent || '');
|
|
||||||
let messaging = null;
|
|
||||||
if (!isIOS && FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') {
|
|
||||||
firebase.initializeApp(FIREBASE_CONFIG);
|
|
||||||
messaging = firebase.messaging();
|
|
||||||
console.log('[SW] Firebase initialised');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Cache ─────────────────────────────────────────────────────────────────────
|
// ── Cache ─────────────────────────────────────────────────────────────────────
|
||||||
const CACHE_NAME = 'rosterchirp-v1';
|
const CACHE_NAME = 'rosterchirp-v1';
|
||||||
@@ -85,7 +65,7 @@ function showRosterChirpNotification(data) {
|
|||||||
// directly (fast, reliable) rather than delegating to the Firebase SDK's
|
// directly (fast, reliable) rather than delegating to the Firebase SDK's
|
||||||
// internal push listener, which can be killed before it finishes on Android.
|
// internal push listener, which can be killed before it finishes on Android.
|
||||||
self.addEventListener('push', (event) => {
|
self.addEventListener('push', (event) => {
|
||||||
console.log('[SW] Push received, hasData:', !!event.data, 'messaging:', !!messaging);
|
console.log('[SW] Push received, hasData:', !!event.data);
|
||||||
|
|
||||||
event.waitUntil((async () => {
|
event.waitUntil((async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user