Files
bibdle/src/routes/api/submit-verse/+server.ts
T
2026-07-07 17:02:24 -04:00

90 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { dailyCompletions } from '$lib/server/db/schema';
import { eq, and } from 'drizzle-orm';
import {
scheduleSubmission,
validateSelection,
getCooldownState
} from '$lib/server/verse-submission';
/**
* POST /api/submit-verse
*
* Accepts a user-chosen verse (bookId/chapter/verse) and reserves a concrete
* future date for it via the scheduling scan. Auth required, solved-today gate,
* 7-day rolling cooldown, and structural validation all enforced here.
*
* Body: { bookId, chapter, verse, localDate }
*/
export const POST: RequestHandler = async ({ request, locals }) => {
// 1. Auth.
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = locals.user.id;
let body: any;
try {
body = await request.json();
} catch {
return json({ error: 'Invalid JSON body' }, { status: 400 });
}
const { bookId, chapter, verse, localDate } = body ?? {};
// 2. Solved-today gate (engagement gate, not security).
if (typeof localDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(localDate)) {
return json({ error: 'A valid localDate (YYYY-MM-DD) is required' }, { status: 400 });
}
const [completion] = await db
.select({ id: dailyCompletions.id })
.from(dailyCompletions)
.where(
and(
eq(dailyCompletions.anonymousId, userId),
eq(dailyCompletions.date, localDate)
)
)
.limit(1);
if (!completion) {
return json({ error: "Solve today's puzzle first" }, { status: 403 });
}
// 3. Cooldown (rolling 7×24h, server UTC).
const cooldown = await getCooldownState(db, userId);
if (cooldown.onCooldown) {
return json(
{ error: 'Cooldown active', cooldownEndsAt: cooldown.cooldownEndsAt },
{ status: 429 }
);
}
// 4. Structural validation.
const validationError = validateSelection({ bookId, chapter, verse });
if (validationError) {
return json({ error: validationError }, { status: 400 });
}
// 57. Compute window + scheduling scan + transactional insert (with retry).
try {
const result = await scheduleSubmission(
db,
{ bookId, chapter, verse },
userId
);
return json(
{
scheduledDate: result.scheduledDate,
reference: result.reference,
windowText: result.windowText
},
{ status: 201 }
);
} catch (err) {
console.error('submit-verse failed:', err);
return json({ error: 'Failed to schedule submission' }, { status: 500 });
}
};