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)}