diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 575d418..9146506 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ on: env: PUBLIC_SUPABASE_URL: ${{ vars.PUBLIC_SUPABASE_URL || 'http://dummy' }} PUBLIC_SUPABASE_ANON_KEY: ${{ vars.PUBLIC_SUPABASE_ANON_KEY || 'anon-key' }} - PUBLIC_SUPABASE_SERVICE_KEY: ${{ vars.PUBLIC_SUPABASE_SERVICE_KEY }} - PUBLIC_GOOGLE_SERVICE_EMAIL: ${{ vars.PUBLIC_GOOGLE_SERVICE_EMAIL }} - PUBLIC_GOOGLE_PRIVATE_KEY: ${{ vars.PUBLIC_GOOGLE_PRIVATE_KEY }} + SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }} + GOOGLE_SERVICE_EMAIL: ${{ secrets.GOOGLE_SERVICE_EMAIL }} + GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }} jobs: build: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a03196..c8bd1ef 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,9 +12,9 @@ env: # SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} PUBLIC_SUPABASE_URL: ${{ vars.PUBLIC_SUPABASE_URL || 'http://dummy' }} PUBLIC_SUPABASE_ANON_KEY: ${{ vars.PUBLIC_SUPABASE_ANON_KEY || 'anon-key' }} - PUBLIC_SUPABASE_SERVICE_KEY: ${{ vars.PUBLIC_SUPABASE_SERVICE_KEY }} - PUBLIC_GOOGLE_SERVICE_EMAIL: ${{ vars.PUBLIC_GOOGLE_SERVICE_EMAIL }} - PUBLIC_GOOGLE_PRIVATE_KEY: ${{ vars.PUBLIC_GOOGLE_PRIVATE_KEY }} + SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }} + GOOGLE_SERVICE_EMAIL: ${{ secrets.GOOGLE_SERVICE_EMAIL }} + GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }} # Custom environment used for the Container registry domain, and a name for the Docker image that this workflow builds. REGISTRY: ghcr.io @@ -59,9 +59,9 @@ jobs: CONTAINER_PORT=${{ env.CONTAINER_PORT }} PUBLIC_SUPABASE_URL=${{ env.PUBLIC_SUPABASE_URL }} PUBLIC_SUPABASE_ANON_KEY=${{ env.PUBLIC_SUPABASE_ANON_KEY }} - PUBLIC_SUPABASE_SERVICE_KEY=${{ env.PUBLIC_SUPABASE_SERVICE_KEY }} - PUBLIC_GOOGLE_SERVICE_EMAIL=${{ env.PUBLIC_GOOGLE_SERVICE_EMAIL }} - PUBLIC_GOOGLE_PRIVATE_KEY=${{ env.PUBLIC_GOOGLE_PRIVATE_KEY }} + SUPABASE_SERVICE_KEY=${{ env.SUPABASE_SERVICE_KEY }} + GOOGLE_SERVICE_EMAIL=${{ env.GOOGLE_SERVICE_EMAIL }} + GOOGLE_PRIVATE_KEY=${{ env.GOOGLE_PRIVATE_KEY }} EOF # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. diff --git a/.gitignore b/.gitignore index fb7f88c..1d41d45 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ /.svelte-kit node_modules .env +docs/ +CLAUDE.md +.agents/ +.claude/ +skills-lock.json diff --git a/src/app.d.ts b/src/app.d.ts index 20bce2d..004f5e9 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,5 @@ import type { Session, SupabaseClient, User } from '@supabase/supabase-js'; +import type { AppRole } from '$lib/server/auth'; import type { Database } from './database.types.ts'; // import generated types declare global { @@ -9,9 +10,11 @@ declare global { safeGetSession: () => Promise<{ session: Session | null; user: User | null }>; session: Session | null; user: User | null; + userRole: AppRole | null; } interface PageData { session: Session | null; + userRole: AppRole | null; } // interface PageState {} // interface Platform {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 4b317a1..e4e5ede 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,3 +1,4 @@ +import type { AppRole } from '$lib/server/auth'; import { type Handle } from '@sveltejs/kit'; import { createServerClient } from '@supabase/ssr'; import { sequence } from '@sveltejs/kit/hooks'; @@ -48,20 +49,21 @@ const supabase: Handle = ({ event, resolve }) => { }); }; -// const authGuard: Handle = async ({ event, resolve }) => { -// const { session, user } = await event.locals.safeGetSession() -// event.locals.session = session -// event.locals.user = user -// -// if (!event.locals.session && event.url.pathname.startsWith('/private')) { -// redirect(303, '/login') -// } -// -// if (event.locals.session && event.url.pathname === '/login') { -// redirect(303, '/private') -// } -// -// return resolve(event) -// } +const authGuard: Handle = async ({ event, resolve }) => { + const { locals } = event; + const { session, user } = await locals.safeGetSession(); + let userRole: AppRole | null = null; -export const handle: Handle = sequence(supabase); + if (user) { + const { data: profile } = await locals.supabase.from('profiles').select('role').eq('id', user.id).single(); + userRole = (profile?.role as AppRole) ?? 'applicant'; + } + + locals.session = session; + locals.user = user; + locals.userRole = userRole; + + return resolve(event); +}; + +export const handle: Handle = sequence(supabase, authGuard); diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..6bea731 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,22 @@ +import { dev } from '$app/environment'; + +function debug(...args: unknown[]) { + if (dev) { + // eslint-disable-next-line no-console + console.log('[DEBUG]', ...args); + } +} + +function warn(...args: unknown[]) { + if (dev) { + // eslint-disable-next-line no-console + console.warn('[WARN]', ...args); + } +} + +function error(...args: unknown[]) { + // eslint-disable-next-line no-console + console.error(...args); +} + +export const logger = { debug, warn, error }; diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..070064b --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,32 @@ +import { type RequestEvent, error } from '@sveltejs/kit'; + +export type AppRole = 'applicant' | 'admin' | 'withdrawn' | 'inactive'; + +/** + * Requires authenticated user. Throws 401 if not logged in. + */ +export function requireAuth(event: RequestEvent) { + const { user, session } = event.locals; + if (!session || !user) { + throw error(401, 'Authentication required'); + } + return { user, session }; +} + +/** + * Requires a specific role. Throws 401 if not authenticated, 403 if wrong role. + */ +export function requireRole(event: RequestEvent, role: AppRole) { + const { user, session } = requireAuth(event); + if (event.locals.userRole !== role) { + throw error(403, 'Insufficient permissions'); + } + return { user, session }; +} + +/** + * Check if current user is admin. Does not throw. + */ +export function isAdmin(event: RequestEvent): boolean { + return event.locals.userRole === 'admin'; +} diff --git a/src/lib/server/supabaseAdmin.ts b/src/lib/server/supabaseAdmin.ts new file mode 100644 index 0000000..08c38c7 --- /dev/null +++ b/src/lib/server/supabaseAdmin.ts @@ -0,0 +1,9 @@ +import { PUBLIC_SUPABASE_URL } from '$env/static/public'; +import { SUPABASE_SERVICE_KEY } from '$env/static/private'; +import { createClient } from '@supabase/supabase-js'; + +/** + * Service-role Supabase client. Bypasses RLS. + * ONLY use in server-side code for admin operations. + */ +export const supabaseAdmin = createClient(PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_KEY); diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index f4f38ea..ebca90d 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,7 +1,10 @@ -export const load = async ({ locals: { safeGetSession }, cookies }) => { +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals: { safeGetSession, userRole }, cookies }) => { const { session } = await safeGetSession(); return { session, cookies: cookies.getAll(), + userRole: userRole ?? null, }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8f32f0e..17a767a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,12 +7,13 @@ import { page } from '$app/state'; import { invalidate } from '$app/navigation'; + import { logger } from '$lib/logger'; import { onMount } from 'svelte'; const { data, children } = $props(); const { session, supabase } = $derived(data); - // Sync $lib variables to data props + // Sync $lib variables to data props (must be synchronous so children can read in onMount) if (data?.uuid) uuid.set(data.uuid); if (data?.user?.user_metadata.full_name) username.set(data.user.user_metadata.full_name); if (data?.filledSigsheet) filledSigsheet.set(data.filledSigsheet); @@ -30,14 +31,12 @@ // Get applicant_names_list onMount(async () => { - console.log('Fetching applicant names.'); const { data: app_data, error: app_error } = await supabase.from('profiles').select('full_name'); if (app_error) { - console.error('Error fetching profile names: ', app_error); + logger.error('Error fetching profile names: ', app_error); } else if (app_data) { applicant_names_list.set(app_data.map(row => row.full_name)); } - console.log(applicant_names_list); }); @@ -65,13 +64,24 @@
{#if page.url.pathname !== '/login/'} - {#if isNavBarOpen} -
- -
- {/if} + +
(isNavBarOpen = false)} + onkeydown={e => { + if (e.key === 'Escape') isNavBarOpen = false; + }} + >
+ +
+ +
{/if}
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 17244c2..95ac1ae 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,6 +1,7 @@ import type { Answer, ISection, Question } from './consti-quiz/constiquiz-types.ts'; import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'; import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'; +import { logger } from '$lib/logger'; export async function load({ data, depends, fetch }) { depends('supabase:auth'); @@ -22,16 +23,11 @@ export async function load({ data, depends, fetch }) { }, }); - const { - data: { session }, - } = await supabase.auth.getSession(); - - const { - data: { user }, - } = await supabase.auth.getUser(); + const { session } = data; + const user = session?.user ?? null; if (!user) { - console.error('Failed to fetch user.'); + logger.error('Failed to fetch user.'); return { session: session, supabase: supabase, @@ -43,13 +39,9 @@ export async function load({ data, depends, fetch }) { } const uuid = user.id; - console.log('Fetched uuid:', uuid); - const username = user.email?.split('@')[0] ?? ''; - console.log('Fetched username:', username); // Fetch filledSigsheet - console.log('Fetching sigsheet from Supabase with ID:', uuid); let filledSigsheet: Set = new Set(); try { const { data: sigRows, error: sigError } = await supabase @@ -60,13 +52,11 @@ export async function load({ data, depends, fetch }) { if (sigError) throw sigError; filledSigsheet = new Set(sigRows?.map(row => row.member_id) ?? []); - console.log(`Fetched filledSigsheet: sigsheet size ${filledSigsheet.size}`); } catch (sigError) { - console.error('Error fetching sigsheet: ', sigError); + logger.error('Error fetching sigsheet: ', sigError); } // Fetch gdrive_folder_id - console.log('Fetching gdrive_folder_id for Applicant:', uuid); let gdrive_folder_id: string = ''; try { const response = await fetch('/api/get_gdrive_folder', { @@ -79,13 +69,13 @@ export async function load({ data, depends, fetch }) { if (!response.ok) { const gDriveError = await response.json().catch(() => ({})); - console.error('Error fetching gdrive folder:', gDriveError); + logger.error('Error fetching gdrive folder:', gDriveError); } else { const folderData = await response.json(); gdrive_folder_id = folderData.folder_id ?? ''; } } catch (gDriveError) { - console.error('Unexpected error fetching gdrive folder:', gDriveError); + logger.error('Unexpected error fetching gdrive folder:', gDriveError); } // Functions for fetching constiquiz @@ -97,8 +87,7 @@ export async function load({ data, depends, fetch }) { `); if (error) { - // TODO: handle error - console.error(error); + logger.error(error); throw error; } @@ -123,7 +112,7 @@ export async function load({ data, depends, fetch }) { `); if (error || !data) { - console.error(error); + logger.error(error); throw error; } @@ -147,7 +136,7 @@ export async function load({ data, depends, fetch }) { .eq('user_id', uuid); if (error) { - console.error(error); + logger.error(error); throw error; } @@ -157,5 +146,17 @@ export async function load({ data, depends, fetch }) { // fetch in parallel for faster results const [sections, questions, answers] = await Promise.all([fetchSections(), fetchQuestions(), fetchAnswers()]); - return { session, supabase, user, uuid, username, filledSigsheet, gdrive_folder_id, sections, questions, answers }; + return { + session, + supabase, + user, + uuid, + username, + filledSigsheet, + gdrive_folder_id, + sections, + questions, + answers, + userRole: data.userRole, + }; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 6e55d3d..47096d7 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,6 @@ {#if data.session}

Hello, {$username}! + {#if data.userRole} + ({data.userRole}) + {/if}

Your Dashboard

@@ -191,7 +198,7 @@

Signature Sheet

- {#each signatureSheet as section} + {#each signatureSheet as section (section.name)}

{section.name}

diff --git a/src/routes/api/admin/applicants/+server.ts b/src/routes/api/admin/applicants/+server.ts new file mode 100644 index 0000000..1be4506 --- /dev/null +++ b/src/routes/api/admin/applicants/+server.ts @@ -0,0 +1,21 @@ +import { type RequestEvent, json } from '@sveltejs/kit'; +import { requireRole } from '$lib/server/auth'; +import { supabaseAdmin } from '$lib/server/supabaseAdmin'; + +/** + * List all applicant profiles + */ +export async function GET(event: RequestEvent) { + requireRole(event, 'admin'); + + const { data, error } = await supabaseAdmin + .from('profiles') + .select('id, username, full_name, avatar_url, role') + .eq('role', 'applicant'); + + if (error) { + return json({ error: error.message }, { status: 500 }); + } + + return json({ applicants: data }); +} diff --git a/src/routes/api/admin/quiz-results/+server.ts b/src/routes/api/admin/quiz-results/+server.ts new file mode 100644 index 0000000..ae59966 --- /dev/null +++ b/src/routes/api/admin/quiz-results/+server.ts @@ -0,0 +1,51 @@ +import { type RequestEvent, json } from '@sveltejs/kit'; +import { requireRole } from '$lib/server/auth'; +import { supabaseAdmin } from '$lib/server/supabaseAdmin'; + +export async function GET(event: RequestEvent) { + requireRole(event, 'admin'); + + // Get all submissions with profile info + const { data: submissions, error: subError } = await supabaseAdmin.from('constiquiz-submissions').select(` + submission_id, + submitted_at, + user_id, + profiles!inner ( username, full_name ) + `); + + if (subError) { + return json({ error: subError.message }, { status: 500 }); + } + + // Get total scores per submitted user + const userIds = submissions?.map(s => s.user_id).filter(Boolean) ?? []; + + if (userIds.length === 0) { + return json({ results: [] }); + } + + const { data: answers, error: ansError } = await supabaseAdmin + .from('constiquiz-answers') + .select('user_id, points') + .in('user_id', userIds); + + if (ansError) { + return json({ error: ansError.message }, { status: 500 }); + } + + // Aggregate scores + const scoreMap: Record = {}; + for (const a of answers ?? []) { + scoreMap[a.user_id] = (scoreMap[a.user_id] ?? 0) + (a.points ?? 0); + } + + const results = submissions?.map(s => ({ + submission_id: s.submission_id, + submitted_at: s.submitted_at, + user_id: s.user_id, + profile: s.profiles, + total_score: scoreMap[s.user_id] ?? 0, + })); + + return json({ results }); +} diff --git a/src/routes/api/admin/quiz-results/[userId]/+server.ts b/src/routes/api/admin/quiz-results/[userId]/+server.ts new file mode 100644 index 0000000..2bc7d5d --- /dev/null +++ b/src/routes/api/admin/quiz-results/[userId]/+server.ts @@ -0,0 +1,35 @@ +import { type RequestEvent, json } from '@sveltejs/kit'; +import { requireRole } from '$lib/server/auth'; +import { supabaseAdmin } from '$lib/server/supabaseAdmin'; + +export async function GET(event: RequestEvent) { + requireRole(event, 'admin'); + const { userId } = event.params; + + const [answersRes, profileRes, submissionRes] = await Promise.all([ + supabaseAdmin + .from('constiquiz-answers') + .select( + ` + answer_id, question_id, answer_text, option_id, points, is_checked, + question:constiquiz-questions!inner ( + title, point_value, type, + section:constiquiz-sections!inner ( title ) + ) + `, + ) + .eq('user_id', userId), + supabaseAdmin.from('profiles').select('id, username, full_name').eq('id', userId).single(), + supabaseAdmin.from('constiquiz-submissions').select('submitted_at').eq('user_id', userId).maybeSingle(), + ]); + + if (answersRes.error) { + return json({ error: answersRes.error.message }, { status: 500 }); + } + + return json({ + profile: profileRes.data, + submitted_at: submissionRes.data?.submitted_at ?? null, + answers: answersRes.data, + }); +} diff --git a/src/routes/api/admin/sigsheet-progress/+server.ts b/src/routes/api/admin/sigsheet-progress/+server.ts new file mode 100644 index 0000000..f762fcf --- /dev/null +++ b/src/routes/api/admin/sigsheet-progress/+server.ts @@ -0,0 +1,43 @@ +import { type RequestEvent, json } from '@sveltejs/kit'; +import { requireRole } from '$lib/server/auth'; +import { supabaseAdmin } from '$lib/server/supabaseAdmin'; + +export async function GET(event: RequestEvent) { + requireRole(event, 'admin'); + + // Get total member count for progress calculation + const { count: totalMembers } = await supabaseAdmin.from('members').select('*', { count: 'exact', head: true }); + + const { data, error } = await supabaseAdmin.from('sigsheet').select(` + sig_id, signed_at, question, answer, member_id, member_name, + applicant:profiles!inner ( id, username, full_name ) + `); + + if (error) { + return json({ error: error.message }, { status: 500 }); + } + + // Group by applicant + type ApplicantProfile = { id: string; username: string; full_name: string }; + type Signature = { sig_id: string; signed_at: string; member_id: string; member_name: string }; + const byApplicant: Record = {}; + for (const row of data ?? []) { + const applicant = row.applicant as unknown as ApplicantProfile; + const key = applicant.id; + if (!byApplicant[key]) { + byApplicant[key] = { profile: applicant, signatures: [], count: 0 }; + } + byApplicant[key]!.signatures.push({ + sig_id: row.sig_id, + signed_at: row.signed_at, + member_id: row.member_id, + member_name: row.member_name, + }); + byApplicant[key]!.count++; + } + + return json({ + total_members: totalMembers, + progress: Object.values(byApplicant), + }); +} diff --git a/src/routes/api/answers/+server.js b/src/routes/api/answers/+server.js index 22de895..64c0f67 100644 --- a/src/routes/api/answers/+server.js +++ b/src/routes/api/answers/+server.js @@ -1,4 +1,5 @@ import { json } from '@sveltejs/kit'; +import { logger } from '$lib/logger'; export async function POST({ locals, request }) { const { supabase } = locals; @@ -30,7 +31,7 @@ export async function POST({ locals, request }) { ); if (error) { - console.error(error); + logger.error(error); return json({ success: false, error: error.message }, { status: 500 }); } diff --git a/src/routes/api/get_gdrive_folder/+server.js b/src/routes/api/get_gdrive_folder/+server.js index b9a5115..49e22e2 100644 --- a/src/routes/api/get_gdrive_folder/+server.js +++ b/src/routes/api/get_gdrive_folder/+server.js @@ -1,70 +1,70 @@ -import { PUBLIC_GOOGLE_PRIVATE_KEY, PUBLIC_GOOGLE_SERVICE_EMAIL } from '$env/static/public'; +import { GOOGLE_PRIVATE_KEY, GOOGLE_SERVICE_EMAIL } from '$env/static/private'; import { gdrive_root_folder } from '$lib/shared'; import { google } from 'googleapis'; -import { supabase } from '$lib/supabaseClient'; +import { logger } from '$lib/logger'; -export async function POST({ request }) { - console.log('Received POST request at /api/get_gdrive_folder'); - - // Add debugging logs to verify environment variables - console.log('Google API Credentials:', { - client_email: PUBLIC_GOOGLE_SERVICE_EMAIL, - private_key: PUBLIC_GOOGLE_PRIVATE_KEY ? 'Provided' : ' Not Provided', - }); +/** @type {import('./$types').RequestHandler} */ +export async function POST({ request, locals }) { + logger.debug('Received POST request at /api/get_gdrive_folder'); try { - const { uuid, username } = await request.json(); - + const { user } = await locals.safeGetSession(); + if (!user) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + const uuid = user.id; + const { username } = await request.json(); + const { supabase } = locals; // Ensure uuid is not empty if (uuid === '' || uuid === null) { - console.error('Validation Error: $uuid is empty: '); + logger.error('Validation Error: $uuid is empty'); return new Response(JSON.stringify({ error: 'UUID is empty.' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } - console.log('$uuid is filled.'); if (username === '' || username === null) { - console.error('Validation Error: $username is empty: '); + logger.error('Validation Error: $username is empty'); return new Response(JSON.stringify({ error: 'Username is empty.' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } - console.log('$username is filled.'); - console.log('Authenticating into Google Drive.'); + logger.debug('Authenticating into Google Drive'); // Authenticate with Google Drive API const auth = new google.auth.GoogleAuth({ credentials: { - client_email: PUBLIC_GOOGLE_SERVICE_EMAIL, - private_key: PUBLIC_GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'), + client_email: GOOGLE_SERVICE_EMAIL, + private_key: GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'), }, scopes: ['https://www.googleapis.com/auth/drive'], }); const drive = google.drive({ version: 'v3', auth }); - console.log('Authenticated into Google Drive.'); + logger.debug('Authenticated into Google Drive'); // Check if applicant already has a gdrive_folder try { - console.log('Searching for folder in Supabase.'); + logger.debug('Searching for folder in Supabase'); const { data, error } = await supabase .from('pic-folders') .select('gdrive_folder') .eq('applicant_uuid', uuid) .single(); - console.log('Done searching for folder in Supabase.'); // Error PGRST116: row not found; So throw unexpected error if (error && error.code !== 'PGRST116') { - console.error('Error reading from Supabase: ', error); + logger.error('Error reading from Supabase: ', error); throw new Error(error.message); } // If applicant has folder, return it if (data) { - console.log('Applicant does have folder:', data.gdrive_folder); + logger.debug('Applicant does have folder:', data.gdrive_folder); return new Response( JSON.stringify({ message: 'Applicant does have folder', @@ -77,7 +77,7 @@ export async function POST({ request }) { ); } } catch (supabaseError) { - console.error('Error reading from Supabase:', supabaseError); + logger.error('Error reading from Supabase:', supabaseError); return new Response(JSON.stringify({ error: 'Error reading from Supabase' }), { status: 500, headers: { 'Content-Type': 'application/json' }, @@ -93,16 +93,15 @@ export async function POST({ request }) { let folder_id = null; try { - console.log('No folder exists for user.'); - console.log('Creating new folder in Google Drive.'); + logger.debug('Creating new folder in Google Drive'); const gdrive_folder = await drive.files.create({ requestBody: fileMetadata, fields: 'id', }); folder_id = gdrive_folder.data.id; - console.log('New folder created:', folder_id); + logger.debug('New folder created:', folder_id); } catch (driveError) { - console.error('Error creating new folder in Google Drive:', { + logger.error('Error creating new folder in Google Drive:', { message: driveError instanceof Error ? driveError.message : 'Unknown error', errors: driveError instanceof Error && 'errors' in driveError @@ -113,7 +112,7 @@ export async function POST({ request }) { } try { - console.log('Inserting new folder into Supabase.'); + logger.debug('Inserting new folder into Supabase'); const { data, error } = await supabase .from('pic-folders') .insert({ @@ -123,11 +122,11 @@ export async function POST({ request }) { .select(); // makes Supabase return inserted rows if (error) { - console.error('Error inserting folder into Supabase:', error); + logger.error('Error inserting folder into Supabase:', error); throw new Error(error.message); } - console.log('Folder successfully inserted into Supabase'); + logger.debug('Folder successfully inserted into Supabase'); return new Response( JSON.stringify({ message: 'Folder successfully inserted into Supabase', @@ -139,14 +138,14 @@ export async function POST({ request }) { }, ); } catch (supabaseError) { - console.error('Error inserting to Supabase:', supabaseError); + logger.error('Error inserting to Supabase:', supabaseError); return new Response(JSON.stringify({ error: 'Error inserting into Supabase' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } } catch (err) { - console.error('Unexpected Error: ', err); + logger.error('Unexpected Error: ', err); return new Response(JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, diff --git a/src/routes/api/upload/+server.js b/src/routes/api/upload/+server.js index 0812dec..cce75ad 100644 --- a/src/routes/api/upload/+server.js +++ b/src/routes/api/upload/+server.js @@ -1,20 +1,23 @@ -import { PUBLIC_GOOGLE_PRIVATE_KEY, PUBLIC_GOOGLE_SERVICE_EMAIL } from '$env/static/public'; +import { GOOGLE_PRIVATE_KEY, GOOGLE_SERVICE_EMAIL } from '$env/static/private'; import { Readable } from 'stream'; import { google } from 'googleapis'; -import { supabase } from '../../../lib/supabaseClient'; +import { logger } from '$lib/logger'; -export async function POST({ request }) { - console.log('Received POST request at /api/upload'); - - // Add debugging logs to verify environment variables - console.log('Google API Credentials:', { - client_email: PUBLIC_GOOGLE_SERVICE_EMAIL, - private_key: PUBLIC_GOOGLE_PRIVATE_KEY ? 'Provided' : 'Not Provided', - }); +/** @type {import('./$types').RequestHandler} */ +export async function POST({ request, locals }) { + logger.debug('Received POST request at /api/upload'); try { + const { user } = await locals.safeGetSession(); + if (!user) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + const uuid = user.id; const formData = await request.formData(); - const uuid = formData.get('uuid'); + const { supabase } = locals; const username = formData.get('username'); const gdrive_folder_id = formData.get('gdrive_folder_id'); const member_id = formData.get('member_id'); @@ -23,11 +26,10 @@ export async function POST({ request }) { const answer = formData.get('answer'); const imageFile = formData.get('image'); - // Add debugging logs to verify folder permissions - console.log('Google Drive Folder ID:', gdrive_folder_id); + logger.debug('Google Drive Folder ID:', gdrive_folder_id); if (!question || !answer || !imageFile || !(imageFile instanceof File)) { - console.error('Validation error: Missing or invalid required fields'); + logger.error('Validation error: Missing or invalid required fields'); return new Response(JSON.stringify({ error: 'Missing or invalid required fields' }), { status: 400, headers: { 'Content-Type': 'application/json' }, @@ -41,8 +43,8 @@ export async function POST({ request }) { // Authenticate with Google Drive API const auth = new google.auth.GoogleAuth({ credentials: { - client_email: PUBLIC_GOOGLE_SERVICE_EMAIL, - private_key: PUBLIC_GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'), + client_email: GOOGLE_SERVICE_EMAIL, + private_key: GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'), }, scopes: ['https://www.googleapis.com/auth/drive.file'], }); @@ -64,41 +66,17 @@ export async function POST({ request }) { fileUrl = null; // Initialize variables try { - console.log('Uploading file to Google Drive with metadata:', fileMetadata); - console.log('File mimeType:', media.mimeType); - - console.log('driveResponse start'); + logger.debug('Uploading file to Google Drive with metadata:', fileMetadata); const driveResponse = await drive.files.create({ requestBody: fileMetadata, media: media, fields: 'id', }); - console.log('driveResponse success'); fileId = driveResponse.data.id; fileUrl = `https://drive.google.com/uc?id=${fileId}`; - console.log('File uploaded successfully. File ID:', fileId); + logger.debug('File uploaded successfully. File ID:', fileId); } catch (driveError) { - console.error('Error uploading to Google Drive:', { - message: driveError instanceof Error ? driveError.message : 'Unknown error', - errors: - driveError instanceof Error && 'errors' in driveError - ? driveError.errors - : 'No additional error details', - stack: driveError instanceof Error ? driveError.stack : 'No stack trace available', - }); - - // Additional debugging for credentials and folder permissions - console.error('Google Drive API credentials:', { - client_email: PUBLIC_GOOGLE_SERVICE_EMAIL, - private_key: PUBLIC_GOOGLE_PRIVATE_KEY ? 'Provided' : 'Not Provided', - }); - console.error('Folder ID:', fileMetadata.parents); - - // Log the media object details - console.error('Media object details:', { - mimeType: media.mimeType, - bodyType: typeof media.body, - }); + logger.error('Google Drive upload failed:', driveError instanceof Error ? driveError.message : driveError); return new Response( JSON.stringify({ error: 'Error uploading to Google Drive. Please check logs for details.' }), @@ -110,15 +88,7 @@ export async function POST({ request }) { } // Save data to Supabase - console.log('Starting to save data to Supabase with the following details:', { - question, - answer, - image_url: fileUrl, // Use the correct file URL - applicant_id: uuid, - member_id, - member_name, - }); - + logger.debug('Saving data to Supabase for applicant:', uuid); try { const { data, error } = await supabase .from('sigsheet') // table name in supabase @@ -135,32 +105,24 @@ export async function POST({ request }) { if (error.message.includes('unique_applicantid_signatoryname_pair')) { throw new Error("You have already have this co-applicant's signature. Try someone else"); } - console.error('Error inserting into Supabase:', error); + logger.error('Error inserting into Supabase:', error); throw new Error(error.message); } - console.log('Data successfully saved to Supabase:', { - question, - answer, - image_url: fileUrl, - applicant_id: uuid, - member_id, - member_name, - }); - + logger.debug('Data successfully saved to Supabase'); return new Response(JSON.stringify({ message: 'Data saved successfully', data }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } catch (supabaseError) { - console.error('Error saving to Supabase:', supabaseError); + logger.error('Error saving to Supabase:', supabaseError); return new Response(JSON.stringify({ error: `${supabaseError}` }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } } catch (err) { - console.error('Unexpected error:', err); + logger.error('Unexpected error:', err); return new Response(JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, diff --git a/src/routes/consti-quiz/+page.server.js b/src/routes/consti-quiz/+page.server.js index bd2afb9..0470df4 100644 --- a/src/routes/consti-quiz/+page.server.js +++ b/src/routes/consti-quiz/+page.server.js @@ -1,3 +1,5 @@ +import { logger } from '$lib/logger'; + export async function load({ locals }) { const { supabase } = locals; const { user } = await locals.safeGetSession(); @@ -12,11 +14,11 @@ export async function load({ locals }) { ]); if (submission.error) { - console.error(submission.error); + logger.error(submission.error); } if (availability.error) { - console.error(availability.error); + logger.error(availability.error); } let isOpen = false; diff --git a/src/routes/consti-quiz/+page.svelte b/src/routes/consti-quiz/+page.svelte index a0e8175..2293d9e 100644 --- a/src/routes/consti-quiz/+page.svelte +++ b/src/routes/consti-quiz/+page.svelte @@ -10,11 +10,13 @@ import Section from './Section.svelte'; import SectionNav from './SectionNav.svelte'; import ShortTextQuestion from './ShortTextQuestion.svelte'; + import { logger } from '$lib/logger'; import { browser } from '$app/environment'; import { onMount } from 'svelte'; const { data } = $props(); + // Data loaded once per page visit — intentionally not reactive const { user, sections, questions, answers, hasSubmitted, isOpen } = data; // NOTE: do we even this need this part @@ -76,11 +78,6 @@ // subtract 10 from bonus const totalPoints = 88; - // NOTE: for debugging purposes only, remove during production - console.log('sections:', sections); - // NOTE: for debugging purposes only, remove during production - console.log('questions:', questions); - // ensure that questions are sorted by ids, since their index is important questions!.sort((a, b) => a.question_id - b.question_id); @@ -155,7 +152,7 @@ return json; } catch (error) { - console.error(error); + logger.error(error); } } @@ -192,7 +189,7 @@ return await res.json(); } catch (err) { - console.error(err); + logger.error(err); } } @@ -265,7 +262,7 @@
{#each rearrangedSections! as { section_id, title, points } (section_id)}
- {#each questions!.filter(question => question.section.title === title) as question, i} + {#each questions!.filter(question => question.section.title === title) as question, i (question.question_id)} {#if question.type === 'long_text'} v !== option); value = valueList.join('-'); - console.log(valueList); } function isSelected(option: string) { diff --git a/src/routes/consti-quiz/SaveButton.svelte b/src/routes/consti-quiz/SaveButton.svelte index c26a074..95e71a3 100644 --- a/src/routes/consti-quiz/SaveButton.svelte +++ b/src/routes/consti-quiz/SaveButton.svelte @@ -1,5 +1,6 @@
= { @@ -90,11 +88,12 @@
- {#each categories as category} + {#each categories as category (category)}