Skip to content

Commit

Permalink
feat(console): password reset implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Jun 23, 2024
1 parent adc0c25 commit 4782df5
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 30 deletions.
12 changes: 7 additions & 5 deletions apps/console/project.inlang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,20 @@
"messages_please_wait": "Bitte warten...",
"log": "Dieses Protokoll wurde von {fileName} aufgerufen!",
"auth_labels_signin": "Anmelden",
"auth_labels_signin_with_email": "Melden Sie sich mit E-Mail an",
"auth_labels_signin_with_webauthn": "With WebAuthn",
"auth_labels_signin_with_email": "Einloggen mit",
"auth_labels_signin_with_webauthn": "Einloggen mit",
"auth_labels_signin_problem": "Anmeldeproblem",
"auth_labels_signup": "Registrieren",
"auth_labels_signout": "Abmelden",
"auth_labels_forgot_password": "Passwort vergessen?",
"auth_labels_change_password": "Ändern Sie Ihr Passwort",
"auth_labels_password_problem": "Problem beim Passwortwechsel",
"auth_labels_update_password": "Passwort aktualisieren",
"auth_labels_reset_problem": "Problem beim Zurücksetzen des Passworts",
"auth_labels_send_peset_email": "Passwort-Zurücksetzen-E-Mail senden",
"auth_messages_aaa": "testing",
"auth_labels_reset_password": "Senden Sie eine E-Mail zum Zurücksetzen des Passworts",
"auth_labels_reset_password_problem": "Problem beim Zurücksetzen des Passworts",
"auth_messages_reset_password_healding": "Setze dein Passwort zurück",
"auth_messages_reset_password_subheading": "Erhalten Sie E-Mail-Anweisungen zum Zurücksetzen Ihres Passworts.",
"auth_messages_aaa": "Senden Sie eine E-Mail zum Zurücksetzen des Passworts",
"auth_forms_first_organization_label": "Organisation",
"auth_forms_first_organization_placeholder": "Organisation",
"auth_forms_first_name_label": "Vorname",
Expand Down
6 changes: 4 additions & 2 deletions apps/console/project.inlang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
"auth_labels_change_password": "Change Your Password",
"auth_labels_password_problem": "Change Password Problem",
"auth_labels_update_password": "Update Password",
"auth_labels_reset_problem": "Reset Password Problem",
"auth_labels_send_peset_email": "Send Password Reset Email",
"auth_labels_reset_password": "Send Password Reset Email",
"auth_labels_reset_password_problem": "Reset Password Problem",
"auth_messages_reset_password_healding": "Reset Your Password",
"auth_messages_reset_password_subheading": "Receive email instructions to reset your password.",
"auth_messages_aaa": "Send Password Reset Email",
"auth_forms_first_organization_label": "Organization",
"auth_forms_first_organization_placeholder": "Organization",
Expand Down
26 changes: 26 additions & 0 deletions apps/console/project.inlang/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"language": "Idioma",
"signin": "Iniciar sesión",
"signup": "Registrarse",
"signout": "Cerrar sesión",
"forgotPassword": "¿Olvidaste tu contraseña?",
"contact": "Contacto",
"privacy": "Privacidad",
"terms": "Términos",
"email": "Dirección de correo electrónico",
"password": "Contraseña",
"firstName": "Nombre",
"lastName": "Apellido",
"profile": "Perfil",
"home": "Inicio",
"protected": "Protegido",
"auth_password_reset_success_emailSent": "Correo electrónico de restablecimiento de contraseña enviado",
"auth_password_reset_success_checkEmail": "Verifique su cuenta de correo electrónico para obtener un enlace para restablecer su contraseña. Si no aparece en unos minutos, verifique su carpeta de correo no deseado.",
"auth_password_reset_resetProblem": "Problema al restablecer la contraseña",
"auth_password_reset_sendResetEmail": "Enviar correo electrónico de restablecimiento de contraseña",
"auth_password_update_success_updated": "Contraseña actualizada correctamente",
"auth_password_update_changePassword": "Cambiar contraseña",
"auth_password_update_passwordProblem": "Problema al cambiar la contraseña",
"auth_password_update_updatePassword": "Actualizar contraseña"
}
2 changes: 2 additions & 0 deletions apps/console/src/lib/schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export const changePasswordSchema = userSchema
.pick({ password: true, confirmPassword: true })
.superRefine((data, ctx) => checkConfirmPassword(ctx, data.confirmPassword, data.password));

export const resetPasswordSchema = userSchema.pick({ email: true });

export const changeEmailSchema = userSchema.pick({ email: true });

