(config: PortraitRoutesConfig)
| 163 | // ============================================================================= |
| 164 | |
| 165 | export function createPortraitRouter(config: PortraitRoutesConfig): Router { |
| 166 | const { orgDb, memberDb, invalidateMemberContextCache } = config; |
| 167 | const router = Router(); |
| 168 | |
| 169 | // GET / — get current portrait metadata |
| 170 | router.get('/', requireAuth, async (req, res) => { |
| 171 | try { |
| 172 | const userId = resolveUserId(req); |
| 173 | if (!userId) { |
| 174 | return res.status(401).json({ error: 'Not authenticated' }); |
| 175 | } |
| 176 | |
| 177 | const portrait = await portraitDb.getActivePortrait(userId); |
| 178 | const pending = await portraitDb.getLatestGenerated(userId); |
| 179 | const monthlyCount = await portraitDb.countMonthlyGenerations(userId); |
| 180 | const canGenerate = await isPaidMember(req, orgDb, memberDb); |
| 181 | |
| 182 | res.json({ |
| 183 | portrait: portrait || null, |
| 184 | pending: pending || null, |
| 185 | generationsThisMonth: monthlyCount, |
| 186 | maxMonthlyGenerations: MAX_MONTHLY_GENERATIONS, |
| 187 | canGenerate, |
| 188 | vibeOptions: Object.keys(VIBE_OPTIONS), |
| 189 | }); |
| 190 | } catch (err) { |
| 191 | logger.error({ err }, 'Failed to get portrait'); |
| 192 | res.status(500).json({ error: 'Failed to load portrait' }); |
| 193 | } |
| 194 | }); |
| 195 | |
| 196 | // POST /generate — upload photo + vibe, generate portrait |
| 197 | router.post('/generate', requireAuth, upload.single('photo'), async (req, res) => { |
| 198 | try { |
| 199 | const userId = resolveUserId(req); |
| 200 | if (!userId) { |
| 201 | return res.status(401).json({ error: 'Not authenticated' }); |
| 202 | } |
| 203 | |
| 204 | if (!await isPaidMember(req, orgDb, memberDb)) { |
| 205 | return res.status(402).json({ error: 'Active subscription required for portrait generation' }); |
| 206 | } |
| 207 | |
| 208 | const monthlyCount = await portraitDb.countMonthlyGenerations(userId); |
| 209 | if (monthlyCount >= MAX_MONTHLY_GENERATIONS) { |
| 210 | return res.status(429).json({ |
| 211 | error: 'Monthly generation limit reached', |
| 212 | generationsThisMonth: monthlyCount, |
| 213 | maxMonthlyGenerations: MAX_MONTHLY_GENERATIONS, |
| 214 | }); |
| 215 | } |
| 216 | |
| 217 | const vibe = (req.body.vibe as string) || 'casual'; |
| 218 | const photoBuffer = req.file?.buffer; |
| 219 | const photoMimeType = req.file?.mimetype; |
| 220 | |
| 221 | const result = await generatePortrait({ |
| 222 | photoBuffer, |
no test coverage detected