adds password reset

This commit is contained in:
Christian Beutel
2024-11-17 21:49:46 +01:00
parent 7c1304f159
commit b9ae01846b
30 changed files with 791 additions and 247 deletions

View File

@@ -21,6 +21,8 @@
"author": "Autor",
"avatar": "Avatar",
"average-speed": "Durchschn. Geschwindigkeit",
"back": "",
"back-to-login": "",
"basic-info": "Basisinformation",
"before": "Vor",
"by": "von",
@@ -80,6 +82,7 @@
"entry": "Eintrag",
"error-creating-user": "Fehler beim Erstellen des Nutzers",
"error-during-login": "Fehler beim Login",
"error-during-password-reset": "",
"error-exporting-trail": "Fehler beim Exportieren des Trails",
"error-printing-map": "Fehler beim Drucken der Karte",
"error-reading-file": "Fehler beim Lesen der Datei",
@@ -97,6 +100,7 @@
"filter-categories": "Kategorien filtern",
"filter-difficulty": "Schwierigkeit filtern",
"focus-map-on": "Karte fokussieren auf",
"forgot-your-password": "",
"french": "Französisch",
"german": "Deutsch",
"get-position-from-exif": "Koordinaten aus EXIF Daten",
@@ -148,6 +152,9 @@
"near": "Nahe",
"new-list": "Neue Liste",
"new-password": "Neues Passwort",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Neue Route",
"no-account": "Du hast noch kein Konto?",
"no-data": "Keine Daten",
@@ -160,7 +167,11 @@
"orientation": "Ausrichtung",
"paper-size": "Papierformat",
"password": "Passwort",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "Passwort aktualisiert",
"passwords-must-match": "",
"photos": "Fotos",
"pick-a-trail": "Route auswählen",
"polish": "Polnisch",
@@ -173,6 +184,7 @@
"register": "Registrieren",
"removed-trail-from": "Route entfernt aus",
"required": "Pflichtfeld",
"reset-password": "",
"save": "Speichern",
"save-list": "Liste speichern",
"save-trail": "Route speichern",

View File

