bug fixes
This commit is contained in:
48
.env.fcmtest-push
Normal file
48
.env.fcmtest-push
Normal file
@@ -0,0 +1,48 @@
|
||||
#** Required
|
||||
DB_PASSWORD=C@nuck2024
|
||||
JWT_SECRET=changemesupersecretjwtkey
|
||||
|
||||
#** App identity
|
||||
PROJECT_NAME=rosterchirp
|
||||
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=rosterchirphost.stretchy.ca
|
||||
HOST_ADMIN_KEY=VBGFHEANTTGRDDWAASJKH
|
||||
|
||||
#** Optional
|
||||
PORT=3144
|
||||
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
|
||||
@@ -199,56 +199,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
if (to) { sql += ` AND start_at <= $${pi++}`; params.push(to); }
|
||||
sql += ' ORDER BY start_at ASC';
|
||||
const rawEvents = await query(req.schema, sql, params);
|
||||
|
||||
// Separate master events and exception instances
|
||||
const masterEvents = [];
|
||||
const exceptionInstances = [];
|
||||
for (const e of rawEvents) {
|
||||
if (e.recurring_master_id) {
|
||||
exceptionInstances.push(e);
|
||||
} else {
|
||||
masterEvents.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map of exception dates by master event
|
||||
const exceptionDatesByMaster = new Map();
|
||||
for (const exc of exceptionInstances) {
|
||||
if (!exceptionDatesByMaster.has(exc.recurring_master_id)) {
|
||||
exceptionDatesByMaster.set(exc.recurring_master_id, new Set());
|
||||
}
|
||||
const dateStr = new Date(exc.original_start_at || exc.start_at).toISOString().split('T')[0];
|
||||
exceptionDatesByMaster.get(exc.recurring_master_id).add(dateStr);
|
||||
}
|
||||
|
||||
const events = [];
|
||||
for (const e of masterEvents) {
|
||||
// Skip master events if all their occurrences in range are replaced by exceptions
|
||||
if (e.recurrence_rule && exceptionDatesByMaster.has(e.id)) {
|
||||
const exceptions = exceptionDatesByMaster.get(e.id);
|
||||
const rule = e.recurrence_rule;
|
||||
|
||||
// Check if this is a simple case where all occurrences are replaced
|
||||
// For now, we'll include the master and let frontend handle filtering
|
||||
// This is safer to avoid missing edge cases
|
||||
}
|
||||
|
||||
for (const e of rawEvents) {
|
||||
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
|
||||
await enrichEvent(req.schema, e);
|
||||
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
|
||||
e.my_response = mine?.response || null;
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
// Add exception instances (they're standalone events)
|
||||
for (const e of exceptionInstances) {
|
||||
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
|
||||
await enrichEvent(req.schema, e);
|
||||
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
|
||||
e.my_response = mine?.response || null;
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
res.json({ events });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
66
docker-compose-fcmtest-push.yaml
Normal file
66
docker-compose-fcmtest-push.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
services:
|
||||
rosterchirp:
|
||||
image: rosterchirp-dev:${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:push@rosterchirp.com}
|
||||
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
|
||||
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
|
||||
volumes:
|
||||
- rosterchirp_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:
|
||||
- rosterchirp_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:
|
||||
rosterchirp_db:
|
||||
driver: local
|
||||
rosterchirp_uploads:
|
||||
driver: local
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
rosterchirp:
|
||||
image: rosterchirp:${ROSTERCHIRP_VERSION:-latest}
|
||||
image: rosterchirp:${RC_VERSION:-latest}
|
||||
container_name: ${PROJECT_NAME:-rosterchirp}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -29,6 +29,9 @@ services:
|
||||
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
|
||||
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
|
||||
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
|
||||
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:push@rosterchirp.com}
|
||||
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
|
||||
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
|
||||
volumes:
|
||||
- rosterchirp_uploads:/app/uploads
|
||||
depends_on:
|
||||
|
||||
13
fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json
Normal file
13
fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1125,57 +1125,14 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) {
|
||||
// Expand all recurring events in a list within a date range
|
||||
function expandEvents(events, rangeStart, rangeEnd) {
|
||||
const result = [];
|
||||
|
||||
// First, collect all exception instances and their dates by master event
|
||||
const exceptionDatesByMaster = new Map();
|
||||
const masterEvents = [];
|
||||
const standaloneEvents = [];
|
||||
|
||||
for (const ev of events) {
|
||||
if (ev.recurring_master_id) {
|
||||
// This is an exception instance
|
||||
if (!exceptionDatesByMaster.has(ev.recurring_master_id)) {
|
||||
exceptionDatesByMaster.set(ev.recurring_master_id, new Set());
|
||||
}
|
||||
const dateStr = new Date(ev.original_start_at || ev.start_at).toISOString().split('T')[0];
|
||||
exceptionDatesByMaster.get(ev.recurring_master_id).add(dateStr);
|
||||
standaloneEvents.push(ev); // Exception instances are standalone events
|
||||
} else if (ev.recurrence_rule?.freq) {
|
||||
masterEvents.push(ev);
|
||||
} else {
|
||||
standaloneEvents.push(ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Expand master events, skipping dates that have exception instances
|
||||
for (const ev of masterEvents) {
|
||||
const exceptions = exceptionDatesByMaster.get(ev.id);
|
||||
if (exceptions) {
|
||||
// Merge the rule's exceptions with the instance exceptions
|
||||
const rule = ev.recurrence_rule || {};
|
||||
const ruleExceptions = new Set(rule.exceptions || []);
|
||||
for (const date of exceptions) {
|
||||
ruleExceptions.add(date);
|
||||
}
|
||||
// Create a copy of the event with merged exceptions
|
||||
const evWithMergedExceptions = {
|
||||
...ev,
|
||||
recurrence_rule: {
|
||||
...rule,
|
||||
exceptions: Array.from(ruleExceptions)
|
||||
}
|
||||
};
|
||||
const expanded = expandRecurringEvent(evWithMergedExceptions, rangeStart, rangeEnd);
|
||||
result.push(...expanded);
|
||||
} else {
|
||||
if (ev.recurrence_rule?.freq) {
|
||||
const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd);
|
||||
result.push(...expanded);
|
||||
} else {
|
||||
result.push(ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all standalone events (non-recurring + exception instances)
|
||||
result.push(...standaloneEvents);
|
||||
|
||||
// Sort by start_at
|
||||
result.sort((a,b) => new Date(a.start_at) - new Date(b.start_at));
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user