@@ -289,4 +289,87 @@ router.post('/reset-password', (req: Request, res: Response) => {
289289 } ) ( ) ;
290290} ) ;
291291
292+ /**
293+ * POST /auth/permissions
294+ * Grant or update course permissions for a user (called by payment webhooks)
295+ *
296+ * Body params:
297+ * - userId: string (required) - CouchDB username
298+ * - courseId: string (required) - Course identifier
299+ * - action: 'grant_access' (required)
300+ * - provider: string (optional) - 'stripe', 'manual', etc.
301+ * - metadata: object (optional) - Additional payment metadata
302+ */
303+ router . post ( '/permissions' , ( req : Request , res : Response ) => {
304+ void ( async ( ) => {
305+ try {
306+ // Verify authorization
307+ const authHeader = req . headers . authorization ;
308+ const expectedAuth = `Bearer ${ process . env . PERMISSIONS_SECRET } ` ;
309+
310+ if ( ! authHeader || authHeader !== expectedAuth ) {
311+ logger . warn ( 'Unauthorized permissions request' ) ;
312+ return res . status ( 401 ) . json ( { ok : false , error : 'Unauthorized' } ) ;
313+ }
314+
315+ const { userId, courseId, action, provider, metadata } = req . body ;
316+
317+ // Validate required fields
318+ if ( ! userId || ! courseId || ! action ) {
319+ return res . status ( 400 ) . json ( {
320+ ok : false ,
321+ error : 'Missing required fields: userId, courseId, action'
322+ } ) ;
323+ }
324+
325+ if ( action !== 'grant_access' ) {
326+ return res . status ( 400 ) . json ( {
327+ ok : false ,
328+ error : 'Invalid action. Only "grant_access" is supported.'
329+ } ) ;
330+ }
331+
332+ // Find user in _users db
333+ const userDoc = await findUserByUsername ( userId ) ;
334+ if ( ! userDoc ) {
335+ logger . error ( `Permissions request for non-existent user: ${ userId } ` ) ;
336+ return res . status ( 404 ) . json ( { ok : false , error : 'User not found' } ) ;
337+ }
338+
339+ // Initialize entitlements if not present
340+ if ( ! userDoc . entitlements ) {
341+ userDoc . entitlements = { } ;
342+ }
343+
344+ // Get existing entitlement (preserve registrationDate if exists)
345+ const existingEntitlement = userDoc . entitlements [ courseId ] ;
346+
347+ // Update to paid status
348+ userDoc . entitlements [ courseId ] = {
349+ status : 'paid' ,
350+ registrationDate : existingEntitlement ?. registrationDate || new Date ( ) . toISOString ( ) ,
351+ purchaseDate : new Date ( ) . toISOString ( ) ,
352+ // No expires field for paid users
353+ } ;
354+
355+ // Save to _users db
356+ await updateUserDoc ( userDoc ) ;
357+
358+ logger . info ( `Granted ${ courseId } access to user ${ userId } via ${ provider || 'unknown' } ` ) ;
359+
360+ res . json ( {
361+ ok : true ,
362+ message : `Access granted to ${ courseId } for user ${ userId } `
363+ } ) ;
364+
365+ } catch ( error ) {
366+ logger . error ( 'Error granting permissions:' , error ) ;
367+ res . status ( 500 ) . json ( {
368+ ok : false ,
369+ error : 'Failed to grant permissions' ,
370+ } ) ;
371+ }
372+ } ) ( ) ;
373+ } ) ;
374+
292375export default router ;
0 commit comments