From d3c0b9a54ac6abc168b3c17a0c28c2bf146e1ab9 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:14:47 +0000 Subject: [PATCH 1/4] feat: add password reset flow - Add /reset-password page to request password reset email - Add /update-password page to set new password from reset link - Add "Forgot password?" link on login page - Uses Supabase Auth resetPasswordForEmail and updateUser APIs Co-authored-by: bitnimble --- src/app/_auth/login_signup.tsx | 1 + src/app/reset-password/page.tsx | 31 ++++++++++ .../reset-password/reset_password.module.css | 18 ++++++ src/app/reset-password/reset_password.tsx | 50 +++++++++++++++ .../reset_password_presenter.ts | 58 ++++++++++++++++++ src/app/update-password/page.tsx | 31 ++++++++++ .../update_password.module.css | 18 ++++++ src/app/update-password/update_password.tsx | 54 ++++++++++++++++ .../update_password_presenter.ts | 61 +++++++++++++++++++ src/utils/routes.ts | 4 ++ 10 files changed, 326 insertions(+) create mode 100644 src/app/reset-password/page.tsx create mode 100644 src/app/reset-password/reset_password.module.css create mode 100644 src/app/reset-password/reset_password.tsx create mode 100644 src/app/reset-password/reset_password_presenter.ts create mode 100644 src/app/update-password/page.tsx create mode 100644 src/app/update-password/update_password.module.css create mode 100644 src/app/update-password/update_password.tsx create mode 100644 src/app/update-password/update_password_presenter.ts diff --git a/src/app/_auth/login_signup.tsx b/src/app/_auth/login_signup.tsx index fa3272c..faa08b4 100644 --- a/src/app/_auth/login_signup.tsx +++ b/src/app/_auth/login_signup.tsx @@ -54,6 +54,7 @@ export const LoginSignup = observer( error={errors.get('password')} onSubmit={login} /> + Forgot password?
Signup instead diff --git a/src/app/reset-password/page.tsx b/src/app/reset-password/page.tsx new file mode 100644 index 0000000..f1ab3ff --- /dev/null +++ b/src/app/reset-password/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useApi } from 'app/api/api_provider'; +import { observer } from 'mobx-react'; +import React from 'react'; +import { ResetPassword } from './reset_password'; +import { ResetPasswordPresenter, ResetPasswordStore } from './reset_password_presenter'; + +function createResetPasswordPage(supabase: ReturnType['supabase']) { + const store = new ResetPasswordStore(); + const presenter = new ResetPasswordPresenter(supabase, store); + + return observer(() => { + return ( + + ); + }); +} + +export default () => { + const api = useApi(); + const [ResetPasswordPage] = React.useState(() => createResetPasswordPage(api.supabase)); + return ; +}; diff --git a/src/app/reset-password/reset_password.module.css b/src/app/reset-password/reset_password.module.css new file mode 100644 index 0000000..e801d93 --- /dev/null +++ b/src/app/reset-password/reset_password.module.css @@ -0,0 +1,18 @@ +.resetPassword { + display: flex; + gap: var(--gridBaseline); + margin: auto; + flex-direction: column; + justify-content: center; + height: 100%; + width: calc(var(--gridBaseline) * 35); + max-width: 90%; +} + +.submitContainer { + display: flex; + width: 100%; + margin-top: var(--gridBaseline); + align-items: center; + justify-content: space-between; +} diff --git a/src/app/reset-password/reset_password.tsx b/src/app/reset-password/reset_password.tsx new file mode 100644 index 0000000..3a5f526 --- /dev/null +++ b/src/app/reset-password/reset_password.tsx @@ -0,0 +1,50 @@ +import { observer } from 'mobx-react'; +import { ResetPasswordField } from './reset_password_presenter'; +import { FormError } from 'ui/base/form/form_error'; +import { RouteLink } from 'ui/base/text/link'; +import { Button } from 'ui/base/button/button'; +import { Textbox } from 'ui/base/textbox/textbox'; +import styles from './reset_password.module.css'; +import { routeFor, RoutePath } from 'utils/routes'; + +type ResetPasswordProps = { + email: string; + submitting: boolean; + success: boolean; + errors: Map; + onChangeEmail(value: string): void; + requestReset(): void; +}; + +export const ResetPassword = observer( + ({ email, submitting, success, errors, onChangeEmail, requestReset }: ResetPasswordProps) => { + if (success) { + return ( +
+

Check your email for a password reset link.

+ Back to login +
+ ); + } + + return ( +
+

Enter your email address and we'll send you a link to reset your password.

+ +
+ Back to login + +
+ +
+ ); + } +); diff --git a/src/app/reset-password/reset_password_presenter.ts b/src/app/reset-password/reset_password_presenter.ts new file mode 100644 index 0000000..f3e6ba6 --- /dev/null +++ b/src/app/reset-password/reset_password_presenter.ts @@ -0,0 +1,58 @@ +import { SupabaseClient } from '@supabase/supabase-js'; +import { action, makeObservable, observable, runInAction } from 'mobx'; +import { FormPresenter, FormStore } from 'ui/base/form/form_presenter'; +import { RoutePath, routeFor } from 'utils/routes'; + +export type ResetPasswordField = 'email' | 'form'; + +export class ResetPasswordStore extends FormStore { + submitting = false; + email = ''; + success = false; + + constructor() { + super(); + makeObservable(this, { + email: observable.ref, + submitting: observable.ref, + success: observable.ref, + }); + } +} + +export class ResetPasswordPresenter extends FormPresenter { + constructor( + private readonly supabase: SupabaseClient, + private readonly store: ResetPasswordStore + ) { + super(store); + makeObservable(this, { + onChangeEmail: action.bound, + }); + } + + onChangeEmail = (value: string) => (this.store.email = value); + + requestReset = async () => { + runInAction(() => this.clearErrors()); + const email = this.store.email; + const errors = [ + ...this.checkEmailFields(['email', email]), + ...this.checkRequiredFields(['email', email]), + ]; + if (errors.length) { + return; + } + + runInAction(() => (this.store.submitting = true)); + const redirectTo = `${window.location.origin}${routeFor([RoutePath.UPDATE_PASSWORD])}`; + const { error } = await this.supabase.auth.resetPasswordForEmail(email, { redirectTo }); + runInAction(() => (this.store.submitting = false)); + + if (error) { + this.pushErrors(['form'], error.message); + } else { + runInAction(() => (this.store.success = true)); + } + }; +} diff --git a/src/app/update-password/page.tsx b/src/app/update-password/page.tsx new file mode 100644 index 0000000..f2e6958 --- /dev/null +++ b/src/app/update-password/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useApi } from 'app/api/api_provider'; +import { observer } from 'mobx-react'; +import React from 'react'; +import { UpdatePassword } from './update_password'; +import { UpdatePasswordPresenter, UpdatePasswordStore } from './update_password_presenter'; + +function createUpdatePasswordPage(supabase: ReturnType['supabase']) { + const store = new UpdatePasswordStore(); + const presenter = new UpdatePasswordPresenter(supabase, store); + + return observer(() => { + return ( + + ); + }); +} + +export default () => { + const api = useApi(); + const [UpdatePasswordPage] = React.useState(() => createUpdatePasswordPage(api.supabase)); + return ; +}; diff --git a/src/app/update-password/update_password.module.css b/src/app/update-password/update_password.module.css new file mode 100644 index 0000000..0d59fda --- /dev/null +++ b/src/app/update-password/update_password.module.css @@ -0,0 +1,18 @@ +.updatePassword { + display: flex; + gap: var(--gridBaseline); + margin: auto; + flex-direction: column; + justify-content: center; + height: 100%; + width: calc(var(--gridBaseline) * 35); + max-width: 90%; +} + +.submitContainer { + display: flex; + width: 100%; + margin-top: var(--gridBaseline); + align-items: center; + justify-content: flex-end; +} diff --git a/src/app/update-password/update_password.tsx b/src/app/update-password/update_password.tsx new file mode 100644 index 0000000..4794e2d --- /dev/null +++ b/src/app/update-password/update_password.tsx @@ -0,0 +1,54 @@ +import { observer } from 'mobx-react'; +import { UpdatePasswordField } from './update_password_presenter'; +import { FormError } from 'ui/base/form/form_error'; +import { Button } from 'ui/base/button/button'; +import { Textbox } from 'ui/base/textbox/textbox'; +import styles from './update_password.module.css'; + +type UpdatePasswordProps = { + password: string; + submitting: boolean; + success: boolean; + errors: Map; + onChangePassword(value: string): void; + updatePassword(): void; +}; + +export const UpdatePassword = observer( + ({ + password, + submitting, + success, + errors, + onChangePassword, + updatePassword, + }: UpdatePasswordProps) => { + if (success) { + return ( +
+

Your password has been updated. Redirecting to login...

+
+ ); + } + + return ( +
+

Enter your new password.

+ +
+ +
+ +
+ ); + } +); diff --git a/src/app/update-password/update_password_presenter.ts b/src/app/update-password/update_password_presenter.ts new file mode 100644 index 0000000..fc1a1b9 --- /dev/null +++ b/src/app/update-password/update_password_presenter.ts @@ -0,0 +1,61 @@ +import { SupabaseClient } from '@supabase/supabase-js'; +import { action, makeObservable, observable, runInAction } from 'mobx'; +import { FormPresenter, FormStore } from 'ui/base/form/form_presenter'; +import { RoutePath, routeFor } from 'utils/routes'; + +export type UpdatePasswordField = 'password' | 'form'; + +export class UpdatePasswordStore extends FormStore { + submitting = false; + password = ''; + success = false; + + constructor() { + super(); + makeObservable(this, { + password: observable.ref, + submitting: observable.ref, + success: observable.ref, + }); + } +} + +export class UpdatePasswordPresenter extends FormPresenter { + constructor( + private readonly supabase: SupabaseClient, + private readonly store: UpdatePasswordStore + ) { + super(store); + makeObservable(this, { + onChangePassword: action.bound, + }); + } + + onChangePassword = (value: string) => (this.store.password = value); + + updatePassword = async () => { + runInAction(() => this.clearErrors()); + const password = this.store.password; + const errors = [ + ...this.checkPasswordRestrictionFields(['password', password]), + ...this.checkRequiredFields(['password', password]), + ]; + if (errors.length) { + return; + } + + runInAction(() => (this.store.submitting = true)); + const { error } = await this.supabase.auth.updateUser({ password }); + runInAction(() => (this.store.submitting = false)); + + if (error) { + this.pushErrors(['form'], error.message); + } else { + runInAction(() => (this.store.success = true)); + // Redirect to login after a short delay + setTimeout(() => { + window.location.href = routeFor([RoutePath.LOGIN]); + }, 2000); + } + }; +} diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 533b99e..00c3abd 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -3,6 +3,8 @@ export const enum RoutePath { MAP = 'map', LOGIN = 'login', LOGOUT = 'logout', + RESET_PASSWORD = 'reset-password', + UPDATE_PASSWORD = 'update-password', SETTINGS = 'settings', SIGNUP = 'signup', SUBMIT = 'submit', @@ -15,6 +17,8 @@ export type RouteSegments = | [RoutePath.MAP_LIST] | [RoutePath.LOGIN] | [RoutePath.LOGOUT] + | [RoutePath.RESET_PASSWORD] + | [RoutePath.UPDATE_PASSWORD] | [RoutePath.SETTINGS] | [RoutePath.SIGNUP] | [RoutePath.INSTRUCTIONS]; From 18e43584d5d42feefc59dc405061b5390ae4bbd6 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:17:58 +0000 Subject: [PATCH 2/4] refactor: address password reset review comments - Move routes from /reset-password and /update-password to /password/reset and /password/reset/update - Inline separate component files into page.tsx - Use global createClient() instead of DI for supabase - Use action.bound and setSubmitting/setSuccess helpers instead of runInAction - Use NEXT_PUBLIC_BASE_URL env var instead of window.location.origin Co-authored-by: bitnimble --- src/app/_auth/login_signup.tsx | 4 ++- .../reset/page.tsx} | 34 +++++++++++++++++-- .../reset}/reset_password.module.css | 0 .../reset}/reset_password_presenter.ts | 24 +++++++------ .../reset/update/page.tsx} | 34 +++++++++++++++++-- .../reset/update}/update_password.module.css | 0 .../update}/update_password_presenter.ts | 22 ++++++------ src/app/reset-password/page.tsx | 31 ----------------- src/app/update-password/page.tsx | 31 ----------------- src/utils/routes.ts | 9 ++--- 10 files changed, 97 insertions(+), 92 deletions(-) rename src/app/{reset-password/reset_password.tsx => password/reset/page.tsx} (66%) rename src/app/{reset-password => password/reset}/reset_password.module.css (100%) rename src/app/{reset-password => password/reset}/reset_password_presenter.ts (66%) rename src/app/{update-password/update_password.tsx => password/reset/update/page.tsx} (62%) rename src/app/{update-password => password/reset/update}/update_password.module.css (100%) rename src/app/{update-password => password/reset/update}/update_password_presenter.ts (73%) delete mode 100644 src/app/reset-password/page.tsx delete mode 100644 src/app/update-password/page.tsx diff --git a/src/app/_auth/login_signup.tsx b/src/app/_auth/login_signup.tsx index faa08b4..ffb089d 100644 --- a/src/app/_auth/login_signup.tsx +++ b/src/app/_auth/login_signup.tsx @@ -54,7 +54,9 @@ export const LoginSignup = observer( error={errors.get('password')} onSubmit={login} /> - Forgot password? + + Forgot password? +
Signup instead diff --git a/src/app/reset-password/reset_password.tsx b/src/app/password/reset/page.tsx similarity index 66% rename from src/app/reset-password/reset_password.tsx rename to src/app/password/reset/page.tsx index 3a5f526..9f1221a 100644 --- a/src/app/reset-password/reset_password.tsx +++ b/src/app/password/reset/page.tsx @@ -1,5 +1,12 @@ +'use client'; + import { observer } from 'mobx-react'; -import { ResetPasswordField } from './reset_password_presenter'; +import React from 'react'; +import { + ResetPasswordField, + ResetPasswordPresenter, + ResetPasswordStore, +} from './reset_password_presenter'; import { FormError } from 'ui/base/form/form_error'; import { RouteLink } from 'ui/base/text/link'; import { Button } from 'ui/base/button/button'; @@ -16,7 +23,7 @@ type ResetPasswordProps = { requestReset(): void; }; -export const ResetPassword = observer( +const ResetPassword = observer( ({ email, submitting, success, errors, onChangeEmail, requestReset }: ResetPasswordProps) => { if (success) { return ( @@ -48,3 +55,26 @@ export const ResetPassword = observer( ); } ); + +function createResetPasswordPage() { + const store = new ResetPasswordStore(); + const presenter = new ResetPasswordPresenter(store); + + return observer(() => { + return ( + + ); + }); +} + +export default () => { + const [ResetPasswordPage] = React.useState(() => createResetPasswordPage()); + return ; +}; diff --git a/src/app/reset-password/reset_password.module.css b/src/app/password/reset/reset_password.module.css similarity index 100% rename from src/app/reset-password/reset_password.module.css rename to src/app/password/reset/reset_password.module.css diff --git a/src/app/reset-password/reset_password_presenter.ts b/src/app/password/reset/reset_password_presenter.ts similarity index 66% rename from src/app/reset-password/reset_password_presenter.ts rename to src/app/password/reset/reset_password_presenter.ts index f3e6ba6..7f2c469 100644 --- a/src/app/reset-password/reset_password_presenter.ts +++ b/src/app/password/reset/reset_password_presenter.ts @@ -1,5 +1,5 @@ -import { SupabaseClient } from '@supabase/supabase-js'; import { action, makeObservable, observable, runInAction } from 'mobx'; +import { createClient } from 'services/session/supabase_client'; import { FormPresenter, FormStore } from 'ui/base/form/form_presenter'; import { RoutePath, routeFor } from 'utils/routes'; @@ -21,19 +21,21 @@ export class ResetPasswordStore extends FormStore { } export class ResetPasswordPresenter extends FormPresenter { - constructor( - private readonly supabase: SupabaseClient, - private readonly store: ResetPasswordStore - ) { + private readonly supabase = createClient(); + + constructor(private readonly store: ResetPasswordStore) { super(store); makeObservable(this, { onChangeEmail: action.bound, + requestReset: action.bound, }); } onChangeEmail = (value: string) => (this.store.email = value); + private setSubmitting = action((value: boolean) => (this.store.submitting = value)); + private setSuccess = action((value: boolean) => (this.store.success = value)); - requestReset = async () => { + async requestReset() { runInAction(() => this.clearErrors()); const email = this.store.email; const errors = [ @@ -44,15 +46,15 @@ export class ResetPasswordPresenter extends FormPresenter { return; } - runInAction(() => (this.store.submitting = true)); - const redirectTo = `${window.location.origin}${routeFor([RoutePath.UPDATE_PASSWORD])}`; + this.setSubmitting(true); + const redirectTo = `${process.env.NEXT_PUBLIC_BASE_URL}${routeFor([RoutePath.PASSWORD, RoutePath.RESET, RoutePath.UPDATE])}`; const { error } = await this.supabase.auth.resetPasswordForEmail(email, { redirectTo }); - runInAction(() => (this.store.submitting = false)); + this.setSubmitting(false); if (error) { this.pushErrors(['form'], error.message); } else { - runInAction(() => (this.store.success = true)); + this.setSuccess(true); } - }; + } } diff --git a/src/app/update-password/update_password.tsx b/src/app/password/reset/update/page.tsx similarity index 62% rename from src/app/update-password/update_password.tsx rename to src/app/password/reset/update/page.tsx index 4794e2d..ab6c8da 100644 --- a/src/app/update-password/update_password.tsx +++ b/src/app/password/reset/update/page.tsx @@ -1,5 +1,12 @@ +'use client'; + import { observer } from 'mobx-react'; -import { UpdatePasswordField } from './update_password_presenter'; +import React from 'react'; +import { + UpdatePasswordField, + UpdatePasswordPresenter, + UpdatePasswordStore, +} from './update_password_presenter'; import { FormError } from 'ui/base/form/form_error'; import { Button } from 'ui/base/button/button'; import { Textbox } from 'ui/base/textbox/textbox'; @@ -14,7 +21,7 @@ type UpdatePasswordProps = { updatePassword(): void; }; -export const UpdatePassword = observer( +const UpdatePassword = observer( ({ password, submitting, @@ -52,3 +59,26 @@ export const UpdatePassword = observer( ); } ); + +function createUpdatePasswordPage() { + const store = new UpdatePasswordStore(); + const presenter = new UpdatePasswordPresenter(store); + + return observer(() => { + return ( + + ); + }); +} + +export default () => { + const [UpdatePasswordPage] = React.useState(() => createUpdatePasswordPage()); + return ; +}; diff --git a/src/app/update-password/update_password.module.css b/src/app/password/reset/update/update_password.module.css similarity index 100% rename from src/app/update-password/update_password.module.css rename to src/app/password/reset/update/update_password.module.css diff --git a/src/app/update-password/update_password_presenter.ts b/src/app/password/reset/update/update_password_presenter.ts similarity index 73% rename from src/app/update-password/update_password_presenter.ts rename to src/app/password/reset/update/update_password_presenter.ts index fc1a1b9..67e3574 100644 --- a/src/app/update-password/update_password_presenter.ts +++ b/src/app/password/reset/update/update_password_presenter.ts @@ -1,5 +1,5 @@ -import { SupabaseClient } from '@supabase/supabase-js'; import { action, makeObservable, observable, runInAction } from 'mobx'; +import { createClient } from 'services/session/supabase_client'; import { FormPresenter, FormStore } from 'ui/base/form/form_presenter'; import { RoutePath, routeFor } from 'utils/routes'; @@ -21,19 +21,21 @@ export class UpdatePasswordStore extends FormStore { } export class UpdatePasswordPresenter extends FormPresenter { - constructor( - private readonly supabase: SupabaseClient, - private readonly store: UpdatePasswordStore - ) { + private readonly supabase = createClient(); + + constructor(private readonly store: UpdatePasswordStore) { super(store); makeObservable(this, { onChangePassword: action.bound, + updatePassword: action.bound, }); } onChangePassword = (value: string) => (this.store.password = value); + private setSubmitting = action((value: boolean) => (this.store.submitting = value)); + private setSuccess = action((value: boolean) => (this.store.success = value)); - updatePassword = async () => { + async updatePassword() { runInAction(() => this.clearErrors()); const password = this.store.password; const errors = [ @@ -44,18 +46,18 @@ export class UpdatePasswordPresenter extends FormPresenter return; } - runInAction(() => (this.store.submitting = true)); + this.setSubmitting(true); const { error } = await this.supabase.auth.updateUser({ password }); - runInAction(() => (this.store.submitting = false)); + this.setSubmitting(false); if (error) { this.pushErrors(['form'], error.message); } else { - runInAction(() => (this.store.success = true)); + this.setSuccess(true); // Redirect to login after a short delay setTimeout(() => { window.location.href = routeFor([RoutePath.LOGIN]); }, 2000); } - }; + } } diff --git a/src/app/reset-password/page.tsx b/src/app/reset-password/page.tsx deleted file mode 100644 index f1ab3ff..0000000 --- a/src/app/reset-password/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { useApi } from 'app/api/api_provider'; -import { observer } from 'mobx-react'; -import React from 'react'; -import { ResetPassword } from './reset_password'; -import { ResetPasswordPresenter, ResetPasswordStore } from './reset_password_presenter'; - -function createResetPasswordPage(supabase: ReturnType['supabase']) { - const store = new ResetPasswordStore(); - const presenter = new ResetPasswordPresenter(supabase, store); - - return observer(() => { - return ( - - ); - }); -} - -export default () => { - const api = useApi(); - const [ResetPasswordPage] = React.useState(() => createResetPasswordPage(api.supabase)); - return ; -}; diff --git a/src/app/update-password/page.tsx b/src/app/update-password/page.tsx deleted file mode 100644 index f2e6958..0000000 --- a/src/app/update-password/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { useApi } from 'app/api/api_provider'; -import { observer } from 'mobx-react'; -import React from 'react'; -import { UpdatePassword } from './update_password'; -import { UpdatePasswordPresenter, UpdatePasswordStore } from './update_password_presenter'; - -function createUpdatePasswordPage(supabase: ReturnType['supabase']) { - const store = new UpdatePasswordStore(); - const presenter = new UpdatePasswordPresenter(supabase, store); - - return observer(() => { - return ( - - ); - }); -} - -export default () => { - const api = useApi(); - const [UpdatePasswordPage] = React.useState(() => createUpdatePasswordPage(api.supabase)); - return ; -}; diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 00c3abd..e8cb5ae 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -3,8 +3,9 @@ export const enum RoutePath { MAP = 'map', LOGIN = 'login', LOGOUT = 'logout', - RESET_PASSWORD = 'reset-password', - UPDATE_PASSWORD = 'update-password', + PASSWORD = 'password', + RESET = 'reset', + UPDATE = 'update', SETTINGS = 'settings', SIGNUP = 'signup', SUBMIT = 'submit', @@ -17,8 +18,8 @@ export type RouteSegments = | [RoutePath.MAP_LIST] | [RoutePath.LOGIN] | [RoutePath.LOGOUT] - | [RoutePath.RESET_PASSWORD] - | [RoutePath.UPDATE_PASSWORD] + | [RoutePath.PASSWORD, RoutePath.RESET] + | [RoutePath.PASSWORD, RoutePath.RESET, RoutePath.UPDATE] | [RoutePath.SETTINGS] | [RoutePath.SIGNUP] | [RoutePath.INSTRUCTIONS]; From 4711628f0439ee7a07bedbcd02c12f02e0cf515c Mon Sep 17 00:00:00 2001 From: D Date: Tue, 20 Jan 2026 10:53:42 +1100 Subject: [PATCH 3/4] clean up --- src/app/password/reset/page.tsx | 95 ++++++------------ .../reset/reset_password_presenter.ts | 18 ++-- src/app/password/reset/update/page.tsx | 98 ++++++------------- .../reset/update/update_password_presenter.ts | 20 ++-- 4 files changed, 77 insertions(+), 154 deletions(-) diff --git a/src/app/password/reset/page.tsx b/src/app/password/reset/page.tsx index 9f1221a..03a9b36 100644 --- a/src/app/password/reset/page.tsx +++ b/src/app/password/reset/page.tsx @@ -1,80 +1,45 @@ 'use client'; -import { observer } from 'mobx-react'; -import React from 'react'; -import { - ResetPasswordField, - ResetPasswordPresenter, - ResetPasswordStore, -} from './reset_password_presenter'; +import { observer } from 'mobx-react-lite'; +import { useState } from 'react'; +import { Button } from 'ui/base/button/button'; import { FormError } from 'ui/base/form/form_error'; import { RouteLink } from 'ui/base/text/link'; -import { Button } from 'ui/base/button/button'; import { Textbox } from 'ui/base/textbox/textbox'; +import { RoutePath, routeFor } from 'utils/routes'; import styles from './reset_password.module.css'; -import { routeFor, RoutePath } from 'utils/routes'; - -type ResetPasswordProps = { - email: string; - submitting: boolean; - success: boolean; - errors: Map; - onChangeEmail(value: string): void; - requestReset(): void; -}; +import { ResetPasswordPresenter, ResetPasswordStore } from './reset_password_presenter'; -const ResetPassword = observer( - ({ email, submitting, success, errors, onChangeEmail, requestReset }: ResetPasswordProps) => { - if (success) { - return ( -
-

Check your email for a password reset link.

- Back to login -
- ); - } +export default observer(() => { + const [store] = useState(new ResetPasswordStore()); + const presenter = new ResetPasswordPresenter(store); + if (store.success) { return (
-

Enter your email address and we'll send you a link to reset your password.

- -
- Back to login - -
- +

Check your email for a password reset link.

+ Back to login
); } -); - -function createResetPasswordPage() { - const store = new ResetPasswordStore(); - const presenter = new ResetPasswordPresenter(store); - return observer(() => { - return ( - +

Enter your email address and we'll send you a link to reset your password.

+ - ); - }); -} - -export default () => { - const [ResetPasswordPage] = React.useState(() => createResetPasswordPage()); - return ; -}; +
+ Back to login + +
+ +
+ ); +}); diff --git a/src/app/password/reset/reset_password_presenter.ts b/src/app/password/reset/reset_password_presenter.ts index 7f2c469..85568cf 100644 --- a/src/app/password/reset/reset_password_presenter.ts +++ b/src/app/password/reset/reset_password_presenter.ts @@ -1,9 +1,9 @@ -import { action, makeObservable, observable, runInAction } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { createClient } from 'services/session/supabase_client'; import { FormPresenter, FormStore } from 'ui/base/form/form_presenter'; import { RoutePath, routeFor } from 'utils/routes'; -export type ResetPasswordField = 'email' | 'form'; +type ResetPasswordField = 'email' | 'form'; export class ResetPasswordStore extends FormStore { submitting = false; @@ -21,22 +21,22 @@ export class ResetPasswordStore extends FormStore { } export class ResetPasswordPresenter extends FormPresenter { - private readonly supabase = createClient(); - constructor(private readonly store: ResetPasswordStore) { super(store); - makeObservable(this, { + makeObservable(this, { onChangeEmail: action.bound, requestReset: action.bound, + setSubmitting: action.bound, + setSuccess: action.bound, }); } onChangeEmail = (value: string) => (this.store.email = value); - private setSubmitting = action((value: boolean) => (this.store.submitting = value)); - private setSuccess = action((value: boolean) => (this.store.success = value)); + private setSubmitting = (value: boolean) => (this.store.submitting = value); + private setSuccess = (value: boolean) => (this.store.success = value); async requestReset() { - runInAction(() => this.clearErrors()); + this.clearErrors(); const email = this.store.email; const errors = [ ...this.checkEmailFields(['email', email]), @@ -48,7 +48,7 @@ export class ResetPasswordPresenter extends FormPresenter { this.setSubmitting(true); const redirectTo = `${process.env.NEXT_PUBLIC_BASE_URL}${routeFor([RoutePath.PASSWORD, RoutePath.RESET, RoutePath.UPDATE])}`; - const { error } = await this.supabase.auth.resetPasswordForEmail(email, { redirectTo }); + const { error } = await createClient().auth.resetPasswordForEmail(email, { redirectTo }); this.setSubmitting(false); if (error) { diff --git a/src/app/password/reset/update/page.tsx b/src/app/password/reset/update/page.tsx index ab6c8da..170d264 100644 --- a/src/app/password/reset/update/page.tsx +++ b/src/app/password/reset/update/page.tsx @@ -1,84 +1,42 @@ 'use client'; -import { observer } from 'mobx-react'; -import React from 'react'; -import { - UpdatePasswordField, - UpdatePasswordPresenter, - UpdatePasswordStore, -} from './update_password_presenter'; -import { FormError } from 'ui/base/form/form_error'; +import { observer } from 'mobx-react-lite'; +import { useState } from 'react'; import { Button } from 'ui/base/button/button'; +import { FormError } from 'ui/base/form/form_error'; import { Textbox } from 'ui/base/textbox/textbox'; import styles from './update_password.module.css'; +import { UpdatePasswordPresenter, UpdatePasswordStore } from './update_password_presenter'; -type UpdatePasswordProps = { - password: string; - submitting: boolean; - success: boolean; - errors: Map; - onChangePassword(value: string): void; - updatePassword(): void; -}; - -const UpdatePassword = observer( - ({ - password, - submitting, - success, - errors, - onChangePassword, - updatePassword, - }: UpdatePasswordProps) => { - if (success) { - return ( -
-

Your password has been updated. Redirecting to login...

-
- ); - } +export default observer(() => { + const [store] = useState(new UpdatePasswordStore()); + const presenter = new UpdatePasswordPresenter(store); + if (store.success) { return (
-

Enter your new password.

- -
- -
- +

Your password has been updated. Redirecting to login...

); } -); - -function createUpdatePasswordPage() { - const store = new UpdatePasswordStore(); - const presenter = new UpdatePasswordPresenter(store); - return observer(() => { - return ( - +

Enter your new password.

+ - ); - }); -} - -export default () => { - const [UpdatePasswordPage] = React.useState(() => createUpdatePasswordPage()); - return ; -}; +
+ +
+ +
+ ); +}); diff --git a/src/app/password/reset/update/update_password_presenter.ts b/src/app/password/reset/update/update_password_presenter.ts index 67e3574..4cd13ba 100644 --- a/src/app/password/reset/update/update_password_presenter.ts +++ b/src/app/password/reset/update/update_password_presenter.ts @@ -1,9 +1,9 @@ -import { action, makeObservable, observable, runInAction } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { createClient } from 'services/session/supabase_client'; import { FormPresenter, FormStore } from 'ui/base/form/form_presenter'; import { RoutePath, routeFor } from 'utils/routes'; -export type UpdatePasswordField = 'password' | 'form'; +type UpdatePasswordField = 'password' | 'form'; export class UpdatePasswordStore extends FormStore { submitting = false; @@ -21,22 +21,22 @@ export class UpdatePasswordStore extends FormStore { } export class UpdatePasswordPresenter extends FormPresenter { - private readonly supabase = createClient(); - constructor(private readonly store: UpdatePasswordStore) { super(store); - makeObservable(this, { + makeObservable(this, { onChangePassword: action.bound, updatePassword: action.bound, + setSubmitting: action.bound, + setSuccess: action.bound, }); } - onChangePassword = (value: string) => (this.store.password = value); - private setSubmitting = action((value: boolean) => (this.store.submitting = value)); - private setSuccess = action((value: boolean) => (this.store.success = value)); + onChangePassword = action((value: string) => (this.store.password = value)); + private setSubmitting = (value: boolean) => (this.store.submitting = value); + private setSuccess = (value: boolean) => (this.store.success = value); async updatePassword() { - runInAction(() => this.clearErrors()); + this.clearErrors(); const password = this.store.password; const errors = [ ...this.checkPasswordRestrictionFields(['password', password]), @@ -47,7 +47,7 @@ export class UpdatePasswordPresenter extends FormPresenter } this.setSubmitting(true); - const { error } = await this.supabase.auth.updateUser({ password }); + const { error } = await createClient().auth.updateUser({ password }); this.setSubmitting(false); if (error) { From fda66ec5143adcd8a693c855ca640ad2a973e860 Mon Sep 17 00:00:00 2001 From: D Date: Tue, 20 Jan 2026 11:45:46 +1100 Subject: [PATCH 4/4] . --- .env.test | 2 +- src/app/_auth/login_signup.module.css | 10 ++- src/app/_auth/login_signup.tsx | 14 +-- .../password/reset/reset_password.module.css | 4 +- .../reset/reset_password_presenter.ts | 4 + src/app/password/reset/update/page.tsx | 12 ++- .../reset/update/update_password.module.css | 4 +- .../reset/update/update_password_presenter.ts | 17 +++- src/services/env.ts | 2 +- src/session/session_presenter.ts | 5 ++ src/ui/base/form/form_presenter.ts | 12 +++ src/ui/nav_bar/nav_bar.module.css | 6 +- src/ui/nav_bar/nav_bar.tsx | 85 +++++++++++++------ 13 files changed, 126 insertions(+), 51 deletions(-) diff --git a/.env.test b/.env.test index eece736..6212e03 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,4 @@ -BASE_URL=http://localhost:3000 +NEXT_PUBLIC_BASE_URL=http://localhost:3000 PGHOST= PGPORT=5432 PGUSER= diff --git a/src/app/_auth/login_signup.module.css b/src/app/_auth/login_signup.module.css index c637ba7..c6d123c 100644 --- a/src/app/_auth/login_signup.module.css +++ b/src/app/_auth/login_signup.module.css @@ -5,8 +5,8 @@ flex-direction: column; justify-content: center; height: 100%; - width: calc(var(--gridBaseline) * 35); - max-width: 90%; + width: calc(var(--gridBaseline) * 50); + max-width: 80%; } .submitContainer { @@ -16,3 +16,9 @@ align-items: center; justify-content: space-between; } + +.accountActions { + display: flex; + flex-direction: column; + gap: var(--gridBaseline); +} diff --git a/src/app/_auth/login_signup.tsx b/src/app/_auth/login_signup.tsx index ffb089d..9b52d17 100644 --- a/src/app/_auth/login_signup.tsx +++ b/src/app/_auth/login_signup.tsx @@ -54,13 +54,15 @@ export const LoginSignup = observer( error={errors.get('password')} onSubmit={login} /> - - Forgot password? -
- - Signup instead - +
+ + Signup instead + + + Forgot password? + +
diff --git a/src/app/password/reset/reset_password.module.css b/src/app/password/reset/reset_password.module.css index e801d93..ffd1f77 100644 --- a/src/app/password/reset/reset_password.module.css +++ b/src/app/password/reset/reset_password.module.css @@ -5,8 +5,8 @@ flex-direction: column; justify-content: center; height: 100%; - width: calc(var(--gridBaseline) * 35); - max-width: 90%; + width: calc(var(--gridBaseline) * 50); + max-width: 80%; } .submitContainer { diff --git a/src/app/password/reset/reset_password_presenter.ts b/src/app/password/reset/reset_password_presenter.ts index 85568cf..7ce6303 100644 --- a/src/app/password/reset/reset_password_presenter.ts +++ b/src/app/password/reset/reset_password_presenter.ts @@ -29,6 +29,10 @@ export class ResetPasswordPresenter extends FormPresenter { setSubmitting: action.bound, setSuccess: action.bound, }); + + console.log( + `${process.env.NEXT_PUBLIC_BASE_URL}${routeFor([RoutePath.PASSWORD, RoutePath.RESET, RoutePath.UPDATE])}` + ); } onChangeEmail = (value: string) => (this.store.email = value); diff --git a/src/app/password/reset/update/page.tsx b/src/app/password/reset/update/page.tsx index 170d264..89b0974 100644 --- a/src/app/password/reset/update/page.tsx +++ b/src/app/password/reset/update/page.tsx @@ -15,7 +15,7 @@ export default observer(() => { if (store.success) { return (
-

Your password has been updated. Redirecting to login...

+

Your password has been updated. Redirecting to home...

); } @@ -28,9 +28,19 @@ export default observer(() => { onChange={presenter.onChangePassword} inputType="password" label="New password" + required={true} onSubmit={presenter.updatePassword} error={store.errors.get('password')} /> +