import { redirect, fail } from '@sveltejs/kit'; import type { Actions } from './$types'; import * as auth from '$lib/server/auth'; import { db } from '$lib/server/db'; import { dailyCompletions } from '$lib/server/db/schema'; import { eq, inArray } from 'drizzle-orm'; export const actions: Actions = { default: async ({ request, cookies }) => { const data = await request.formData(); const email = data.get('email')?.toString(); const password = data.get('password')?.toString(); const anonymousId = data.get('anonymousId')?.toString(); if (!email || !password) { return fail(400, { error: 'Email and password are required' }); } // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return fail(400, { error: 'Please enter a valid email address' }); } if (password.length < 6) { return fail(400, { error: 'Password must be at least 6 characters' }); } try { // Get user by email const user = await auth.getUserByEmail(email); if (!user || !user.passwordHash) { return fail(400, { error: 'Invalid email or password' }); } // Verify password const isValidPassword = await auth.verifyPassword(password, user.passwordHash); if (!isValidPassword) { return fail(400, { error: 'Invalid email or password' }); } // Migrate anonymous stats if different anonymous ID if (anonymousId && anonymousId !== user.id) { try { // Update all daily completions from the local anonymous ID to the user's ID await db .update(dailyCompletions) .set({ anonymousId: user.id }) .where(eq(dailyCompletions.anonymousId, anonymousId)); console.log(`Migrated stats from ${anonymousId} to ${user.id}`); // Deduplicate any entries for the same date after migration const allUserCompletions = await db .select() .from(dailyCompletions) .where(eq(dailyCompletions.anonymousId, user.id)); // Group by date to find duplicates const dateGroups = new Map(); for (const completion of allUserCompletions) { const date = completion.date; if (!dateGroups.has(date)) { dateGroups.set(date, []); } dateGroups.get(date)!.push(completion); } // Process dates with duplicates const duplicateIds: string[] = []; for (const [date, completions] of dateGroups) { if (completions.length > 1) { // Sort by completedAt timestamp (earliest first) completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime()); // Keep the first (earliest), mark the rest for deletion const toDelete = completions.slice(1); duplicateIds.push(...toDelete.map(c => c.id)); console.log(`Found ${completions.length} duplicates for date ${date}, keeping earliest, deleting ${toDelete.length}`); } } // Delete duplicate entries if (duplicateIds.length > 0) { await db .delete(dailyCompletions) .where(inArray(dailyCompletions.id, duplicateIds)); console.log(`Deleted ${duplicateIds.length} duplicate completion entries`); } } catch (error) { console.error('Error migrating anonymous stats:', error); // Don't fail the signin if stats migration fails } } // Create session const sessionToken = auth.generateSessionToken(); const session = await auth.createSession(sessionToken, user.id); auth.setSessionTokenCookie({ cookies }, sessionToken, session.expiresAt); return { success: true }; } catch (error) { console.error('Sign in error:', error); return fail(500, { error: 'An error occurred during sign in' }); } } };