Notifications
CI / ci (push) Canceled after 0s

Added a cron job processor so emails and notifications can get processed.

Added super admin settings with a gui to edit.
This commit is contained in:
Stefan Lazic committed 2026-09-11 17:46:28 +01:00
1 parent cbeccc6c1f
commit 1a76d1cacb
17 files changed
+645 -48

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
#!/bin/sh
# Calls one of the app's CRON_SECRET-gated endpoints (see
# src/app/api/cron/process-notifications/route.ts and
# src/app/api/cron/sync-google-reviews/route.ts) for every business
# currently provisioned under STACKS_DIR — so a single crontab entry on
# the host covers every business automatically, including ones added
# later, without a manual crontab line per business.
#
# Without something calling process-notifications on a schedule, queued
# notifications (staff invitation emails, appointment reminders, loyalty
# reward emails, etc.) are created in the database as PENDING and never
# actually sent — see that route's own header for why this is a required
# piece of infrastructure, not optional. This script is the reference
# implementation for wiring that up; see the root README's "Notification
# delivery" section for the crontab lines that call it.
#
# Usage:
# process-cron.sh notifications # recommended every 1-2 minutes
# process-cron.sh reviews # recommended once every 24 hours
#
# STACKS_DIR defaults to /opt/nextchair/stacks — override via env var if
# this repo is cloned somewhere else (must match every other STACKS_DIR
# use across this deployment — see .env.infra.example's own note on why).
set -eu
if [ $# -lt 1 ]; then
echo "Usage: $0 notifications|reviews" >&2
exit 1
fi
ENDPOINT="$1"
STACKS_DIR="${STACKS_DIR:-/opt/nextchair/stacks}"
case "$ENDPOINT" in
notifications) PATH_SUFFIX="process-notifications" ;;
reviews) PATH_SUFFIX="sync-google-reviews" ;;
*)
echo "Unknown endpoint '$ENDPOINT' — expected 'notifications' or 'reviews'." >&2
exit 1
;;
esac
if [ ! -d "$STACKS_DIR" ]; then
echo "STACKS_DIR '$STACKS_DIR' does not exist." >&2
exit 1
fi
for env_file in "$STACKS_DIR"/*/.env; do
[ -f "$env_file" ] || continue
domain=$(grep -E '^DOMAIN=' "$env_file" | head -n1 | cut -d= -f2- | tr -d '"')
cron_secret=$(grep -E '^CRON_SECRET=' "$env_file" | head -n1 | cut -d= -f2- | tr -d '"')
slug=$(basename "$(dirname "$env_file")")
if [ -z "$domain" ] || [ -z "$cron_secret" ]; then
echo "[$slug] skipped — no DOMAIN/CRON_SECRET in .env (not fully provisioned yet)." >&2
continue
fi
status=$(curl -sS -o /tmp/process-cron-response.$$ -w '%{http_code}' -X POST \
"https://${domain}/api/cron/${PATH_SUFFIX}" \
-H "Authorization: Bearer ${cron_secret}" || echo "curl_failed")
if [ "$status" = "200" ]; then
echo "[$slug] $PATH_SUFFIX ok"
else
echo "[$slug] $PATH_SUFFIX FAILED (status=$status): $(cat /tmp/process-cron-response.$$ 2>/dev/null)" >&2
fi
rm -f /tmp/process-cron-response.$$
done
+63
View File
@@ -0,0 +1,63 @@
#!/bin/sh
# Sets SUPER_ADMIN_EMAIL/FIRST_NAME/LAST_NAME in every business's .env
# under STACKS_DIR and recreates each container so
# scripts/ensure-super-admin.ts (run on every container start via
# docker/entrypoint.sh) picks it up immediately. No password to set —
# sign-in is OTP-only; see ensure-super-admin.ts's own header for why.
#
# Only touches these three keys in each .env — every other secret/setting
# already in the file (Google/email/push credentials, etc.) is left
# byte-for-byte alone. Safe to re-run any time you want to change who the
# Super Administrator is, or after adding a new business.
#
# Usage:
# set-super-admin.sh <email> <first-name> <last-name>
#
# STACKS_DIR defaults to /opt/nextchair/stacks — override via env var if
# this repo is cloned somewhere else.
set -eu
if [ $# -lt 3 ]; then
echo "Usage: $0 <email> <first-name> <last-name>" >&2
exit 1
fi
EMAIL="$1"
FIRST_NAME="$2"
LAST_NAME="$3"
STACKS_DIR="${STACKS_DIR:-/opt/nextchair/stacks}"
if [ ! -d "$STACKS_DIR" ]; then
echo "STACKS_DIR '$STACKS_DIR' does not exist." >&2
exit 1
fi
for env_file in "$STACKS_DIR"/*/.env; do
[ -f "$env_file" ] || continue
dir=$(dirname "$env_file")
slug=$(basename "$dir")
tmp="${env_file}.tmp.$$"
awk -v email="$EMAIL" -v first="$FIRST_NAME" -v last="$LAST_NAME" '
BEGIN { seen_email=0; seen_first=0; seen_last=0 }
/^SUPER_ADMIN_EMAIL=/ { print "SUPER_ADMIN_EMAIL=\"" email "\""; seen_email=1; next }
/^SUPER_ADMIN_FIRST_NAME=/ { print "SUPER_ADMIN_FIRST_NAME=\"" first "\""; seen_first=1; next }
/^SUPER_ADMIN_LAST_NAME=/ { print "SUPER_ADMIN_LAST_NAME=\"" last "\""; seen_last=1; next }
/^SUPER_ADMIN_PASSWORD=/ { next }
{ print }
END {
if (!seen_email) print "SUPER_ADMIN_EMAIL=\"" email "\""
if (!seen_first) print "SUPER_ADMIN_FIRST_NAME=\"" first "\""
if (!seen_last) print "SUPER_ADMIN_LAST_NAME=\"" last "\""
}
' "$env_file" > "$tmp" && mv "$tmp" "$env_file"
echo "[$slug] .env updated — recreating container..."
if (cd "$dir" && docker compose up -d --force-recreate) > /tmp/set-super-admin-$$.log 2>&1; then
echo "[$slug] ok"
else
echo "[$slug] FAILED to recreate container:" >&2
cat /tmp/set-super-admin-$$.log >&2
fi
rm -f /tmp/set-super-admin-$$.log
done