From 2ccbf4be830b28cbb698a4fcd4455ba4e2da0b3e Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 12 Dec 2024 11:01:09 -0500 Subject: [PATCH] Update email verification and password reset flows; refactor Docker Compose and enhance email management --- backend/server/main/settings.py | 14 ++-- docker-compose.yml | 2 +- frontend/pnpm-lock.yaml | 10 +-- .../src/routes/_allauth/[...path]/+server.ts | 4 +- frontend/src/routes/settings/+page.svelte | 35 +++++++++- .../forgot-password/confirm/+page.server.ts | 57 ----------------- .../forgot-password/confirm/+page.svelte | 64 ------------------- .../reset-password}/+page.server.ts | 0 .../reset-password}/+page.svelte | 0 .../user/reset-password/[key]/+page.server.ts | 55 ++++++++++++++++ .../user/reset-password/[key]/+page.svelte | 47 ++++++++++++++ .../user/verify-email/[key]/+page.server.ts | 33 ++++++++++ .../user/verify-email/[key]/+page.svelte | 13 ++++ 13 files changed, 197 insertions(+), 137 deletions(-) delete mode 100644 frontend/src/routes/settings/forgot-password/confirm/+page.server.ts delete mode 100644 frontend/src/routes/settings/forgot-password/confirm/+page.svelte rename frontend/src/routes/{settings/forgot-password => user/reset-password}/+page.server.ts (100%) rename frontend/src/routes/{settings/forgot-password => user/reset-password}/+page.svelte (100%) create mode 100644 frontend/src/routes/user/reset-password/[key]/+page.server.ts create mode 100644 frontend/src/routes/user/reset-password/[key]/+page.svelte create mode 100644 frontend/src/routes/user/verify-email/[key]/+page.server.ts create mode 100644 frontend/src/routes/user/verify-email/[key]/+page.svelte diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 23d091f..2ceae08 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -178,11 +178,15 @@ SESSION_SAVE_EVERY_REQUEST = True FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000') -# HEADLESS_FRONTEND_URLS = { -# "account_confirm_email": "https://app.project.org/account/verify-email/{key}", -# "account_reset_password_from_key": "https://app.org/account/password/reset/key/{key}", -# "account_signup": "https://app.org/account/signup", -# } +HEADLESS_FRONTEND_URLS = { + "account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}", + "account_reset_password": f"{FRONTEND_URL}/user/reset-password", + "account_reset_password_from_key": f"{FRONTEND_URL}/user/reset-password/{{key}}", + "account_signup": f"{FRONTEND_URL}/signup", + # Fallback in case the state containing the `next` URL is lost and the handshake + # with the third-party provider fails. + "socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback", +} EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SITE_ID = 1 diff --git a/docker-compose.yml b/docker-compose.yml index d2ee82a..6ec6bba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: - server db: - image: postgis/postgis:16-3.4 + image: postgis/postgis:15-3.3 container_name: adventurelog-db restart: unless-stopped environment: diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ec7917f..7783f84 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -831,8 +831,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001636: - resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} + caniuse-lite@1.0.30001688: + resolution: {integrity: sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -2610,7 +2610,7 @@ snapshots: autoprefixer@10.4.19(postcss@8.4.38): dependencies: browserslist: 4.23.1 - caniuse-lite: 1.0.30001636 + caniuse-lite: 1.0.30001688 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 @@ -2644,7 +2644,7 @@ snapshots: browserslist@4.23.1: dependencies: - caniuse-lite: 1.0.30001636 + caniuse-lite: 1.0.30001688 electron-to-chromium: 1.4.810 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) @@ -2668,7 +2668,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001636: {} + caniuse-lite@1.0.30001688: {} chokidar@3.6.0: dependencies: diff --git a/frontend/src/routes/_allauth/[...path]/+server.ts b/frontend/src/routes/_allauth/[...path]/+server.ts index 92071cb..681a3fa 100644 --- a/frontend/src/routes/_allauth/[...path]/+server.ts +++ b/frontend/src/routes/_allauth/[...path]/+server.ts @@ -17,8 +17,8 @@ export async function POST({ url, params, request, fetch, cookies }) { } export async function PATCH({ url, params, request, fetch, cookies }) { - const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; - return handleRequest(url, params, request, fetch, cookies, searchParam, true); + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); } export async function PUT({ url, params, request, fetch, cookies }) { diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 6b55c8c..5224ab8 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -100,6 +100,30 @@ addToast('error', 'Error adding email'); } } + + async function primaryEmail(email: { email: any; verified?: boolean; primary?: boolean }) { + let res = await fetch('/_allauth/browser/v1/account/email/', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: email.email, primary: true }) + }); + if (res.ok) { + addToast('success', 'Email set as primary'); + // remove primary from all other emails and set this one as primary + emails = emails.map((e) => { + if (e.email === email.email) { + e.primary = true; + } else { + e.primary = false; + } + return e; + }); + } else { + addToast('error', 'Error setting email as primary'); + } + }

{$t('settings.settings_page')}