export const webAuthnSchema = z.object({
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/lib/server/middleware/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const publicPaths = [
'/fonts',
'/signin',
'/signup',
'/password-reset',
'/reset',
'/privacy',
'/terms',
'/docs',
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/lib/server/middleware/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Handle } from '@sveltejs/kit';
* This should be the first middleware.
*/
const log = new Logger('server:middleware:limiter');
const rateLimitedPaths = ['/signin', '/signup', '/password-reset', '/downlaod'];
const rateLimitedPaths = ['/signin', '/signup', '/reset', '/downlaod'];

export const rateLimiter = (async ({ event, resolve }) => {
const {
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/routes/(auth)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { UserRound } from 'lucide-svelte';
</div>
</div>
<TabGroup justify="justify-center">
<TabAnchor href="/signin" selected={$page.url.pathname === '/signin'}>
<TabAnchor href="/signin" selected={$page.url.pathname === '/signin' || $page.url.pathname === '/reset'}>
<span>{m.auth_labels_signin()}</span>
</TabAnchor>
<TabAnchor href="/signup" selected={$page.url.pathname === '/signup'}>
Expand Down
4 changes: 0 additions & 4 deletions apps/console/src/routes/(auth)/password-reset/+page.svelte

This file was deleted.

56 changes: 56 additions & 0 deletions apps/console/src/routes/(auth)/reset/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { i18n } from '$lib/i18n';
import { resetPasswordSchema } from '$lib/schema/user';
import { limiter } from '$lib/server/limiter/limiter';
import { Logger, sleep } from '@spectacular/utils';
import { fail } from '@sveltejs/kit';
import { message, setError, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';

const log = new Logger('auth:reset:server');

export const actions = {
default: async (event) => {
const {
request,
cookies,
locals: {
paraglide: { lang },
nhost,
},
} = event;

const form = await superValidate(request, zod(resetPasswordSchema));

const status = await limiter.check(event);
if (status.limited) {
event.setHeaders({
'Retry-After': status.retryAfter.toString(),
});
return message(
form,
{
type: 'error',
message: `Rate limit has been reached. Please retry after ${status.retryAfter} seconds`,
},
{ status: 429 },
);
}

await sleep(8000);

if (!form.valid) return fail(400, { form });
const { email } = form.data;
const { error } = await nhost.auth.resetPassword({
email,
options: {
redirectTo: '/profile',
},
});
if (error) {
log.error(error);
return setError(form, `Failed to send reset password instructions: ${error.message}`, { status: 409 }); // 424 ???
}

return message(form, { type: 'success', message: 'Send reset password instructions to email provided' });
},
};
155 changes: 155 additions & 0 deletions apps/console/src/routes/(auth)/reset/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script lang="ts">
import { page } from '$app/stores';
import * as m from '$i18n/messages';
import { handleMessage } from '$lib/components/layout/toast-manager';
import { resetPasswordSchema } from '$lib/schema/user';
import { isLoadingForm } from '$lib/stores/loading';
import { getToastStore } from '@skeletonlabs/skeleton';
// import { ConicGradient } from '@skeletonlabs/skeleton';
// import type { ConicStop } from '@skeletonlabs/skeleton';
import { DebugShell } from '@spectacular/skeleton/components';
import { Logger } from '@spectacular/utils';
import { AlertTriangle, Loader, MoreHorizontal } from 'lucide-svelte';
import { fade } from 'svelte/transition';
import SuperDebug, { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
export let data;
const log = new Logger('auth:reset:browser');
const toastStore = getToastStore();
const { form, delayed, timeout, enhance, errors, constraints, message, tainted, posted, submitting, capture, restore } =
superForm(data.form, {
dataType: 'json',
taintedMessage: null,
syncFlashMessage: false,
delayMs: 100,
timeoutMs: 4000,
validators: zodClient(resetPasswordSchema),
onError({ result }) {
// TODO:
// message.set(result.error.message)
log.error('reset password error:', { result });
},
onUpdated({ form }) {
if (form.message) {
handleMessage(form.message, toastStore);
}
},
});
export const snapshot = { capture, restore };
// Reactivity
delayed.subscribe((v) => ($isLoadingForm = v));
// const conicStops: ConicStop[] = [
// { color: 'transparent', start: 0, end: 25 },
// { color: 'rgb(var(--color-primary-900))', start: 75, end: 100 }
// ];
</script>

<svelte:head>
<title>Datablocks | Reset</title>
<meta name="description" content="Reset Password" />
</svelte:head>

<h3 class="h3 pt-5">{m.auth_messages_reset_password_healding()}</h3>
<small class="text-gray-500">{m.auth_messages_reset_password_subheading()}</small>

<!-- Form Level Errors / Messages -->
{#if $message}
<aside
class="alert mt-6"
class:variant-filled-success={$message.type == 'success'}
class:variant-filled-error={$message.type == 'error'}
class:variant-filled-warning={$message.type == 'warning'}
transition:fade|local={{ duration: 200 }}
>
<!-- Icon -->
<!-- <AlertTriangle /> -->
<!-- Message -->
<div class="alert-message">
{#if $message}
<p class="font-medium">{$message.message}</p>
{/if}
</div>
<!-- Actions -->
<!-- <div class="alert-actions">
<button class="btn-icon variant-filled"><X /></button>
</div> -->
</aside>
{/if}
{#if $errors._errors}
<aside class="alert mt-6" class:variant-filled-error={$page.status >= 400} transition:fade|local={{ duration: 200 }}>
<div class="alert-message">
<h3 class="h3">{m.auth_labels_reset_password_problem()}</h3>
<ul class="list">
{#each $errors._errors as error}
<li>
<span><AlertTriangle /></span>
<span class="flex-auto">{error}</span>
</li>
{/each}
</ul>
</div>
</aside>
{/if}

<form method="POST" use:enhance>
<div class="mt-6">
<label class="label">
<span class="sr-only">{m.auth_forms_email_label()}</span>
<input
name="email"
type="email"
class="input"
autocomplete="email"
placeholder={m.auth_forms_email_placeholder()}
data-invalid={$errors.email}
bind:value={$form.email}
class:input-error={$errors.email}
{...$constraints.email}
/>
{#if $errors.email}
<small>{$errors.email}</small>
{/if}
</label>
</div>
<div class="mt-6">
<button type="submit" class="variant-filled-primary btn w-full">
{#if $timeout}
<MoreHorizontal class="animate-ping" />
{:else if $delayed}
<Loader class="animate-spin" />
<!-- <ConicGradient stops={conicStops} spin width="w-6" /> -->
{:else}
{m.auth_labels_reset_password()}
{/if}
</button>
</div>
</form>

<DebugShell>
<SuperDebug
label="Miscellaneous"
status={false}
data={{
message: $message,
submitting: $submitting,
delayed: $delayed,
timeout: $timeout,
posted: $posted,
}}
/>
<br />
<SuperDebug label="Form" data={$form} />
<br />
<SuperDebug label="Tainted" status={false} data={$tainted} />
<br />
<SuperDebug label="Errors" status={false} data={$errors} />
<br />
<SuperDebug label="Constraints" status={false} data={$constraints} />
<!-- <br />
<SuperDebug label="$page data" status={false} data={$page} /> -->
</DebugShell>
13 changes: 13 additions & 0 deletions apps/console/src/routes/(auth)/reset/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { i18n } from '$lib/i18n';
import { resetPasswordSchema } from '$lib/schema/user';
import { isAuthenticated } from '$lib/stores/user';
import { redirect } from '@sveltejs/kit';
import { get } from 'svelte/store';
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';

export const load = async (event) => {
if (get(isAuthenticated)) redirect(302, i18n.resolveRoute('/dashboard'));
const form = await superValidate(zod(resetPasswordSchema));
return { form };
};
8 changes: 4 additions & 4 deletions apps/console/src/routes/(auth)/signin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { getToastStore } from '@skeletonlabs/skeleton';
import { DebugShell } from '@spectacular/skeleton/components';
import { Icon } from '@spectacular/skeleton/components/icons';
import { Logger } from '@spectacular/utils';
import { AlertTriangle, Github, Loader, Mail, MoreHorizontal } from 'lucide-svelte';
import { Fingerprint } from 'lucide-svelte';
// import { SiGithub } from "@icons-pack/svelte-simple-icons";
import { AlertTriangle, Fingerprint, Github, Loader, Mail, MoreHorizontal } from 'lucide-svelte';
import { fade } from 'svelte/transition';
import SuperDebug, { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
export let data;
const log = new Logger('auth:signin');
const log = new Logger('auth:signin:browser');
const toastStore = getToastStore();
const {
Expand Down Expand Up @@ -223,7 +223,7 @@ $pwlForm.redirectTo = $page.url.searchParams.get('redirectTo') ?? $pwlForm.redir
</form>

<div class="mt-5 flex flex-row items-center justify-center">
<a href="/password-reset" class="anchor font-semibold">{m.auth_labels_forgot_password()}</a>
<a href="/reset" class="anchor font-semibold">{m.auth_labels_forgot_password()}</a>
</div>

<!-- Divider -->
Expand Down
14 changes: 2 additions & 12 deletions apps/console/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,7 @@ const modalComponentRegistry: Record<string, ModalComponent> = {
modalSearch: { ref: Search },
};
const noSidebarPaths = [
'/signin',
'/signup',
'/password-reset',
'/privacy',
'/terms',
'/docs',
'/blog',
'/about',
'/contact',
];
const noSidebarPaths = ['/signin', '/signup', '/reset', '/privacy', '/terms', '/docs', '/blog', '/about', '/contact'];
function matchNoSidebarPaths(pathname: string): boolean {
const canonicalPath = i18n.route(pathname);
if (canonicalPath === '/' || startsWith(canonicalPath, noSidebarPaths)) {
Expand Down Expand Up @@ -101,7 +91,7 @@ $: if (browser) {
// nhost.auth.client.interpreter?.send('SIGNOUT');
(async () => {
const { error } = await nhost.auth.signOut();
if (error) log.error({ error })
if (error) log.error({ error });
})();
}
}
Expand Down

0 comments on commit 4782df5

Please sign in to comment.