@@ -21,6 +21,8 @@
"author": "Author",
"avatar": "Avatar",
"average-speed": "Avg. Speed",
"back": "Back",
"back-to-login": "Back to Login",
"basic-info": "Basic Info",
"before": "Before",
"by": "by",
@@ -80,6 +82,7 @@
"entry": "Entry",
"error-creating-user": "Error creating user",
"error-during-login": "Error during login",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
"error-printing-map": "Error printing map",
"error-reading-file": "Error reading file",
@@ -97,6 +100,7 @@
"filter-categories": "Filter categories",
"filter-difficulty": "Filter difficulty",
"focus-map-on": "Focus map on",
"forgot-your-password": "Forgot your password?",
"french": "French",
"german": "German",
"get-position-from-exif": "Get coordinates from EXIF data",
@@ -148,6 +152,9 @@
"near": "Near",
"new-list": "New List",
"new-password": "New password",
"new-password-error": "Error setting new password",
"new-password-success": "The new password has been set",
"new-password-text": "Set a new password",
"new-trail": "New Trail",
"no-account": "Don't have an account?",
"no-data": "No data",
@@ -160,7 +167,11 @@
"orientation": "Orientation",
"paper-size": "Paper size",
"password": "Password",
"password-confirm": "Confirm password",
"password-reset-sent": "A password reset email has been sent",
"password-reset-text": "We will send a reset link to your email address.",
"password-updated": "Password updated",
"passwords-must-match": "Passwords must match",
"photos": "Photos",
"pick-a-trail": "Pick a trail",
"polish": "Polish",
@@ -173,6 +184,7 @@
"register": "Register",
"removed-trail-from": "Removed trail from",
"required": "Required",
"reset-password": "Reset Password",
"save": "Save",
"save-list": "Save List",
"save-trail": "Save Trail",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "Avatar",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "Informations de base",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "Entrée",
"error-creating-user": "Erreur durant la création de l'utilisateur",
"error-during-login": "Erreur durant la connexion",
"error-during-password-reset": "",
"error-exporting-trail": "",
"error-printing-map": "Erreur d'impression de la carte",
"error-reading-file": "Erreur de lecture du fichier",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "Français",
"german": "Allemand",
"get-position-from-exif": "",
@@ -148,6 +152,9 @@
"near": "Proche",
"new-list": "Nouvelle liste",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Nouvel itinéraire",
"no-account": "Pas encore de compte ?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "Orientation",
"paper-size": "Taille de papier",
"password": "Mot de passe",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "Photos",
"pick-a-trail": "Choisir un itinéraire",
"polish": "polonais",
@@ -173,6 +184,7 @@
"register": "S'enregistrer",
"removed-trail-from": "Enlever l'itinéraire de",
"required": "Requis",
"reset-password": "",
"save": "Sauvegarder",
"save-list": "",
"save-trail": "Sauvegarder l'itinéraire",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "Avatar",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "Alap információk",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "Bejegyzés",
"error-creating-user": "Hiba felhasználó hozzáadása közben",
"error-during-login": "Hiba bejelentkezés közben",
"error-during-password-reset": "",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "Hiba a fájl olvasása közben",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "Francia",
"german": "Német",
"get-position-from-exif": "",
@@ -148,6 +152,9 @@
"near": "Közelben",
"new-list": "Új lista",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Új útvonal",
"no-account": "Nincs még fiókja?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "",
"paper-size": "",
"password": "Jelszó",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "Képek",
"pick-a-trail": "Válasszon útvonalat",
"polish": "Lengyel",
@@ -173,6 +184,7 @@
"register": "Regisztráció",
"removed-trail-from": "Eltávolított nyomvonal a",
"required": "Kötelező",
"reset-password": "",
"save": "Mentés",
"save-list": "",
"save-trail": "Útvonal mentése",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "Avatar",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "Informazioni di base",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "Voce",
"error-creating-user": "Errore durante la creazione dell'utente",
"error-during-login": "Errore durante il login",
"error-during-password-reset": "",
"error-exporting-trail": "Errore durante l'esportazione del percorso",
"error-printing-map": "Errore durante la stampa della mappa",
"error-reading-file": "Errore durante la lettura del file",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "Francese",
"german": "Tedesco",
"get-position-from-exif": "Ottieni posizione da dati EXIF",
@@ -148,6 +152,9 @@
"near": "Vicino",
"new-list": "Nuova lista",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Nuovo percorso",
"no-account": "Non hai ancora un account?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "Orientamento",
"paper-size": "Formato carta",
"password": "Password",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "Foto",
"pick-a-trail": "Scegli un percorso",
"polish": "Polacco",
@@ -173,6 +184,7 @@
"register": "Registrati",
"removed-trail-from": "Percorso rimosso da",
"required": "Obbligatorio",
"reset-password": "",
"save": "Salva",
"save-list": "",
"save-trail": "Salva percorso",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "Profielfoto",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "Algemene informatie",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "Item",
"error-creating-user": "Het account kan niet worden aangemaakt",
"error-during-login": "Het inloggen is mislukt",
"error-during-password-reset": "",
"error-exporting-trail": "",
"error-printing-map": "Fout bij afdrukken van kaart",
"error-reading-file": "Het bestand kan niet worden ingelezen",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "Frans",
"german": "Duits",
"get-position-from-exif": "",
@@ -148,6 +152,9 @@
"near": "Nabij",
"new-list": "Nieuwe lijst",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Nieuwe wandelroute",
"no-account": "Heb je nog geen account?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "Oriëntatie",
"paper-size": "Papierformaat",
"password": "Wachtwoord",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "Foto's",
"pick-a-trail": "Kies een wandelroute",
"polish": "Pools",
@@ -173,6 +184,7 @@
"register": "Registreren",
"removed-trail-from": "De wandelroute is verwijderd van",
"required": "Verplicht",
"reset-password": "",
"save": "Bewaren",
"save-list": "",
"save-trail": "Wandelroute bewaren",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "Awatar",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "Podstawowe informacje",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "Pozycja",
"error-creating-user": "Błąd tworzenia użytkownika",
"error-during-login": "Błąd podczas logowania",
"error-during-password-reset": "",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "Błąd wczytywania pliku",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "Francuski",
"german": "Niemiecki",
"get-position-from-exif": "",
@@ -148,6 +152,9 @@
"near": "Blisko",
"new-list": "Nowa Lista",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Nowa ścieżka",
"no-account": "Nie masz konta?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "",
"paper-size": "",
"password": "Hasło",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "Zdjęcia",
"pick-a-trail": "Wybierz ścieżkę",
"polish": "Polski",
@@ -173,6 +184,7 @@
"register": "Zarejestruj",
"removed-trail-from": "Usunięto ścieżkę z",
"required": "Wymagane",
"reset-password": "",
"save": "Zapisz",
"save-list": "",
"save-trail": "Zapisz ścieżkę",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "Avatar",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "Informações básicas",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "Entrada",
"error-creating-user": "Erro ao criar usuário",
"error-during-login": "Erro durante o login",
"error-during-password-reset": "",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "Erro ao ler o arquivo",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "Francês",
"german": "Alemão",
"get-position-from-exif": "",
@@ -148,6 +152,9 @@
"near": "Próximo",
"new-list": "Nova lista",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "Nova trilha",
"no-account": "Não tem uma conta?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "",
"paper-size": "",
"password": "Senha",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "Fotos",
"pick-a-trail": "Escolha uma trilha",
"polish": "Polonês",
@@ -173,6 +184,7 @@
"register": "Registo",
"removed-trail-from": "Trilha removida de",
"required": "Obrigatório",
"reset-password": "",
"save": "Guardar",
"save-list": "",
"save-trail": "Guardar trilho",