@@ -225,14 +249,19 @@ {#if email.primary}
Primary
{/if} - {#if !email.verified} {/if} + {#if !email.primary} + + {/if} +

{/each} {#if emails.length === 0} diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts b/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts deleted file mode 100644 index 9aeca23..0000000 --- a/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { fail, redirect, type Actions } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; -const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; -const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; - -export const load = (async (event) => { - const token = event.url.searchParams.get('token'); - const uid = event.url.searchParams.get('uid'); - - return { - props: { - token, - uid - } - }; -}) satisfies PageServerLoad; - -export const actions: Actions = { - reset: async (event) => { - const formData = await event.request.formData(); - - const new_password1 = formData.get('new_password1') as string; - const new_password2 = formData.get('new_password2') as string; - const token = formData.get('token') as string; - const uid = formData.get('uid') as string; - - if (!new_password1 || !new_password2) { - return fail(400, { message: 'settings.password_is_required' }); - } - - if (new_password1 !== new_password2) { - return fail(400, { message: 'settings.password_does_not_match' }); - } - - if (!token || !uid) { - return redirect(302, '/settings/forgot-password'); - } else { - let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - token: token, - uid: uid, - new_password1, - new_password2 - }) - }); - if (!response.ok) { - return fail(response.status, { message: 'settings.invalid_token' }); - } else { - return redirect(302, '/login'); - } - } - } -}; diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte b/frontend/src/routes/settings/forgot-password/confirm/+page.svelte deleted file mode 100644 index 06bc156..0000000 --- a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -

{$t('settings.change_password')}

- -{#if data.props.token && data.props.uid} -

{$t('settings.login_redir')}

- -{:else} -
-
-

{$t('settings.token_required')}

- - -
-
-{/if} - - - Password Reset Confirm - - diff --git a/frontend/src/routes/settings/forgot-password/+page.server.ts b/frontend/src/routes/user/reset-password/+page.server.ts similarity index 100% rename from frontend/src/routes/settings/forgot-password/+page.server.ts rename to frontend/src/routes/user/reset-password/+page.server.ts diff --git a/frontend/src/routes/settings/forgot-password/+page.svelte b/frontend/src/routes/user/reset-password/+page.svelte similarity index 100% rename from frontend/src/routes/settings/forgot-password/+page.svelte rename to frontend/src/routes/user/reset-password/+page.svelte diff --git a/frontend/src/routes/user/reset-password/[key]/+page.server.ts b/frontend/src/routes/user/reset-password/[key]/+page.server.ts new file mode 100644 index 0000000..951310c --- /dev/null +++ b/frontend/src/routes/user/reset-password/[key]/+page.server.ts @@ -0,0 +1,55 @@ +import { fail, redirect } from '@sveltejs/kit'; +import { fetchCSRFToken } from '$lib/index.server'; +import type { PageServerLoad, Actions } from './$types'; + +export const load = (async ({ params }) => { + const key = params.key; + if (!key) { + throw redirect(302, '/'); + } + return { key }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const password = formData.get('password'); + const confirm_password = formData.get('confirm_password'); + const key = event.params.key; + + if (!password || !confirm_password) { + return fail(400, { message: 'both_passwords_required' }); + } + + if (password !== confirm_password) { + return fail(400, { message: 'passwords_not_match' }); + } + + const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; + const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + const csrfToken = await fetchCSRFToken(); + + const response = await event.fetch( + `${serverEndpoint}/_allauth/browser/v1/auth/password/reset`, + { + headers: { + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + method: 'POST', + credentials: 'include', + body: JSON.stringify({ key: key, password: password }) + } + ); + + if (response.status !== 401) { + const error_message = await response.json(); + console.error(error_message); + console.log(response); + return fail(response.status, { message: 'reset_failed' }); + } + + return redirect(302, '/login'); + } +}; diff --git a/frontend/src/routes/user/reset-password/[key]/+page.svelte b/frontend/src/routes/user/reset-password/[key]/+page.svelte new file mode 100644 index 0000000..6772f6f --- /dev/null +++ b/frontend/src/routes/user/reset-password/[key]/+page.svelte @@ -0,0 +1,47 @@ + + +

{$t('settings.change_password')}

+ +
+
+ + +
+
+ + +
+ + + {#if $page.form?.message} +
+ {$page.form?.message} +
+ {/if} +
+ + + Password Reset Confirm + + diff --git a/frontend/src/routes/user/verify-email/[key]/+page.server.ts b/frontend/src/routes/user/verify-email/[key]/+page.server.ts new file mode 100644 index 0000000..86f82ee --- /dev/null +++ b/frontend/src/routes/user/verify-email/[key]/+page.server.ts @@ -0,0 +1,33 @@ +import { fetchCSRFToken } from '$lib/index.server'; +import type { PageServerLoad } from './$types'; + +export const load = (async (event) => { + // get key from route params + const key = event.params.key; + if (!key) { + return { status: 404 }; + } + const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; + const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + const csrfToken = await fetchCSRFToken(); + + let verifyFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/email/verify`, { + headers: { + Cookie: `csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + method: 'POST', + credentials: 'include', + + body: JSON.stringify({ key: key }) + }); + if (!verifyFetch.ok) { + let error_message = await verifyFetch.json(); + console.error(error_message); + console.error('Failed to verify email'); + return { status: 404 }; + } + return { + verified: true + }; +}) satisfies PageServerLoad; diff --git a/frontend/src/routes/user/verify-email/[key]/+page.svelte b/frontend/src/routes/user/verify-email/[key]/+page.svelte new file mode 100644 index 0000000..e17173f --- /dev/null +++ b/frontend/src/routes/user/verify-email/[key]/+page.svelte @@ -0,0 +1,13 @@ + + +{#if data.verified} +

Email verified

+

Your email has been verified. You can now log in.

+{:else} +

Email verification failed

+

Your email could not be verified. Please try again.

+{/if}