From 57ecd9fa2928dbd0849a76f6378aff10e0be9d7a Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Tue, 7 Apr 2026 00:49:05 +0900 Subject: [PATCH] adding password upate route --- messages/en.json | 5 + messages/ja.json | 5 + src/routes/auth/confirm/+server.ts | 33 ++++ .../auth/update-password/+page.server.ts | 58 ++++++ src/routes/auth/update-password/+page.svelte | 173 ++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 src/routes/auth/confirm/+server.ts create mode 100644 src/routes/auth/update-password/+page.server.ts create mode 100644 src/routes/auth/update-password/+page.svelte diff --git a/messages/en.json b/messages/en.json index d22b5c39..2a0b7645 100644 --- a/messages/en.json +++ b/messages/en.json @@ -204,6 +204,11 @@ "auth_send_reset_link": "Send Reset Link", "auth_sending": "Sending…", "auth_enter_email": "Please enter your email address.", + "auth_update_password_page_title": "Update Password - CropWatch", + "auth_update_password_heading": "Set New Password", + "auth_update_password_subtitle": "Enter your new password below.", + "auth_update_password_confirm_label": "CONFIRM PASSWORD", + "auth_update_password_submit": "Update Password", "locations_all_title": "All Locations", "locations_all_subtitle": "Aggregated from current device telemetry", "locations_add_location": "Add Location", diff --git a/messages/ja.json b/messages/ja.json index 1c6909fa..a3ce2e24 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -204,6 +204,11 @@ "auth_send_reset_link": "再設定リンクを送信", "auth_sending": "送信中…", "auth_enter_email": "メールアドレスを入力してください。", + "auth_update_password_page_title": "パスワード更新 - CropWatch", + "auth_update_password_heading": "新しいパスワードを設定", + "auth_update_password_subtitle": "新しいパスワードを入力してください。", + "auth_update_password_confirm_label": "パスワード確認", + "auth_update_password_submit": "パスワードを更新", "locations_all_title": "すべてのロケーション", "locations_all_subtitle": "現在のデバイステレメトリから集計", "locations_add_location": "ロケーションを追加", diff --git a/src/routes/auth/confirm/+server.ts b/src/routes/auth/confirm/+server.ts new file mode 100644 index 00000000..1cb6f9cd --- /dev/null +++ b/src/routes/auth/confirm/+server.ts @@ -0,0 +1,33 @@ +import type { EmailOtpType } from '@supabase/supabase-js'; +import { redirect } from '@sveltejs/kit'; +import { getSupabaseClient } from '$lib/supabase.server'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url, cookies }) => { + const token_hash = url.searchParams.get('token_hash'); + const type = url.searchParams.get('type') as EmailOtpType | null; + const next = url.searchParams.get('next') ?? '/auth/login'; + + if (token_hash && type) { + const supabase = getSupabaseClient(); + const { data, error } = await supabase.auth.verifyOtp({ type, token_hash }); + + if (!error && data.session) { + // For recovery flow, store the access token so the update-password page can use it + if (type === 'recovery') { + cookies.set('supabase_recovery_token', data.session.access_token, { + path: '/', + httpOnly: true, + secure: url.protocol === 'https:', + sameSite: 'lax', + maxAge: 600 // 10 minutes + }); + } + + redirect(303, next); + } + } + + // Token invalid or missing — send to an error-aware login page + redirect(303, '/auth/login?reason=auth-required'); +}; diff --git a/src/routes/auth/update-password/+page.server.ts b/src/routes/auth/update-password/+page.server.ts new file mode 100644 index 00000000..af0cb759 --- /dev/null +++ b/src/routes/auth/update-password/+page.server.ts @@ -0,0 +1,58 @@ +import { fail, redirect, type Actions } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { createClient } from '@supabase/supabase-js'; +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; + +export const load: PageServerLoad = async ({ cookies }) => { + const token = cookies.get('supabase_recovery_token'); + if (!token) { + redirect(303, '/auth/login?reason=auth-required'); + } + return {}; +}; + +export const actions: Actions = { + default: async ({ request, cookies }) => { + const token = cookies.get('supabase_recovery_token'); + if (!token) { + redirect(303, '/auth/login?reason=auth-required'); + } + + const data = await request.formData(); + const password = data.get('password') as string; + const confirmPassword = data.get('confirmPassword') as string; + + if (!password || password.length < 6) { + return fail(400, { message: 'Password must be at least 6 characters.' }); + } + + if (password !== confirmPassword) { + return fail(400, { message: 'Passwords do not match.' }); + } + + // Create a Supabase client authenticated with the recovery access token + const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { + auth: { + autoRefreshToken: false, + persistSession: false + }, + global: { + headers: { + Authorization: `Bearer ${token}` + } + } + }); + + const { error } = await supabase.auth.updateUser({ password }); + + if (error) { + console.error('Password update error:', error); + return fail(500, { message: 'Failed to update password. The link may have expired.' }); + } + + // Clear the recovery token cookie + cookies.delete('supabase_recovery_token', { path: '/' }); + + redirect(303, '/auth/login?reason=password-reset'); + } +}; diff --git a/src/routes/auth/update-password/+page.svelte b/src/routes/auth/update-password/+page.svelte new file mode 100644 index 00000000..684245a9 --- /dev/null +++ b/src/routes/auth/update-password/+page.svelte @@ -0,0 +1,173 @@ + + + + {m.auth_update_password_page_title()} + + + +
+
+ {m.app_name()} +
+ +

{m.auth_update_password_heading()}

+

{m.auth_update_password_subtitle()}

+ +
{ + if (submitting) return; + submitting = true; + + return async ({ result }) => { + if (result.type === 'failure' && typeof result.data?.message === 'string') { + toast.add({ message: result.data.message, tone: 'danger' }); + } + submitting = false; + await applyAction(result); + }; + }} + > + + + {#if password.length > 0} +
+

{m.auth_password_requirements()}

+
    +
  • {m.auth_password_requirement_length()}
  • +
  • {m.auth_password_requirement_lowercase()}
  • +
  • {m.auth_password_requirement_uppercase()}
  • +
  • {m.auth_password_requirement_number()}
  • +
  • {m.auth_password_requirement_symbol()}
  • +
+
+ {/if} + + + + {#if passwordsMatch} +

{m.auth_passwords_match()}

+ {:else if passwordsMismatch} +

{m.auth_passwords_do_not_match()}

+ {/if} + + + + {submitting ? m.auth_sending() : m.auth_update_password_submit()} + +
+ + + + {m.auth_back_to_login()} + +
+
+ +