View File

@@ -21,6 +21,8 @@
"author": "",
"avatar": "头像",
"average-speed": "",
"back": "",
"back-to-login": "",
"basic-info": "基本信息",
"before": "",
"by": "",
@@ -80,6 +82,7 @@
"entry": "日程",
"error-creating-user": "创建用户错误",
"error-during-login": "登录错误",
"error-during-password-reset": "",
"error-exporting-trail": "",
"error-printing-map": "",
"error-reading-file": "读取文件错误",
@@ -97,6 +100,7 @@
"filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "",
"forgot-your-password": "",
"french": "法语",
"german": "德语",
"get-position-from-exif": "",
@@ -148,6 +152,9 @@
"near": "附近",
"new-list": "新列表",
"new-password": "",
"new-password-error": "",
"new-password-success": "",
"new-password-text": "",
"new-trail": "创建新路线",
"no-account": "还未注册?",
"no-data": "",
@@ -160,7 +167,11 @@
"orientation": "",
"paper-size": "",
"password": "密码",
"password-confirm": "",
"password-reset-sent": "",
"password-reset-text": "",
"password-updated": "",
"passwords-must-match": "",
"photos": "图片",
"pick-a-trail": "选择一个路线",
"polish": "波兰语",
@@ -173,6 +184,7 @@
"register": "注册",
"removed-trail-from": "路线已删除自",
"required": "必填",
"reset-password": "",
"save": "保存",
"save-list": "",
"save-trail": "保存路线",

View File

