Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BASE_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=http://localhost:3000
PGHOST=
PGPORT=5432
PGUSER=
Expand Down
10 changes: 8 additions & 2 deletions src/app/_auth/login_signup.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,3 +16,9 @@
align-items: center;
justify-content: space-between;
}

.accountActions {
display: flex;
flex-direction: column;
gap: var(--gridBaseline);
}
11 changes: 8 additions & 3 deletions src/app/_auth/login_signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,14 @@ export const LoginSignup = observer(
onSubmit={login}
/>
<div className={styles.submitContainer}>
<RouteLink href={routeFor([RoutePath.SIGNUP])} onClick={onNavigateClick}>
Signup instead
</RouteLink>
<div className={styles.accountActions}>
<RouteLink href={routeFor([RoutePath.SIGNUP])} onClick={onNavigateClick}>
Signup instead
</RouteLink>
<RouteLink href={routeFor([RoutePath.PASSWORD, RoutePath.RESET])}>
Forgot password?
</RouteLink>
</div>
<Button loading={submitting} onClick={login}>
Login
</Button>
Expand Down
45 changes: 45 additions & 0 deletions src/app/password/reset/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client';

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 { Textbox } from 'ui/base/textbox/textbox';
import { RoutePath, routeFor } from 'utils/routes';
import styles from './reset_password.module.css';
import { ResetPasswordPresenter, ResetPasswordStore } from './reset_password_presenter';

export default observer(() => {
const [store] = useState(new ResetPasswordStore());
const presenter = new ResetPasswordPresenter(store);

if (store.success) {
return (
<div className={styles.resetPassword}>
<p>Check your email for a password reset link.</p>
<RouteLink href={routeFor([RoutePath.LOGIN])}>Back to login</RouteLink>
</div>
);
}

return (
<div className={styles.resetPassword}>
<p>Enter your email address and we&apos;ll send you a link to reset your password.</p>
<Textbox
value={store.email}
onChange={presenter.onChangeEmail}
label="Email"
onSubmit={presenter.requestReset}
error={store.errors.get('email')}
/>
<div className={styles.submitContainer}>
<RouteLink href={routeFor([RoutePath.LOGIN])}>Back to login</RouteLink>
<Button loading={store.submitting} onClick={presenter.requestReset}>
Send reset link
</Button>
</div>
<FormError error={store.errors.get('form')} />
</div>
);
});
18 changes: 18 additions & 0 deletions src/app/password/reset/reset_password.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.resetPassword {
display: flex;
gap: var(--gridBaseline);
margin: auto;
flex-direction: column;
justify-content: center;
height: 100%;
width: calc(var(--gridBaseline) * 50);
max-width: 80%;
}

.submitContainer {
display: flex;
width: 100%;
margin-top: var(--gridBaseline);
align-items: center;
justify-content: space-between;
}
64 changes: 64 additions & 0 deletions src/app/password/reset/reset_password_presenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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';

type ResetPasswordField = 'email' | 'form';

export class ResetPasswordStore extends FormStore<ResetPasswordField> {
submitting = false;
email = '';
success = false;

constructor() {
super();
makeObservable(this, {
email: observable.ref,
submitting: observable.ref,
success: observable.ref,
});
}
}

export class ResetPasswordPresenter extends FormPresenter<ResetPasswordField> {
constructor(private readonly store: ResetPasswordStore) {
super(store);
makeObservable<typeof this, 'setSubmitting' | 'setSuccess'>(this, {
onChangeEmail: action.bound,
requestReset: action.bound,
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);
private setSubmitting = (value: boolean) => (this.store.submitting = value);
private setSuccess = (value: boolean) => (this.store.success = value);

async requestReset() {
this.clearErrors();
const email = this.store.email;
const errors = [
...this.checkEmailFields(['email', email]),
...this.checkRequiredFields(['email', email]),
];
if (errors.length) {
return;
}

this.setSubmitting(true);
const redirectTo = `${process.env.NEXT_PUBLIC_BASE_URL}${routeFor([RoutePath.PASSWORD, RoutePath.RESET, RoutePath.UPDATE])}`;
const { error } = await createClient().auth.resetPasswordForEmail(email, { redirectTo });
this.setSubmitting(false);

if (error) {
this.pushErrors(['form'], error.message);
} else {
this.setSuccess(true);
}
}
}
52 changes: 52 additions & 0 deletions src/app/password/reset/update/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client';

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';

export default observer(() => {
const [store] = useState(new UpdatePasswordStore());
const presenter = new UpdatePasswordPresenter(store);

if (store.success) {
return (
<div className={styles.updatePassword}>
<p>Your password has been updated. Redirecting to home...</p>
</div>
);
}

return (
<div className={styles.updatePassword}>
<p>Enter your new password.</p>
<Textbox
value={store.password}
onChange={presenter.onChangePassword}
inputType="password"
label="New password"
required={true}
onSubmit={presenter.updatePassword}
error={store.errors.get('password')}
/>
<Textbox
value={store.confirmPassword}
onChange={presenter.onChangeConfirmPassword}
inputType="password"
label="Confirm new password"
required={true}
onSubmit={presenter.updatePassword}
error={store.errors.get('confirmPassword')}
/>
<div className={styles.submitContainer}>
<Button loading={store.submitting} onClick={presenter.updatePassword}>
Update password
</Button>
</div>
<FormError error={store.errors.get('form')} />
</div>
);
});
18 changes: 18 additions & 0 deletions src/app/password/reset/update/update_password.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.updatePassword {
display: flex;
gap: var(--gridBaseline);
margin: auto;
flex-direction: column;
justify-content: center;
height: 100%;
width: calc(var(--gridBaseline) * 50);
max-width: 80%;
}

.submitContainer {
display: flex;
width: 100%;
margin-top: var(--gridBaseline);
align-items: center;
justify-content: flex-end;
}
72 changes: 72 additions & 0 deletions src/app/password/reset/update/update_password_presenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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';

type UpdatePasswordField = 'password' | 'confirmPassword' | 'form';

export class UpdatePasswordStore extends FormStore<UpdatePasswordField> {
submitting = false;
password = '';
confirmPassword = '';
success = false;

constructor() {
super();
makeObservable(this, {
password: observable.ref,
confirmPassword: observable.ref,
submitting: observable.ref,
success: observable.ref,
});
}
}

export class UpdatePasswordPresenter extends FormPresenter<UpdatePasswordField> {
constructor(private readonly store: UpdatePasswordStore) {
super(store);
makeObservable<typeof this, 'setSubmitting' | 'setSuccess'>(this, {
onChangePassword: action.bound,
onChangeConfirmPassword: action.bound,
updatePassword: action.bound,
setSubmitting: action.bound,
setSuccess: action.bound,
});
}

onChangePassword = (value: string) => (this.store.password = value);
onChangeConfirmPassword = (value: string) => (this.store.confirmPassword = value);
private setSubmitting = (value: boolean) => (this.store.submitting = value);
private setSuccess = (value: boolean) => (this.store.success = value);

async updatePassword() {
this.clearErrors();
const password = this.store.password;
const confirmPassword = this.store.confirmPassword;
const errors = [
...this.checkPasswordRestrictionFields(['password', password]),
...this.checkRequiredFields(['password', password], ['confirmPassword', confirmPassword]),
...this.checkPasswordConfirmFields(
['password', password],
['confirmPassword', confirmPassword]
),
];
if (errors.length) {
return;
}

this.setSubmitting(true);
const { error } = await createClient().auth.updateUser({ password });
this.setSubmitting(false);

if (error) {
this.pushErrors(['form'], error.message);
} else {
this.setSuccess(true);
// Redirect to login after a short delay
setTimeout(() => {
window.location.href = routeFor([RoutePath.MAP_LIST]);
}, 2000);
}
}
}
2 changes: 1 addition & 1 deletion src/services/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function getEnvVars() {

function createEnvVars(): EnvVars {
const envVars: { [K in keyof EnvVars]: EnvVars[K] | undefined } = {
baseUrl: process.env.BASE_URL,
baseUrl: process.env.NEXT_PUBLIC_BASE_URL,
pgHost: process.env.PGHOST,
pgPort: Number(process.env.PGPORT || undefined),
pgDatabase: process.env.PGDATABASE,
Expand Down
5 changes: 5 additions & 0 deletions src/session/session_presenter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Api } from 'app/api/api';
import { makeAutoObservable, runInAction } from 'mobx';
import { User } from 'schema/users';
import { createClient } from 'services/session/supabase_client';

export class SessionStore {
hasLoaded = false;
Expand All @@ -20,6 +21,10 @@ export class SessionPresenter {
}

async maybeLoadSession() {
// TODO: rewrite sessionstore and presenter in the client to just use Supabase Auth
const client = createClient();
await client.auth.initialize();

if (this.store.hasLoaded) {
return;
}
Expand Down
12 changes: 12 additions & 0 deletions src/ui/base/form/form_presenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ export class FormPresenter<Field extends string> {
return errorFields;
}

protected checkPasswordConfirmFields(
password: readonly [Field, string],
confirmPassword: readonly [Field, string]
) {
const matches = password[1] === confirmPassword[1];
if (!matches) {
this.pushErrors([confirmPassword[0]], 'Password does not match.');
return [confirmPassword[0]];
}
return [];
}

protected clearErrors() {
this._store.errors.clear();
}
Expand Down
6 changes: 1 addition & 5 deletions src/ui/nav_bar/nav_bar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@
align-items: center;
}

.userStatus {
margin-right: calc(var(--gridBaseline) * 2);
}

.menuItem {
margin: 0 var(--gridBaseline);
}

.themeToggleContainer {
margin-right: calc(var(--gridBaseline) * 2);
margin-right: var(--gridBaseline);
}
Loading