mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -04:00
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
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 });
|
||
}
|
||
|
||
// 5–7. 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 });
|
||
}
|
||
};
|