From ed0d02959a97db10bb53e24f039b1e3944131104 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:34:16 +0300 Subject: [PATCH] wire frontend email signup --- frontend/website/src/router/index.ts | 26 +++++ frontend/website/src/stores/auth.ts | 91 +++++++++++++++- frontend/website/src/views/CheckEmailView.vue | 39 +++++++ .../website/src/views/ConfirmEmailView.vue | 100 ++++++++++++++++++ frontend/website/src/views/SignupView.vue | 65 ++++++++---- 5 files changed, 297 insertions(+), 24 deletions(-) create mode 100644 frontend/website/src/views/CheckEmailView.vue create mode 100644 frontend/website/src/views/ConfirmEmailView.vue diff --git a/frontend/website/src/router/index.ts b/frontend/website/src/router/index.ts index 66d053a..c55b993 100644 --- a/frontend/website/src/router/index.ts +++ b/frontend/website/src/router/index.ts @@ -29,6 +29,32 @@ const router = createRouter({ guestOnly: true, }, }, + { + path: '/check-email', + name: 'check-email', + component: () => import('@/views/CheckEmailView.vue'), + meta: { + guestOnly: true, + }, + beforeEnter: () => { + if (!useAuthStore().signupCompleted) { + return { name: 'signup' } + } + }, + }, + { + path: '/confirm-email', + name: 'confirm-email', + component: () => import('@/views/ConfirmEmailView.vue'), + meta: { + guestOnly: true, + }, + beforeEnter: (to) => { + if (typeof to.query.token !== 'string' || to.query.token === '') { + return { name: 'signup' } + } + }, + }, { path: '/dashboard', name: 'dashboard', diff --git a/frontend/website/src/stores/auth.ts b/frontend/website/src/stores/auth.ts index fc9eef3..b834955 100644 --- a/frontend/website/src/stores/auth.ts +++ b/frontend/website/src/stores/auth.ts @@ -13,17 +13,20 @@ const meResponseSchema = z.object({ user: authUserSchema, }) -const loginErrorResponseSchema = z.object({ +const authErrorResponseSchema = z.object({ error: z.string(), }) export type AuthUser = z.infer export type LoginFieldErrors = Partial> +export type SignupFieldErrors = Partial> +export type ConfirmEmailFieldErrors = Partial> export const useAuthStore = defineStore('auth', () => { const user = ref(null) const loading = ref(false) const error = ref(null) + const signupCompleted = ref(false) const isAuthenticated = computed(() => user.value !== null) async function fetchMe(): Promise { @@ -85,7 +88,7 @@ export const useAuthStore = defineStore('auth', () => { } user.value = null - const errorResponse = loginErrorResponseSchema.safeParse(responseBody) + const errorResponse = authErrorResponseSchema.safeParse(responseBody) error.value = errorResponse.success ? errorResponse.data.error : 'Unable to log in. Please try again.' @@ -101,6 +104,76 @@ export const useAuthStore = defineStore('auth', () => { } } + async function signup(email: string): Promise { + loading.value = true + error.value = null + signupCompleted.value = false + + try { + const response = await fetch(`${API_BASE}/api/signup`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }) + + if (response.status === 201) { + signupCompleted.value = true + + return true + } + + error.value = await responseError(response, 'Unable to sign up. Please try again.') + + return false + } catch { + error.value = 'Unable to sign up. Please try again.' + + return false + } finally { + loading.value = false + } + } + + async function confirmEmail(token: string, password: string): Promise { + loading.value = true + error.value = null + + try { + const response = await fetch(`${API_BASE}/api/confirm-email`, { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token, password }), + }) + + if (response.status === 200) { + const responseBody: unknown = await response.json() + user.value = meResponseSchema.parse(responseBody).user + signupCompleted.value = false + + return true + } + + user.value = null + error.value = await responseError(response, 'Unable to confirm your email. Please try again.') + + return false + } catch { + user.value = null + error.value = 'Unable to confirm your email. Please try again.' + + return false + } finally { + loading.value = false + } + } + async function logout(): Promise { try { await fetch(`${API_BASE}/api/logout`, { @@ -119,9 +192,23 @@ export const useAuthStore = defineStore('auth', () => { user, loading, error, + signupCompleted, isAuthenticated, fetchMe, login, + signup, + confirmEmail, logout, } }) + +async function responseError(response: Response, fallback: string): Promise { + try { + const responseBody: unknown = await response.json() + const parsedError = authErrorResponseSchema.safeParse(responseBody) + + return parsedError.success ? parsedError.data.error : fallback + } catch { + return fallback + } +} diff --git a/frontend/website/src/views/CheckEmailView.vue b/frontend/website/src/views/CheckEmailView.vue new file mode 100644 index 0000000..dc7d672 --- /dev/null +++ b/frontend/website/src/views/CheckEmailView.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/frontend/website/src/views/ConfirmEmailView.vue b/frontend/website/src/views/ConfirmEmailView.vue new file mode 100644 index 0000000..13966dc --- /dev/null +++ b/frontend/website/src/views/ConfirmEmailView.vue @@ -0,0 +1,100 @@ + + + diff --git a/frontend/website/src/views/SignupView.vue b/frontend/website/src/views/SignupView.vue index 123bf72..9ba636d 100644 --- a/frontend/website/src/views/SignupView.vue +++ b/frontend/website/src/views/SignupView.vue @@ -1,7 +1,40 @@