From 3d07a4ed80fa694cfc929550382b5329669b759a Mon Sep 17 00:00:00 2001 From: JoachimLK Date: Mon, 13 Apr 2026 20:20:09 +0200 Subject: [PATCH] feat: implement applicant portal schema and authentication utilities - Added schema for applicant portal including tables for tokens, accounts, and sessions. - Implemented functions for generating and validating portal tokens. - Created utilities for managing applicant sessions and accounts via Google OAuth. - Developed dashboard functionality to fetch application data for candidates. - Included helpers for formatting job types and interview details. --- .env.example | 7 +- app/components/portal/InterviewCard.vue | 95 + app/components/portal/PipelineProgress.vue | 152 + app/components/portal/StatusBadge.vue | 50 + app/components/portal/StatusTimeline.vue | 111 + app/layouts/portal.vue | 46 + app/pages/jobs/[slug]/apply.vue | 14 +- app/pages/jobs/[slug]/confirmation.vue | 58 +- app/pages/portal/applications/[id].vue | 223 + app/pages/portal/auth/sign-in.vue | 95 + app/pages/portal/index.vue | 217 + app/pages/portal/t/[token].vue | 385 ++ server/api/applications/[id].get.ts | 11 +- server/api/portal/applications/[id].get.ts | 58 + server/api/portal/auth/google/callback.get.ts | 105 + server/api/portal/auth/google/index.get.ts | 47 + server/api/portal/auth/session.get.ts | 33 + server/api/portal/auth/sign-out.post.ts | 21 + server/api/portal/dashboard.get.ts | 34 + server/api/portal/token/[token].get.ts | 50 + server/api/public/jobs/[slug]/apply.post.ts | 24 +- .../migrations/0021_blushing_thing.sql | 44 + .../migrations/0022_mute_green_goblin.sql | 3 + .../migrations/meta/0021_snapshot.json | 4505 ++++++++++++++++ .../migrations/meta/0022_snapshot.json | 4530 +++++++++++++++++ server/database/migrations/meta/_journal.json | 14 + server/database/schema/app.ts | 2 + server/database/schema/index.ts | 1 + server/database/schema/portal.ts | 87 + server/utils/portal-auth.ts | 150 + server/utils/portal-dashboard.ts | 399 ++ 31 files changed, 11564 insertions(+), 7 deletions(-) create mode 100644 app/components/portal/InterviewCard.vue create mode 100644 app/components/portal/PipelineProgress.vue create mode 100644 app/components/portal/StatusBadge.vue create mode 100644 app/components/portal/StatusTimeline.vue create mode 100644 app/layouts/portal.vue create mode 100644 app/pages/portal/applications/[id].vue create mode 100644 app/pages/portal/auth/sign-in.vue create mode 100644 app/pages/portal/index.vue create mode 100644 app/pages/portal/t/[token].vue create mode 100644 server/api/portal/applications/[id].get.ts create mode 100644 server/api/portal/auth/google/callback.get.ts create mode 100644 server/api/portal/auth/google/index.get.ts create mode 100644 server/api/portal/auth/session.get.ts create mode 100644 server/api/portal/auth/sign-out.post.ts create mode 100644 server/api/portal/dashboard.get.ts create mode 100644 server/api/portal/token/[token].get.ts create mode 100644 server/database/migrations/0021_blushing_thing.sql create mode 100644 server/database/migrations/0022_mute_green_goblin.sql create mode 100644 server/database/migrations/meta/0021_snapshot.json create mode 100644 server/database/migrations/meta/0022_snapshot.json create mode 100644 server/database/schema/portal.ts create mode 100644 server/utils/portal-auth.ts create mode 100644 server/utils/portal-dashboard.ts diff --git a/.env.example b/.env.example index 2f3a864c..bd7a4d79 100644 --- a/.env.example +++ b/.env.example @@ -103,7 +103,12 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000 # When configured, "Continue with " buttons appear on the auth pages. # Google — Create credentials at https://console.cloud.google.com/apis/credentials -# Redirect URI: https://yourdomain.com/api/auth/callback/google +# Redirect URIs (add BOTH): +# https://yourdomain.com/api/auth/callback/google +# https://yourdomain.com/api/portal/auth/google/callback +# For local dev, also add: +# http://localhost:3000/api/auth/callback/google +# http://localhost:3000/api/portal/auth/google/callback # AUTH_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com # AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-google-client-secret diff --git a/app/components/portal/InterviewCard.vue b/app/components/portal/InterviewCard.vue new file mode 100644 index 00000000..7d198467 --- /dev/null +++ b/app/components/portal/InterviewCard.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/components/portal/PipelineProgress.vue b/app/components/portal/PipelineProgress.vue new file mode 100644 index 00000000..a239de0b --- /dev/null +++ b/app/components/portal/PipelineProgress.vue @@ -0,0 +1,152 @@ + + + diff --git a/app/components/portal/StatusBadge.vue b/app/components/portal/StatusBadge.vue new file mode 100644 index 00000000..0ee38ff5 --- /dev/null +++ b/app/components/portal/StatusBadge.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/components/portal/StatusTimeline.vue b/app/components/portal/StatusTimeline.vue new file mode 100644 index 00000000..99ef2ee6 --- /dev/null +++ b/app/components/portal/StatusTimeline.vue @@ -0,0 +1,111 @@ + + + diff --git a/app/layouts/portal.vue b/app/layouts/portal.vue new file mode 100644 index 00000000..15c55814 --- /dev/null +++ b/app/layouts/portal.vue @@ -0,0 +1,46 @@ + + + diff --git a/app/pages/jobs/[slug]/apply.vue b/app/pages/jobs/[slug]/apply.vue index 9050f133..8358087b 100644 --- a/app/pages/jobs/[slug]/apply.vue +++ b/app/pages/jobs/[slug]/apply.vue @@ -163,6 +163,8 @@ async function handleSubmit() { const hasAnyFiles = Object.keys(fileUploads.value).length > 0 || !!resumeFile.value + let result: { success: boolean; portalToken?: string | null } | undefined + if (hasAnyFiles) { // Use FormData when files are present const formData = new FormData() @@ -201,13 +203,13 @@ async function handleSubmit() { if (utmTerm) formData.append('utmTerm', utmTerm) if (utmContent) formData.append('utmContent', utmContent) - await $fetch(`/api/public/jobs/${jobSlug}/apply`, { + result = await $fetch<{ success: boolean; portalToken?: string | null }>(`/api/public/jobs/${jobSlug}/apply`, { method: 'POST', body: formData, }) } else { // No files — use JSON as before - await $fetch(`/api/public/jobs/${jobSlug}/apply`, { + result = await $fetch<{ success: boolean; portalToken?: string | null }>(`/api/public/jobs/${jobSlug}/apply`, { method: 'POST', body: { firstName: form.value.firstName.trim(), @@ -228,7 +230,13 @@ async function handleSubmit() { } track('application_submitted', { slug: jobSlug }) - await navigateTo(`/jobs/${jobSlug}/confirmation`) + // Redirect directly to the portal dashboard if we have a token, + // otherwise fall back to the confirmation page + if (result?.portalToken) { + await navigateTo(`/portal/t/${result.portalToken}?fresh=1`) + } else { + await navigateTo({ path: `/jobs/${jobSlug}/confirmation` }) + } } catch (err: any) { const message = err.data?.statusMessage ?? 'Something went wrong. Please try again.' submitError.value = message diff --git a/app/pages/jobs/[slug]/confirmation.vue b/app/pages/jobs/[slug]/confirmation.vue index 329443a3..7e67760d 100644 --- a/app/pages/jobs/[slug]/confirmation.vue +++ b/app/pages/jobs/[slug]/confirmation.vue @@ -1,5 +1,5 @@