V2 lives entirely under v2/ and is built around three asks the team raised about V1: per-video assets sometimes drifted onto the wrong trend, hashtag scrapes returned junk that wasn't filterable per-client, and there was no multi-user model behind Microsoft SSO. Highlights: - Stable TikTok numeric-id key for every per-video asset; URL form drift is logged loudly to drift_log.jsonl and never silently nulls assets. Stage 5 manifest hard-gates Stage 6 if any selected video is missing any required asset; --drop-failing auto-backfills from the next-best recipe candidates. - Per-brief engagement floor (min_likes / min_plays / min_stl_pct), applied at Apify scrape time and re-validated locally; spend_log.json records raw_returned vs kept_after_floor per scrape. - Users + teams + memberships with owner/admin/editor/viewer roles; SSO upserts a user keyed on Azure oid, auto-creates a personal team, and a super-admin is bootstrapped via BOOTSTRAP_SUPER_ADMIN_EMAIL on first sign-in. Phase A integration test: 16/16 pass. - 10-stage TS pipeline (brief → seed → scrape1 → select → scrape2 → validate → analyse → insights → trends → qa → build) wired through one CLI; each stage idempotent + resumable from disk via .state sentinels. §4.5 rubrics shipped under prompts/ and loaded into Claude calls. - React 18 + Vite + TS + Tailwind operator SPA: brief intake form, team management, super-admin user list, help/FAQ ported from V1. - Separate Docker Compose project (name: social-reporting-v2, port 3457, Postgres 5437) with deploy/setup-v2.sh, deploy-v2.sh, rollback-to-v1.sh scripts that take over V1's /social-reports URL and let us roll back. Verification: 62 unit tests pass (auth/session, ids extractor with full URL fixture, engagement floor, recipes, manifest, linking-fix, MoM compare). Live smoke run on a Dove brief: 1400 raw → 253 kept (82% culled) → 21 fully-bundled videos → 25 editorial trends across 8 brief-driven categories, with drift=0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47 lines
1.9 KiB
TypeScript
47 lines
1.9 KiB
TypeScript
// Super-admin only endpoints.
|
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
import { requireAuth } from '../middleware/require-auth.js';
|
|
import { requireSuperAdmin } from '../middleware/require-super-admin.js';
|
|
import { listAllUsers, setSuperAdmin, getUserById } from '../db/users.js';
|
|
import { parseJSONBody, sendJSON } from '../lib/http.js';
|
|
|
|
export async function handleListAllUsers(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
const session = requireAuth(req, res);
|
|
if (!session) return;
|
|
if (!await requireSuperAdmin(res, session)) return;
|
|
const users = await listAllUsers();
|
|
sendJSON(res, 200, {
|
|
users: users.map((u) => ({
|
|
id: u.id, email: u.email, display_name: u.display_name,
|
|
is_super_admin: u.is_super_admin,
|
|
created_at: u.created_at, last_login_at: u.last_login_at,
|
|
})),
|
|
});
|
|
}
|
|
|
|
export async function handleToggleSuperAdmin(
|
|
req: IncomingMessage, res: ServerResponse, userId: string,
|
|
): Promise<void> {
|
|
const session = requireAuth(req, res);
|
|
if (!session) return;
|
|
if (!await requireSuperAdmin(res, session)) return;
|
|
|
|
let is_super_admin: boolean | undefined;
|
|
try {
|
|
const body = await parseJSONBody<{ is_super_admin?: boolean }>(req);
|
|
is_super_admin = body.is_super_admin;
|
|
} catch { sendJSON(res, 400, { error: 'Invalid body' }); return; }
|
|
if (typeof is_super_admin !== 'boolean') {
|
|
sendJSON(res, 400, { error: 'is_super_admin must be a boolean' });
|
|
return;
|
|
}
|
|
// Prevent self-demotion (avoid locking everyone out).
|
|
if (session.user_id === userId && !is_super_admin) {
|
|
sendJSON(res, 400, { error: 'Cannot remove your own super-admin role' });
|
|
return;
|
|
}
|
|
const target = await getUserById(userId);
|
|
if (!target) { sendJSON(res, 404, { error: 'User not found' }); return; }
|
|
const updated = await setSuperAdmin(userId, is_super_admin);
|
|
sendJSON(res, 200, { ok: true, user: { id: updated.id, email: updated.email, is_super_admin: updated.is_super_admin } });
|
|
}
|