From b527e247051b102f40f6ad26b1bf0d29f083dddf Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Fri, 10 Apr 2026 13:18:59 -0400 Subject: [PATCH] update --- .env.example | 21 ++- .gitignore | 36 +++++ Caddyfile.example | 23 +--- README.md | 232 ++++++++++++++++++++++++++++++--- backend/src/models/db.js | 20 +-- backend/src/routes/host.js | 4 +- backend/src/routes/settings.js | 12 +- docker-compose.host.yaml | 6 +- docker-compose.yaml | 3 +- frontend/public/sw.js | 34 +---- 10 files changed, 302 insertions(+), 89 deletions(-) create mode 100644 .gitignore diff --git a/.env.example b/.env.example index a20bb72..4530d30 100644 --- a/.env.example +++ b/.env.example @@ -22,22 +22,31 @@ DB_USER=rosterchirp APP_TYPE=selfhost # ── 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 # ── Optional ────────────────────────────────────────────────────────────────── PORT=3000 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. -# 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_PROJECT_ID= # FIREBASE_MESSAGING_SENDER_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= -# 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: +# -- 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 (include curlybracket to curlybracket): # 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= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0aaaed6 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Caddyfile.example b/Caddyfile.example index 3aa1824..30046c3 100644 --- a/Caddyfile.example +++ b/Caddyfile.example @@ -12,18 +12,17 @@ # CF_API_TOKEN=your_cloudflare_token (or equivalent) # # 3. Add a wildcard DNS record in your DNS provider: -# *.rosterchirp.com → your server IP -# rosterchirp.com → your server IP +# *.example.com → your server IP # # Usage: # Copy this file to /etc/caddy/Caddyfile (or wherever Caddy reads it) # Reload: caddy reload # ── Wildcard subdomain ──────────────────────────────────────────────────────── -# Handles team1.rosterchirp.com, teamB.rosterchirp.com, etc. -# Replace rosterchirp.com with your actual HOST_DOMAIN. +# Handles mychat.example.com, teamB.example.com, chathost.example.com, etc. +# Replace example.com with your actual APP_DOMAIN. -*.rosterchirp.com { +*.example.com { tls { 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 ───────────────────────────────────────────────────── # 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. # Caddy will automatically obtain and renew the SSL cert. @@ -80,7 +69,7 @@ rosterchirp.com { # } # } # -# *.rosterchirp.com, rosterchirp.com { +# *.example.com { # tls { on_demand } # reverse_proxy localhost:3000 # } diff --git a/README.md b/README.md index d809b21..872f4f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ +<<<<<<< HEAD +# RosterChirp + +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 --- @@ -28,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 - **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 +- **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 - **Authentication** — Email/password login with optional Remember Me (30-day session) @@ -35,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 - **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 +- **Online presence** — Real-time online/offline status tracked per user +- **Last seen** — Users' last online timestamp updated on disconnect ### Notifications - **In-app notifications** — Mention alerts with toast notifications - **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 - **User Manager** — Create, suspend, activate, delete users; reset passwords; change roles - **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 - **Version display** — Current app version shown in the Settings panel - **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 - **Getting Started modal** — Appears automatically on first login; users can dismiss permanently with "Do not show again" @@ -66,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 | Layer | Technology | |---|---| | Backend | Node.js, Express, Socket.io | -| Database | SQLite (better-sqlite3) | +| Database | PostgreSQL 16 (via `pg`) | | Frontend | React 18, Vite | -| Markdown rendering | marked | -| Emoji picker | emoji-mart | +| Push notifications | Firebase Cloud Messaging (FCM) | | Image processing | sharp | -| Push notifications | web-push (VAPID) | -| Containerization | Docker, Docker Compose | +| Containerization | Docker, Docker Compose v2 | | Reverse proxy / SSL | Caddy (recommended) | --- @@ -85,8 +137,9 @@ A modern, self-hosted team messaging Progressive Web App (PWA) built for small t ## Requirements - **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) +- (Optional) A Firebase project for push notifications --- @@ -101,7 +154,7 @@ All builds use `build.sh`. No host Node.js installation is required. ./build.sh # Build and tag as a specific version -./build.sh 1.0.0 +./build.sh 0.13.1 ``` --- @@ -111,14 +164,18 @@ All builds use `build.sh`. No host Node.js installation is required. ### 1. Clone the repository ```bash +<<<<<<< HEAD +git clone https://your-git/youruser/rosterchirp.git +======= git clone https://your-gitea/youruser/rosterchirp.git +>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28 cd rosterchirp ``` ### 2. Build the Docker image ```bash -./build.sh 1.0.0 +./build.sh 0.13.1 ``` ### 3. Configure environment @@ -128,9 +185,9 @@ cp .env.example .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 docker compose up -d @@ -145,7 +202,11 @@ Open `http://your-server:3000`, log in with your `ADMIN_EMAIL` and `ADMIN_PASS`, ## HTTPS & SSL +<<<<<<< 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 @@ -158,27 +219,62 @@ chat.yourdomain.com { ### docker-compose.yaml (with Caddy) ```yaml -version: '3.8' services: rosterchirp: +<<<<<<< HEAD + image: rosterchirp:${ROSTERCHIRP_VERSION:-latest} +======= image: rosterchirp:${rosterchirp_VERSION:-latest} +>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28 container_name: rosterchirp restart: unless-stopped expose: - "3000" environment: - NODE_ENV=production + - APP_TYPE=${APP_TYPE:-selfhost} - ADMIN_NAME=${ADMIN_NAME:-Admin User} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@rosterchirp.local} - ADMIN_PASS=${ADMIN_PASS:-Admin@1234} - USER_PASS=${USER_PASS:-user@1234} - ADMPW_RESET=${ADMPW_RESET:-false} - JWT_SECRET=${JWT_SECRET:-changeme} +<<<<<<< HEAD + - 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: + - rosterchirp_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: image: caddy:alpine @@ -208,24 +304,57 @@ volumes: | Variable | Default | Description | |---|---|---| +<<<<<<< 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`) | | `ADMIN_NAME` | `Admin User` | Display name of 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 | | `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.** | +<<<<<<< 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) | | `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 | +>>>>>>> 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` ```env +<<<<<<< HEAD +ROSTERCHIRP_VERSION=0.13.1 +APP_TYPE=selfhost +======= rosterchirp_VERSION=1.0.0 +>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28 TZ=America/Toronto ADMIN_NAME=Your Name @@ -237,9 +366,17 @@ ADMPW_RESET=false JWT_SECRET=replace-this-with-a-long-random-string-at-least-32-chars +<<<<<<< HEAD +APP_NAME=RosterChirp +======= PORT=3000 APP_NAME=rosterchirp +>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28 DEFCHAT_NAME=General Chat + +DB_NAME=rosterchirp +DB_USER=rosterchirp +DB_PASSWORD=a-strong-db-password ``` --- @@ -265,7 +402,7 @@ Accessible from the bottom-left menu (admin only). | Reset password | User is forced to set a new password on next login | | Suspend | Blocks login; messages are preserved | | 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 | ### CSV Import Format @@ -291,6 +428,7 @@ Jane Smith,jane@example.com,,admin | Rename | Admin only | Owner only | ❌ Not allowed | | Read-only mode | ✅ Optional | ❌ N/A | ❌ N/A | | Duplicate prevention | N/A | ✅ Redirects to existing | ✅ Redirects to existing | +| Managed (Group Manager) | ❌ | ✅ Optional | ❌ | ### @Mention Scoping @@ -312,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 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 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**. @@ -329,17 +499,28 @@ Users can access the guide at any time via **User menu → Help**. | Volume | Container path | Contents | |---|---|---| +<<<<<<< HEAD +| `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 ```bash # 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 \ -v rosterchirp_db:/data \ -v $(pwd):/backup alpine \ tar czf /backup/rosterchirp_db_$(date +%Y%m%d).tar.gz -C /data . +>>>>>>> 1af039ab0a72560aace9b284d541f5201c920e28 # Backup uploads docker run --rm \ @@ -352,14 +533,25 @@ docker run --rm \ ## Upgrades & Rollbacks +Database migrations run automatically on startup. There is no manual migration step. + ```bash # Upgrade +<<<<<<< HEAD +./build.sh 0.13.1 +# Set ROSTERCHIRP_VERSION=0.13.1 in .env +docker compose up -d + +# Rollback +# 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 ``` @@ -371,7 +563,7 @@ Data volumes are untouched in both cases. | 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 | --- @@ -402,10 +594,10 @@ cd backend && 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 -MIT +Proprietary — all rights reserved. diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 0820776..a355ae4 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -14,13 +14,13 @@ const fs = require('fs'); const path = require('path'); const bcrypt = require('bcryptjs'); -// APP_TYPE validation — host mode requires HOST_DOMAIN and HOST_ADMIN_KEY. -// If either is missing, fall back to selfhost and warn rather than silently +// APP_TYPE validation — host mode requires APP_DOMAIN, HOST_SLUG, and HOST_ADMIN_KEY. +// If any are missing, fall back to selfhost and warn rather than silently // exposing a broken or insecure host control plane. let APP_TYPE = (process.env.APP_TYPE || 'selfhost').toLowerCase().trim(); if (APP_TYPE === 'host') { - if (!process.env.HOST_DOMAIN || !process.env.HOST_ADMIN_KEY) { - console.warn('[DB] WARNING: APP_TYPE=host requires HOST_DOMAIN and HOST_ADMIN_KEY to be set.'); + if (!process.env.APP_DOMAIN || !process.env.HOST_SLUG || !process.env.HOST_ADMIN_KEY) { + 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.'); APP_TYPE = 'selfhost'; } @@ -52,12 +52,17 @@ function resolveSchema(req) { if (APP_TYPE === 'selfhost') return 'public'; 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 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}`)) { const slug = host.slice(0, -(baseDomain.length + 1)); 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) 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}`); } diff --git a/backend/src/routes/host.js b/backend/src/routes/host.js index a21fc72..be4f364 100644 --- a/backend/src/routes/host.js +++ b/backend/src/routes/host.js @@ -162,7 +162,7 @@ router.post('/tenants', async (req, res) => { // 7. Reload domain cache await reloadTenantCache(); - const baseDomain = process.env.HOST_DOMAIN || 'rosterchirp.com'; + const baseDomain = process.env.APP_DOMAIN || 'rosterchirp.com'; const tenant = tr.rows[0]; tenant.url = `https://${slug}.${baseDomain}`; @@ -320,7 +320,7 @@ router.get('/status', async (req, res) => { try { 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 baseDomain = process.env.HOST_DOMAIN || 'rosterchirp.com'; + const baseDomain = process.env.APP_DOMAIN || 'rosterchirp.com'; res.json({ ok: true, appType: process.env.APP_TYPE || 'selfhost', diff --git a/backend/src/routes/settings.js b/backend/src/routes/settings.js index 68085bf..fbfc5bc 100644 --- a/backend/src/routes/settings.js +++ b/backend/src/routes/settings.js @@ -39,14 +39,16 @@ router.get('/', async (req, res) => { if (admin) obj.admin_email = admin.email; obj.app_version = process.env.ROSTERCHIRP_VERSION || 'dev'; obj.user_pass = process.env.USER_PASS || 'user@1234'; - // Tell the frontend whether this request came from the HOST_DOMAIN. - // Used to show/hide the Control Panel menu item — only visible on the host's own 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 subdomain. 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 = ( process.env.APP_TYPE === 'host' && - !!hostDomain && - (reqHost === hostDomain || reqHost === `www.${hostDomain}` || reqHost === 'localhost') + !!hostControlDomain && + (reqHost === hostControlDomain || reqHost === 'localhost') ) ? 'true' : 'false'; res.json({ settings: obj }); } catch (e) { res.status(500).json({ error: e.message }); } diff --git a/docker-compose.host.yaml b/docker-compose.host.yaml index 48be51b..6db2def 100644 --- a/docker-compose.host.yaml +++ b/docker-compose.host.yaml @@ -8,7 +8,8 @@ # # Required .env additions for host mode: # APP_TYPE=host -# HOST_DOMAIN=rosterchirp.com +# APP_DOMAIN=example.com +# HOST_SLUG=chathost # HOST_ADMIN_KEY=your_secret_host_admin_key # 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_USER=${DB_USER:-rosterchirp} - 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} volumes: - rosterchirp_uploads:/app/uploads diff --git a/docker-compose.yaml b/docker-compose.yaml index a11ce05..9248556 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,7 +21,8 @@ services: - DB_NAME=${DB_NAME:-rosterchirp} - DB_USER=${DB_USER:-rosterchirp} - 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:-} - FIREBASE_API_KEY=${FIREBASE_API_KEY:-} - FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-} diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 00cb9b0..2cfbae5 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,29 +1,9 @@ -// ── Firebase Messaging (background push for Android PWA) ────────────────────── -// Config must be hardcoded here — the SW is woken by push events before any -// async fetch can resolve, so Firebase must be initialised synchronously. -importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js'); -importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js'); - -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'); -} +// ── Service Worker — RosterChirp ─────────────────────────────────────────────── +// Push notifications are handled via the standard W3C Push API (`push` event). +// The Firebase SDK is not initialised here — FCM delivers the payload via the +// standard push event and event.data.json() is sufficient to read it. +// Firebase SDK initialisation (for getToken) happens in the main thread (Chat.jsx), +// where the config is fetched at runtime from /api/push/firebase-config. // ── Cache ───────────────────────────────────────────────────────────────────── const CACHE_NAME = 'rosterchirp-v1'; @@ -85,7 +65,7 @@ function showRosterChirpNotification(data) { // directly (fast, reliable) rather than delegating to the Firebase SDK's // internal push listener, which can be killed before it finishes on Android. 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 () => { try {