Skip to content

Commit 817b64f

Browse files
committed
add init-trial endpoint
1 parent 1312cc6 commit 817b64f

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

packages/express/src/routes/auth.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,95 @@ router.post('/reset-password', (req: Request, res: Response) => {
289289
})();
290290
});
291291

292+
/**
293+
* POST /auth/initialize-trial
294+
* Initialize trial entitlement for newly created user
295+
* Called by frontend immediately after CouchDB account creation
296+
*
297+
* Body params:
298+
* - username: string (required) - CouchDB username
299+
* - origin: string (optional) - Frontend origin URL (e.g., 'letterspractice.com') for courseId inference
300+
* - courseId: string (optional) - Explicit courseId, if not provided inferred from origin
301+
*/
302+
router.post('/initialize-trial', (req: Request, res: Response) => {
303+
void (async () => {
304+
try {
305+
const { username, origin, courseId: explicitCourseId } = req.body;
306+
307+
if (!username) {
308+
return res.status(400).json({ ok: false, error: 'Username required' });
309+
}
310+
311+
// Infer courseId from origin if not explicitly provided
312+
let courseId = explicitCourseId;
313+
if (!courseId && origin) {
314+
// Map origin to courseId (e.g., 'letterspractice.com' → 'letterspractice-basic')
315+
const originMap: Record<string, string> = {
316+
'letterspractice.com': 'letterspractice-basic',
317+
'localhost:5173': 'letterspractice-basic', // local dev
318+
'localhost:3000': 'letterspractice-basic', // express server itself
319+
};
320+
courseId = originMap[origin] || origin.replace(/\./g, '-').toLowerCase();
321+
}
322+
323+
// Default to letterspractice-basic if still not determined
324+
if (!courseId) {
325+
courseId = 'letterspractice-basic';
326+
}
327+
328+
// Find user in _users db (should exist since just created via CouchDB)
329+
const userDoc = await findUserByUsername(username);
330+
if (!userDoc) {
331+
return res.status(404).json({ ok: false, error: 'User not found' });
332+
}
333+
334+
// Initialize entitlements if not present
335+
if (!userDoc.entitlements) {
336+
userDoc.entitlements = {};
337+
}
338+
339+
// Don't overwrite if entitlement already exists (idempotent)
340+
if (userDoc.entitlements[courseId]) {
341+
logger.info(`Trial already initialized for ${username} on ${courseId} - skipping`);
342+
return res.json({
343+
ok: true,
344+
message: 'Trial already initialized',
345+
entitlement: userDoc.entitlements[courseId]
346+
});
347+
}
348+
349+
// Calculate expiration: 30 days from now
350+
const now = new Date();
351+
const expiresDate = new Date(now);
352+
expiresDate.setDate(expiresDate.getDate() + 30);
353+
354+
// Create trial entitlement
355+
userDoc.entitlements[courseId] = {
356+
status: 'trial',
357+
registrationDate: now.toISOString(),
358+
purchaseDate: now.toISOString(), // Same as registration for trials
359+
expires: expiresDate.toISOString(),
360+
};
361+
362+
await updateUserDoc(userDoc);
363+
364+
logger.info(`Trial initialized for ${username} on ${courseId} - expires ${expiresDate.toISOString()}`);
365+
366+
res.json({
367+
ok: true,
368+
entitlement: userDoc.entitlements[courseId]
369+
});
370+
371+
} catch (error) {
372+
logger.error('Error initializing trial:', error);
373+
res.status(500).json({
374+
ok: false,
375+
error: 'Failed to initialize trial',
376+
});
377+
}
378+
})();
379+
});
380+
292381
/**
293382
* POST /auth/permissions
294383
* Grant or update course permissions for a user (called by payment webhooks)

0 commit comments

Comments
 (0)