refactor: ♻️ Rewrite password reset page, polish auth

This commit is contained in:
Jesse Wierzbinski 2024-12-03 12:30:10 +01:00
parent 7cd71f252e
commit f672ce5a69
No known key found for this signature in database
12 changed files with 205 additions and 105 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -43,10 +43,6 @@ const form = useForm({
validationSchema: formSchema,
});
const onSubmit = form.handleSubmit((values) => {
console.info("Form submitted!", values);
});
const redirectUrl = new URL("/api/auth/login", `https://${instance.domain}`);
const params = useUrlSearchParams();
@ -85,7 +81,7 @@ const issuerRedirectUrl = (issuerId: string) => {
<template>
<div class="grid gap-6">
<form @submit="onSubmit" method="post" :action="redirectUrl.toString()">
<form @submit="form.submitForm" method="post" :action="redirectUrl.toString()">
<div class="grid gap-6">
<FormField v-slot="{ componentField }" name="identifier">
<FormItem>
@ -93,7 +89,7 @@ const issuerRedirectUrl = (issuerId: string) => {
Email (or username)
</FormLabel>
<FormControl>
<Input placeholder="petergriffin" type="email" auto-capitalize="none"
<Input placeholder="petergriffin" type="text" auto-capitalize="none"
auto-complete="idenfifier" auto-correct="off" :disabled="isLoading"
v-bind="componentField" />
</FormControl>

View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
import { type AlertVariants, alertVariants } from ".";
const props = defineProps<{
class?: HTMLAttributes["class"];
variant?: AlertVariants["variant"];
}>();
</script>
<template>
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
<slot />
</h5>
</template>

View file

@ -0,0 +1,23 @@
import { type VariantProps, cva } from "class-variance-authority";
export { default as Alert } from "./Alert.vue";
export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
export type AlertVariants = VariantProps<typeof alertVariants>;

View file

@ -4,7 +4,6 @@ export default defineNuxtConfig({
"@nuxtjs/tailwindcss",
"@vueuse/nuxt",
"@nuxt/fonts",
"@vee-validate/nuxt",
"nuxt-security",
"@vite-pwa/nuxt",
"shadcn-nuxt",
@ -258,15 +257,6 @@ export default defineNuxtConfig({
},
],
},
veeValidate: {
autoImports: true,
componentNames: {
Form: "VeeForm",
Field: "VeeField",
FieldArray: "VeeFieldArray",
ErrorMessage: "VeeErrorMessage",
},
},
runtimeConfig: {
public: {
apiHost: "https://social.lysand.org",

View file

@ -33,7 +33,6 @@
"@nuxt/fonts": "^0.10.2",
"@nuxtjs/color-mode": "3.5.2",
"@tailwindcss/typography": "^0.5.15",
"@vee-validate/nuxt": "^4.14.7",
"@vee-validate/zod": "^4.14.7",
"@versia/client": "0.1.3",
"@vite-pwa/nuxt": "^0.10.6",

View file

@ -1,7 +1,8 @@
<script setup lang="ts">
import { Client } from "@versia/client";
import { Loader } from "lucide-vue-next";
import { AlertCircle, Loader } from "lucide-vue-next";
import UserAuthForm from "~/components/oauth/login.vue";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Button } from "~/components/ui/button";
import { NuxtLink } from "#components";
@ -11,6 +12,7 @@ const {
const host = new URL(apiHost).host;
const instance = useInstanceFromClient(new Client(new URL(useBaseUrl().value)));
const { error, error_description } = useUrlSearchParams();
</script>
<template>
@ -44,6 +46,13 @@ const instance = useInstanceFromClient(new Client(new URL(useBaseUrl().value)));
</div>
<div class="lg:p-8 w-full max-w-xl">
<div class="mx-auto flex w-full flex-col justify-center space-y-10 sm:w-[350px]">
<Alert v-if="error" variant="destructive" class="mb-4">
<AlertCircle class="w-4 h-4" />
<AlertTitle>{{ error }}</AlertTitle>
<AlertDescription>
{{ error_description }}
</AlertDescription>
</Alert>
<div class="flex flex-col space-y-2 text-center">
<h1 class="text-2xl font-semibold tracking-tight">
Log in to your account.

View file

@ -2,7 +2,8 @@
<div class="flex h-svh items-center justify-center px-6 py-12 lg:px-8 bg-center bg-no-repeat bg-cover" :style="{
backgroundImage: 'url(/images/banner.webp)'
}">
<Card class="w-full max-w-md">
<Card class="w-full max-w-md" as="form" method="POST" :action="url.pathname.replace('/oauth/consent', '/oauth/authorize')">
<input type="hidden" v-for="([key, value]) in url.searchParams" :key="key" :name="key" :value="value" />
<CardHeader>
<CardTitle as="h1" class="text-2xl break-words">Authorize &ldquo;{{ application }}&rdquo;?</CardTitle>
</CardHeader>

View file

@ -1,88 +1,129 @@
<template>
<div class="flex min-h-screen relative flex-col gap-10 justify-center py-12 px-8">
<img crossorigin="anonymous" src="https://cdn.versia.pub/branding/icon.svg" alt="Versia logo"
class="mx-auto hidden md:inline-block h-20 ring-1 ring-white/20 rounded" />
<div v-if="validUrlParameters" class="mx-auto w-full max-w-md">
<VeeForm class="flex flex-col gap-y-6" method="POST" :validation-schema="schema" action="/api/auth/reset">
<input type="hidden" name="token" :value="params.token" />
<h1 class="font-bold text-2xl text-gray-50 text-center tracking-tight">Reset your password</h1>
<div v-if="error" class="ring-1 ring-white/10 rounded p-4 bg-red-500 text-white">
<h2 class="font-bold text-lg">An error occured</h2>
<span class="text-sm">{{ params.error_description }}</span>
</div>
<VeeField name="password" v-slot="{ errorMessage, field }" validate-on-change>
<Field>
<LabelAndError>
<Label for="password">New password</Label>
<FieldError v-if="errorMessage">{{ errorMessage }}</FieldError>
</LabelAndError>
<PasswordInput id="password" placeholder="hunter2" autocomplete="new-password" required
v-bind="field" :is-invalid="!!errorMessage" :show-strength="true" />
</Field>
</VeeField>
<VeeField name="password-confirm" as="div" v-slot="{ errors, field }" validate-on-change>
<Field>
<LabelAndError>
<Label for="password-confirm">Confirm password</Label>
<FieldError v-if="errors.length > 0">{{ errors[0] }}</FieldError>
</LabelAndError>
<PasswordInput id="password-confirm" placeholder="hunter2" autocomplete="new-password" required
v-bind="field" :is-invalid="errors.length > 0" />
</Field>
</VeeField>
<p class="text-xs font-semibold text-red-300">This will reset your
password. Make sure to put it in a password manager.
</p>
<Button theme="primary" type="submit" class="w-full">Reset</Button>
</VeeForm>
</div>
<div v-else-if="params.success">
<h1 class="text-2xl font-bold tracking-tight text-gray-50 sm:text-4xl text-center">Password reset
successful!
</h1>
<p class="mt-6 text-lg leading-8 text-gray-300 text-center">
You can now login to your account with your new password.
</p>
</div>
<div v-else class="mx-auto max-w-md">
<h1 class="text-2xl font-bold tracking-tight text-gray-50 sm:text-4xl">Invalid access
parameters
</h1>
<p class="mt-6 text-lg leading-8 text-gray-300">This page should be accessed
through a valid password reset request. Please ask your admin to reset your password.
</p>
<p class="mt-6 text-lg leading-8 text-gray-300">
Found a problem? Report it on <a href="https://github.com/versia-pub/server/issues/new/choose"
target="_blank" class="underline text-primary2-700">the issue tracker</a>.
</p>
</div>
<div class="flex h-svh items-center justify-center px-6 py-12 lg:px-8 bg-center bg-no-repeat bg-cover" :style="{
backgroundImage: 'url(/images/banner.webp)'
}">
<Card v-if="params.success" class="w-full max-w-md">
<CardHeader>
<CardTitle>Success</CardTitle>
<CardDescription>
Your password has been reset. You can now log in with your new password.
</CardDescription>
</CardHeader>
<CardFooter class="grid">
<Button :as="NuxtLink" href="/" variant="default">
Back to front page
</Button>
</CardFooter>
</Card>
<Card v-else class="w-full max-w-md">
<form method="POST" action="/api/auth/reset" @submit="form.submitForm">
<CardHeader>
<Alert v-if="params.login_reset" variant="default" class="mb-4">
<AlertCircle class="w-4 h-4" />
<AlertTitle>Info</AlertTitle>
<AlertDescription>
Your password has been reset by an administrator. Please change it here.
</AlertDescription>
</Alert>
<Alert v-if="params.error" variant="destructive" class="mb-4">
<AlertCircle class="w-4 h-4" />
<AlertTitle>{{ params.error }}</AlertTitle>
<AlertDescription>
{{ params.error_description }}
</AlertDescription>
</Alert>
<CardTitle as="h1">Reset your password</CardTitle>
<CardDescription>
Enter your new password below. Make sure to put it in a password manager.
</CardDescription>
</CardHeader>
<CardContent class="grid gap-6">
<FormField v-slot="{ componentField }" name="token">
<FormItem>
<FormControl>
<input type="hidden" v-bind="componentField" />
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>
New password
</FormLabel>
<FormControl>
<Input placeholder="hunter2" type="password" auto-capitalize="none" auto-correct="off"
v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password-confirm">
<FormItem>
<FormLabel>
Confirm password
</FormLabel>
<FormControl>
<Input placeholder="hunter2" type="password" auto-capitalize="none" auto-correct="off"
v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</CardContent>
<CardFooter class="grid gap-2">
<Button variant="default" type="submit">Reset</Button>
</CardFooter>
</form>
</Card>
</div>
</template>
<script setup lang="ts">
import { toTypedSchema } from "@vee-validate/zod";
import { AlertCircle } from "lucide-vue-next";
import { useForm } from "vee-validate";
import { z } from "zod";
import FieldError from "~/components/inputs/field-error.vue";
import Field from "~/components/inputs/field.vue";
import LabelAndError from "~/components/inputs/label-and-error.vue";
import Label from "~/components/inputs/label.vue";
import PasswordInput from "~/components/inputs/password-input.vue";
import Button from "~/packages/ui/components/buttons/button.vue";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { NuxtLink } from "#components";
identity.value = null;
const schema = toTypedSchema(
const formSchema = toTypedSchema(
z
.object({
password: z.string().min(3).max(100),
"password-confirm": z.string().min(3).max(100),
token: z.string(),
password: z
.string()
.min(3, {
message: "Must be at least 3 characters long",
})
.max(100, {
message: "Must be at most 100 characters long",
}),
"password-confirm": z
.string()
.min(3, {
message: "Must be at least 3 characters long",
})
.max(100, {
message: "Must be at most 100 characters long",
}),
})
.superRefine((data, ctx) => {
if (data.password !== data["password-confirm"]) {
@ -97,14 +138,11 @@ const schema = toTypedSchema(
);
const params = useUrlSearchParams();
let error = params.error;
let errorDescription = params.error_description;
if (params.login_reset) {
error = "Login reset";
errorDescription =
"Your password has been reset by an administrator. Please change it here.";
}
const validUrlParameters = !!params.token;
const form = useForm({
validationSchema: formSchema,
initialValues: {
token: (params.token as string) ?? undefined,
},
});
</script>

View file

@ -1,5 +1,5 @@
<template>
<SettingsSidebar>
<!-- <SettingsSidebar>
<template #behaviour>
<SettingPage :page="SettingPages.Behaviour" />
</template>
@ -12,11 +12,11 @@
<template #emojis>
<EmojiEditor />
</template>
</SettingsSidebar>
</SettingsSidebar> -->
</template>
<script setup lang="ts">
import EmojiEditor from "~/components/settings/emojis/emojis.vue";
/* import EmojiEditor from "~/components/settings/emojis/emojis.vue";
import SettingPage from "~/components/settings/page.vue";
import ProfileEditor from "~/components/settings/profile-editor.vue";
import SettingsSidebar from "~/components/sidebars/settings-sidebar.vue";
@ -24,5 +24,5 @@ import { SettingPages } from "~/settings";
definePageMeta({
layout: "app",
});
}); */
</script>