Skip to content

Commit 1312cc6

Browse files
committed
add permissions endpoint for purchases
1 parent 160e06f commit 1312cc6

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

packages/express/.env.development

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ COUCHDB_PASSWORD=password
99
NODE_ENV=platform
1010

1111
VERSION=localdev
12+
13+
# Shared secret for payment webhook authorization (Stripe → /express/permissions)
14+
# Must match PERMISSIONS_SECRET in business backend .env
15+
PERMISSIONS_SECRET=dev-permissions-secret-12345

packages/express/src/routes/auth.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
292375
export default router;

0 commit comments

Comments
 (0)