import express, { type Application, type Request, type Response, type RequestHandler } from 'express'; import type { Repository } from '../db/repository.js'; import { requireAuth } from './auth.js'; const passthrough: RequestHandler = (_req, _res, next) => next(); /** * /api/users/me endpoints. * * When `authActive` is true, routes are gated by `requireAuth` and rely on * `req.user.id` being populated by passport. When auth is disabled (tests or * standalone mode), the guard is skipped and callers are expected to inject * `req.user` via their own middleware (e.g. the test harness). */ export function mountUsersApi(app: Application, repo: Repository, authActive = true): void { const guard = authActive ? requireAuth : passthrough; // Viewer's orgs for the visibility ('org' scope) picker: Gitea orgs (cached // at OAuth callback) + local orgs (membership). Returning both here means // every picker (CreateTaskDialog / Schedules / DetailPanel / Preferences) // lists local orgs without any client change. app.get('/api/users/me/orgs', guard, (req: Request, res: Response) => { const user = req.user as Express.User | undefined; if (!user) { // Defensive: with authActive=false and no injected user, return 401-shaped error. res.status(401).json({ error: 'Unauthorized' }); return; } const gitea = repo.listUserGiteaOrgs(user.id); const local = repo.listUserLocalOrgs(user.id).map(o => ({ orgId: o.orgId, orgName: o.name, fetchedAt: '' })); res.json({ orgs: [...gitea, ...local] }); }); // Update viewer's per-user preferences (currently just default visibility). app.patch('/api/users/me/preferences', guard, express.json(), (req: Request, res: Response) => { const user = req.user as Express.User | undefined; if (!user) { res.status(401).json({ error: 'Unauthorized' }); return; } const body = (req.body ?? {}) as { defaultVisibility?: unknown; defaultVisibilityOrgId?: unknown; }; const { defaultVisibility, defaultVisibilityOrgId } = body; if (defaultVisibility !== undefined && defaultVisibility !== null && !['private', 'org', 'public'].includes(defaultVisibility as string)) { res.status(400).json({ error: 'invalid defaultVisibility' }); return; } if (defaultVisibility === 'org') { const scopeId = typeof defaultVisibilityOrgId === 'string' ? defaultVisibilityOrgId : ''; if (!scopeId) { res.status(400).json({ error: 'default_visibility_org_id is required when defaultVisibility is "org"' }); return; } if (!user.orgIds.includes(scopeId)) { res.status(400).json({ error: 'default_visibility_org_id must be one of your orgs' }); return; } } repo.updateUser(user.id, { defaultVisibility: (defaultVisibility as 'private' | 'org' | 'public' | undefined) ?? undefined, defaultVisibilityOrgId: (defaultVisibilityOrgId as string | null | undefined) ?? null, }); res.json({ ok: true }); }); }