import { error } from '@sveltejs/kit'; import { db } from '$lib/server/db/index.js'; import { inviteLink, participant, audioFile, rating, overallRating } from '$lib/server/db/schema.js'; import { eq, isNull, and, inArray } from 'drizzle-orm'; import { parseStoredTags, matchesInviteTags } from '$lib/server/tag-utils.js'; import { env } from '$env/dynamic/private'; export async function load({ url, cookies }) { const token = url.searchParams.get('token'); if (!token) { throw error(400, 'Invalid or missing invite token'); } const invites = await db.select().from(inviteLink).where( and( eq(inviteLink.token, token), isNull(inviteLink.deletedAt) ) ); if (invites.length === 0) { throw error(404, 'Invite link not found or has been deleted'); } const { tags: inviteTagString, ...safeInvite } = invites[0]; const inviteTags = parseStoredTags(inviteTagString); let participantId = cookies.get(`participant-${token}`); let isExistingParticipant = false; if (participantId) { const participants = await db .select() .from(participant) .where( and( eq(participant.id, participantId), isNull(participant.deletedAt) ) ); isExistingParticipant = participants.length > 0; } if (!isExistingParticipant) { participantId = crypto.randomUUID(); await db.insert(participant).values({ id: participantId, inviteToken: token, sessionId: null, createdAt: new Date() }); await db .update(inviteLink) .set({ isUsed: true, usedAt: new Date() }) .where(eq(inviteLink.token, token)); cookies.set(`participant-${token}`, participantId, { path: '/', httpOnly: true, secure: false, sameSite: 'strict', maxAge: 60 * 60 * 24 * 30 }); } const audioRows = await db.select({ id: audioFile.id, filename: audioFile.filename, contentType: audioFile.contentType, duration: audioFile.duration, fileSize: audioFile.fileSize, createdAt: audioFile.createdAt, tags: audioFile.tags }) .from(audioFile) .where(isNull(audioFile.deletedAt)); // Only show active audio files const filteredAudio = audioRows .map((audio) => ({ data: { id: audio.id, filename: audio.filename, contentType: audio.contentType, duration: audio.duration, fileSize: audio.fileSize, createdAt: audio.createdAt }, tags: parseStoredTags(audio.tags) })) .filter(({ tags }) => matchesInviteTags(tags, inviteTags)) .map(({ data }) => data); const allowedAudioIds = filteredAudio.map((file) => file.id); const displayContinuousRating = env.DISPLAY_CONT_RATING !== 'false'; let completedRatings = []; if (allowedAudioIds.length > 0) { if (displayContinuousRating) { completedRatings = await db .select({ audioFileId: rating.audioFileId }) .from(rating) .innerJoin(audioFile, and( eq(rating.audioFileId, audioFile.id), isNull(audioFile.deletedAt) )) .where( and( eq(rating.participantId, participantId), eq(rating.isCompleted, true), isNull(rating.deletedAt), inArray(rating.audioFileId, allowedAudioIds) ) ); } else { completedRatings = await db .select({ audioFileId: overallRating.audioFileId }) .from(overallRating) .innerJoin(audioFile, and( eq(overallRating.audioFileId, audioFile.id), isNull(audioFile.deletedAt) )) .where( and( eq(overallRating.participantId, participantId), isNull(overallRating.deletedAt), inArray(overallRating.audioFileId, allowedAudioIds) ) ); } } const completedAudioIds = new Set(completedRatings.map((r) => r.audioFileId)); const audioFilesWithStatus = filteredAudio.map(file => ({ ...file, isCompleted: completedAudioIds.has(file.id) })); return { invite: safeInvite, participantId, audioFiles: audioFilesWithStatus, token, completedCount: completedRatings.length, totalCount: filteredAudio.length, displayContinuousRating }; }