diff --git a/server/api/habits/[id]/complete.post.ts b/server/api/habits/[id]/complete.post.ts new file mode 100644 index 0000000..36b25f3 --- /dev/null +++ b/server/api/habits/[id]/complete.post.ts @@ -0,0 +1,87 @@ +import { useSession } from 'h3'; + +interface CompletionResponse { + message: string; + reward: { + coins: number; + }; + updatedCoins: number; +} + +/** + * A helper function to safely get the authenticated user's ID from the session. + */ +async function getUserIdFromSession(event: any): Promise { + const session = await useSession(event, { + password: process.env.SESSION_PASSWORD, + }); + const userId = session.data?.user?.id; + if (!userId) { + throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); + } + return userId; +} + +export default defineEventHandler(async (event): Promise => { + const userId = await getUserIdFromSession(event); + const habitId = parseInt(event.context.params?.id || '', 10); + + if (isNaN(habitId)) { + throw createError({ statusCode: 400, statusMessage: 'Invalid habit ID.' }); + } + + const habit = await prisma.habit.findFirst({ + where: { id: habitId, userId }, + }); + + if (!habit) { + throw createError({ statusCode: 404, statusMessage: 'Habit not found.' }); + } + + const today = new Date(); + const dayOfWeek = today.getDay(); // 0 (Sunday) to 6 (Saturday) + if (!(habit.daysOfWeek as number[]).includes(dayOfWeek)) { + throw createError({ statusCode: 400, statusMessage: 'Habit is not active today.' }); + } + + // Normalize date to the beginning of the day for consistent checks + const startOfToday = new Date(); + startOfToday.setUTCHours(0, 0, 0, 0); + + const existingCompletion = await prisma.habitCompletion.findFirst({ + where: { + habitId: habitId, + date: startOfToday, // Use precise equality check + }, + }); + + if (existingCompletion) { + throw createError({ statusCode: 409, statusMessage: 'Habit already completed today.' }); + } + + const rewardCoins = 3; + const [, updatedUser] = await prisma.$transaction([ + prisma.habitCompletion.create({ + data: { + habitId: habitId, + date: startOfToday, // Save the normalized date + }, + }), + prisma.user.update({ + where: { id: userId }, + data: { + coins: { + increment: rewardCoins, + }, + }, + }), + ]); + + return { + message: 'Habit completed successfully!', + reward: { + coins: rewardCoins, + }, + updatedCoins: updatedUser.coins, + }; +}); diff --git a/server/api/habits/index.get.ts b/server/api/habits/index.get.ts new file mode 100644 index 0000000..11c341a --- /dev/null +++ b/server/api/habits/index.get.ts @@ -0,0 +1,56 @@ +import { useSession } from 'h3'; + +// DTO to shape the output +interface HabitCompletionDto { + date: Date; +} + +interface HabitDto { + id: number; + name: string; + daysOfWeek: number[]; + completions: HabitCompletionDto[]; +} + +/** + * A helper function to safely get the authenticated user's ID from the session. + */ +async function getUserIdFromSession(event: any): Promise { + const session = await useSession(event, { + password: process.env.SESSION_PASSWORD, + }); + + const userId = session.data?.user?.id; + if (!userId) { + throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); + } + return userId; +} + +export default defineEventHandler(async (event): Promise => { + const userId = await getUserIdFromSession(event); + + const habits = await prisma.habit.findMany({ + where: { + userId: userId, + }, + include: { + completions: { + orderBy: { + date: 'desc', + }, + take: 30, // Limit completions to the last 30 for performance + }, + }, + }); + + // Map to DTOs + return habits.map((habit: any) => ({ + id: habit.id, + name: habit.name, + daysOfWeek: habit.daysOfWeek, // Assuming daysOfWeek is already in the correct format + completions: habit.completions.map((comp: any) => ({ + date: comp.date, + })), + })); +}); diff --git a/server/api/habits/index.post.ts b/server/api/habits/index.post.ts new file mode 100644 index 0000000..701e874 --- /dev/null +++ b/server/api/habits/index.post.ts @@ -0,0 +1,62 @@ +import { useSession } from 'h3'; + +interface HabitDto { + id: number; + name: string; + daysOfWeek: number[]; +} + +/** + * A helper function to safely get the authenticated user's ID from the session. + */ +async function getUserIdFromSession(event: any): Promise { + const session = await useSession(event, { + password: process.env.SESSION_PASSWORD, + }); + + const userId = session.data?.user?.id; + if (!userId) { + throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); + } + return userId; +} + +export default defineEventHandler(async (event): Promise => { + const userId = await getUserIdFromSession(event); + const { name, daysOfWeek } = await readBody(event); + + // --- Validation --- + if (!name || !Array.isArray(daysOfWeek)) { + throw createError({ statusCode: 400, statusMessage: 'Invalid input: name and daysOfWeek are required.' }); + } + + // Sanitize daysOfWeek to ensure it's a unique set of valid numbers + const validDays = daysOfWeek.filter(day => typeof day === 'number' && day >= 0 && day <= 6); + const sanitizedDays = [...new Set(validDays)].sort(); + + + // --- Business Rule: Max 3 Habits --- + const habitCount = await prisma.habit.count({ where: { userId } }); + if (habitCount >= 3) { + throw createError({ statusCode: 400, statusMessage: 'Maximum number of 3 habits reached.' }); + } + + // --- Create Habit --- + const newHabit = await prisma.habit.create({ + data: { + userId, + name, + daysOfWeek: sanitizedDays, + }, + }); + + // Set 201 Created status + setResponseStatus(event, 201); + + // Return DTO + return { + id: newHabit.id, + name: newHabit.name, + daysOfWeek: newHabit.daysOfWeek as number[], + }; +});