Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ jobs:
run: chmod +x ./bin/fetch_private_deps && ./bin/fetch_private_deps

- name: Install backend deps
run: npm install --prefix backend
run: npm ci --prefix backend

- name: Install frontend deps
run: npm install --prefix frontend
run: npm ci --prefix frontend

- name: Build frontend
run: CI=false npm --prefix frontend run build
Expand Down Expand Up @@ -74,10 +74,10 @@ jobs:
run: chmod +x ./bin/fetch_private_deps && ./bin/fetch_private_deps

- name: Install backend deps
run: npm install --prefix backend
run: npm ci --prefix backend

- name: Install frontend deps
run: npm install --prefix frontend
run: npm ci --prefix frontend

- name: Run backend unit tests
run: npm --prefix backend run test:unit
Expand Down Expand Up @@ -121,10 +121,10 @@ jobs:
run: chmod +x ./bin/fetch_private_deps && ./bin/fetch_private_deps

- name: Install backend deps
run: npm install --prefix backend
run: npm ci --prefix backend

- name: Install frontend deps
run: npm install --prefix frontend
run: npm ci --prefix frontend

- name: Run backend comprehensive test suite with coverage
run: npm --prefix backend run test:coverage -- --json --outputFile=coverage/test-results.json
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ scripts/
dump/

# Events backend cloned at build time (replaces git submodule)
backend/events
backend/events

# backend coverage
backend/coverage/
2 changes: 2 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ function createApp() {
'/',
'/landing',
'/mobile',
'/invite',
'/.well-known',
'/contact',
'/support',
'/privacy-policy',
Expand Down
69 changes: 57 additions & 12 deletions backend/migrations/seedPivotFeedEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,24 @@ const DEMO_EVENTS = [
tags: ['wellness'],
registrationCount: 21,
},
{
slug: 'pivot-seed-indie-film',
name: 'Indie Film Night — The Last Garden',
description: 'Limited run at Nitehawk. Grab tickets for your showtime.',
location: 'Nitehawk Cinema, Williamsburg, Brooklyn, NY',
dayOffset: 2,
startHour: 18,
durationHours: 6,
externalLink: 'https://partiful.com/e/pivot-seed-indie-film',
host: { name: 'Nitehawk Cinema' },
tags: ['movies', 'art-and-culture'],
registrationCount: 56,
timeSlots: [
{ id: '6pm', label: '6:00 PM', startHour: 18, durationHours: 2.25 },
{ id: '8-30pm', label: '8:30 PM', startHour: 20.5, durationHours: 2.25 },
{ id: '11pm', label: '11:00 PM', startHour: 23, durationHours: 2.25 },
],
},
];

async function resolveCatalogOrgId(Org) {
Expand All @@ -430,20 +448,43 @@ async function resolveCatalogOrgId(Org) {
}

function buildEventWindow(dayOffset, durationHours, now, startHour = 19) {
const wholeHour = Math.floor(startHour);
const minutes = Math.round((startHour - wholeHour) * 60);
const start = new Date(
Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate() + dayOffset,
startHour,
0,
wholeHour,
minutes,
0,
),
);
const end = new Date(start.getTime() + durationHours * 60 * 60 * 1000);
return { start, end };
}

function buildPivotTimeSlots(demo, now) {
if (!Array.isArray(demo.timeSlots) || !demo.timeSlots.length) {
return null;
}

return demo.timeSlots.map((slot) => {
const { start, end } = buildEventWindow(
demo.dayOffset,
slot.durationHours ?? 2,
now,
slot.startHour,
);
return {
id: slot.id,
label: slot.label,
start_time: start,
end_time: end,
};
});
}

async function run() {
const now = new Date();
const batchWeek = toIsoWeek(now);
Expand All @@ -461,6 +502,19 @@ async function run() {
demo.startHour ?? 19,
);

const timeSlots = buildPivotTimeSlots(demo, now);
const pivotMeta = {
batchWeek,
source: 'manual',
sourceUrl: demo.externalLink,
host: demo.host,
tags: demo.tags,
ingestStatus: 'published',
importedAt: now.toISOString(),
importedBy: 'seed:pivot-feed-events',
...(timeSlots ? { timeSlots } : {}),
};

await Event.findOneAndUpdate(
{ 'customFields.pivot.sourceUrl': demo.externalLink },
{
Expand All @@ -480,16 +534,7 @@ async function run() {
hostingId: catalogOrgId,
isDeleted: false,
customFields: {
pivot: {
batchWeek,
source: 'manual',
sourceUrl: demo.externalLink,
host: demo.host,
tags: demo.tags,
ingestStatus: 'published',
importedAt: now.toISOString(),
importedBy: 'seed:pivot-feed-events',
},
pivot: pivotMeta,
},
},
},
Expand Down
15 changes: 15 additions & 0 deletions backend/migrations/sendPivotWeeklyPush.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,21 @@ async function run() {

const { sent, failed } = await sendAllMessages(messages);
console.log(`[send:pivot-weekly-push] sent=${sent} failed=${failed}`);

// Freeze this week's Lab metrics now that the drop went out (best-effort).
try {
const { rebuildWeeklySnapshot } = require('../services/pivotWeeklySnapshotService');
const rebuild = await rebuildWeeklySnapshot(req, { batchWeek });
if (rebuild.error) {
console.warn(`[send:pivot-weekly-push] snapshot rebuild skipped: ${rebuild.error}`);
} else {
console.log(`[send:pivot-weekly-push] weekly snapshot rebuilt for ${batchWeek}`);
}
} catch (error) {
console.warn(
`[send:pivot-weekly-push] snapshot rebuild failed (send already completed): ${error.message}`
);
}
}

run()
Expand Down
26 changes: 25 additions & 1 deletion backend/routes/authRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const { getFriendRequests } = require('../utilities/friendUtils');
const { createSession, validateSession, deleteSession, deleteAllUserSessions, getUserSessions, getUserSessionsForGlobalUser, deleteSessionById, deleteSessionByIdForGlobalUser, revokeAllOtherSessionsForGlobalUser } = require('../utilities/sessionUtils');
const { getCookieDomain } = require('../utilities/cookieUtils');
const authGlobalService = require('../services/authGlobalService');
const { normalizePivotLeaveAuthUser } = require('../services/pivotProfileService');
const {
isAdminLevelAccount,
getMfaStatus,
Expand Down Expand Up @@ -138,6 +139,12 @@ async function getCurrentTenantAdminUser(req) {
}

async function completeLoginWithAdminMfa(req, res, globalUser, tenantUser, platformRoles, message) {
if (tenantUser?._id) {
const normalized = await normalizePivotLeaveAuthUser(req, tenantUser._id, platformRoles);
if (normalized) {
tenantUser = normalized;
}
}
if (tenantUser?.accessSuspended) {
return {
status: 403,
Expand Down Expand Up @@ -420,7 +427,24 @@ router.post('/refresh-token', async (req, res) => {
const { user, globalUser } = validation;
const isMobile = isMobileClient(req);

if (user?.accessSuspended) {
if (user?._id) {
const normalized = await normalizePivotLeaveAuthUser(req, user._id);
if (normalized) {
if (normalized.accessSuspended) {
return res.status(403).json({
success: false,
message: 'This account has been suspended.',
code: 'ACCOUNT_SUSPENDED',
});
}
} else if (user?.accessSuspended) {
return res.status(403).json({
success: false,
message: 'This account has been suspended.',
code: 'ACCOUNT_SUSPENDED',
});
}
} else if (user?.accessSuspended) {
return res.status(403).json({
success: false,
message: 'This account has been suspended.',
Expand Down
Loading