@@ -56,7 +56,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
r = await f('/api/v1/trail?' + new URLSearchParams({
expand: "category,waypoints,summit_logs,trail_share_via_trail",
filter: trailIds.map((id: Record<string, any>) => `id="${id}"`).join('||'),
filter: `'${trailIds.join(',')}'~id`,
sort: `${filter.sortOrder}${filter.sort}`
}), {
method: 'GET',
@@ -101,7 +101,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
}
r = await fetch('/api/v1/trail?' + new URLSearchParams({
filter: trailIds.map((id: Record<string, any>) => `id="${id}"`).join("||"),
filter: `'${trailIds.join(',')}'~id`,
expand: "category,waypoints,summit_logs",
sort: `+name`,
}), {

View File

@@ -133,5 +133,30 @@ export async function users_delete(user: User) {
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
}
export async function users_reset_password(reset: { email: string }) {
const r = await fetch('/api/v1/auth/reset', {
method: 'POST',
body: JSON.stringify(reset),
})
if (r.ok) {
return await r.json();
} else {
throw new ClientResponseError(await r.json())
}
}
export async function users_confirm_reset(reset: { password: string, passwordConfirm: string, token: string }) {
const r = await fetch('/api/v1/auth/confirm-reset', {
method: 'POST',
body: JSON.stringify(reset),
})
if (r.ok) {
return await r.json();
} else {
throw new ClientResponseError(await r.json())
}
}

View File

@@ -0,0 +1,26 @@
<script>
import { page } from "$app/stores";
</script>
<div
class="flex flex-col gap-y-4 items-center justify-center relative"
style="height: calc(100dvh - 120px); font-size: 4rem"
>
<img
class="absolute top-1/2 left-1/2 aspect-square rounded-xl object-cover"
width="500"
src="/imgs/error.webp"
alt="Default error img"
/>
<div
class="z-10 rounded-full p-8 bg-background flex flex-col gap-y-4 items-center"
style="max-width: 400px"
>
<h1 class="font-bold" style="line-height: 4rem;">
{$page.status}
</h1>
<p class="text-lg text-center">
{$page.error?.message}
</p>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
import { ClientResponseError } from "pocketbase";
export async function POST(event: RequestEvent) {
try {
let data;
try {
data = await event.request.json()
} catch (e) {
throw new ClientResponseError({ status: 400, response: { message: "Invalid json" } })
}
const r = await pb.collection('users').confirmPasswordReset(data.token, data.password, data.passwordConfirm);
return json(r);
} catch (e: any) {
throw error(e.status, e);
}
}

View File

@@ -0,0 +1,20 @@
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
import { ClientResponseError } from "pocketbase";
export async function POST(event: RequestEvent) {
try {
let data;
try {
data = await event.request.json()
} catch (e) {
throw new ClientResponseError({ status: 400, response: { message: "Invalid json" } })
}
const r = await pb.collection('users').requestPasswordReset(data.email);
return json(r);
} catch (e: any) {
throw error(e.status, e);
}
}

View File

@@ -0,0 +1,7 @@
import { error, type ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
if(!params.token) {
error(400, "Invalid token")
}
};

View File

@@ -0,0 +1,98 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import Button from "$lib/components/base/button.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
import type { User } from "$lib/models/user";
import { theme } from "$lib/stores/theme_store";
import { show_toast } from "$lib/stores/toast_store";
import {
users_confirm_reset,
users_reset_password,
} from "$lib/stores/user_store";
import { createForm } from "$lib/vendor/svelte-form-lib";
import { _ } from "svelte-i18n";
import { object, ref, string } from "yup";
let loading: boolean = false;
const { form, errors, handleChange, handleSubmit } = createForm({
initialValues: {
password: "",
passwordConfirm: "",
},
validationSchema: object({
password: string().required($_("required")),
passwordConfirm: string()
.required($_("required"))
.oneOf([ref("password"), ""], $_('passwords-must-match')),
}),
onSubmit: async (form) => {
loading = true;
try {
await users_confirm_reset({
...form,
token: $page.params.token,
});
goto("/login");
show_toast({
icon: "check",
type: "success",
text: $_("new-password-success"),
});
} catch (e) {
show_toast({
icon: "close",
type: "error",
text: $_("new-password-error"),
});
} finally {
loading = false;
}
},
});
</script>
<svelte:head>
<title>{$_("login")} | wanderer</title>
</svelte:head>
<main class="flex justify-center">
<form
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-4 w-[28rem] mt-8"
on:submit={handleSubmit}
>
{#if $theme == "light"}
<LogoTextTwoLineDark></LogoTextTwoLineDark>
{:else}
<LogoTextTwoLineLight></LogoTextTwoLineLight>
{/if}
<h4 class="text-xl font-semibold">{$_("new-password-text")}</h4>
<div class="space-y-6 w-80">
<TextField
name="password"
label={$_("password")}
type="password"
bind:value={$form.password}
on:change={handleChange}
error={$errors.password}
></TextField>
<TextField
name="password"
label={$_("password-confirm")}
type="password"
bind:value={$form.passwordConfirm}
on:change={handleChange}
error={$errors.passwordConfirm}
></TextField>
<Button
primary={true}
extraClasses={"min-w-full"}
type="submit"
{loading}>{$_("reset-password")}</Button
>
</div>
</form>
</main>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { goto } from "$app/navigation";
import Button from "$lib/components/base/button.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
import type { User } from "$lib/models/user";
import { theme } from "$lib/stores/theme_store";
import { show_toast } from "$lib/stores/toast_store";
import { users_reset_password } from "$lib/stores/user_store";
import { createForm } from "$lib/vendor/svelte-form-lib";
import { _ } from "svelte-i18n";
import { object, string } from "yup";
let loading: boolean = false;
const { form, errors, handleChange, handleSubmit } = createForm({
initialValues: {
email: "",
},
validationSchema: object<User>({
email: string().required($_("required")),
}),
onSubmit: async (email) => {
loading = true;
try {
await users_reset_password(email);
goto("/");
show_toast({
icon: "check",
type: "success",
text: $_("password-reset-sent"),
});
} catch (e) {
show_toast({
icon: "close",
type: "error",
text: $_("error-during-password-reset"),
});
} finally {
loading = false;
}
},
});
</script>
<svelte:head>
<title>{$_("login")} | wanderer</title>
</svelte:head>
<main class="flex justify-center">
<form
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-4 w-[28rem] mt-8"
on:submit={handleSubmit}
>
{#if $theme == "light"}
<LogoTextTwoLineDark></LogoTextTwoLineDark>
{:else}
<LogoTextTwoLineLight></LogoTextTwoLineLight>
{/if}
<h4 class="text-xl font-semibold">{$_("forgot-your-password")}</h4>
<p class="text-gray-500">{$_('password-reset-text')}</p>
<div class="space-y-6 w-80">
<TextField
name="email"
label={$_("email")}
bind:value={$form.email}
on:change={handleChange}
error={$errors.email}
></TextField>
<div class="grid grid-cols-2 gap-x-4">
<a class="btn-secondary text-center" href="/login"
>{$_("back-to-login")}</a
>
<Button
primary={true}
extraClasses={"min-w-full"}
type="submit"
{loading}>{$_("reset-password")}</Button
>
</div>
</div>
</form>
</main>

View File

@@ -97,14 +97,20 @@
on:change={handleChange}
error={$errors.username}
></TextField>
<TextField
name="password"
label={$_("password")}
type="password"
bind:value={$form.password}
on:change={handleChange}
error={$errors.password}
></TextField>
<div class="flex flex-col">
<TextField
name="password"
label={$_("password")}
type="password"
bind:value={$form.password}
on:change={handleChange}
error={$errors.password}
></TextField>
<a
class="text-xs underline decoration-dashed float-end mt-1 self-end"
href="/auth/reset">{$_("forgot-your-password")}</a
>
</div>
<Button
primary={true}
extraClasses={"min-w-full"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 176 KiB

BIN
web/static/imgs/error.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB