mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: ♻️ Rewrite password reset page, polish auth
This commit is contained in:
parent
7cd71f252e
commit
f672ce5a69
|
|
@ -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>
|
||||
|
|
|
|||
16
components/ui/alert/Alert.vue
Normal file
16
components/ui/alert/Alert.vue
Normal 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>
|
||||
14
components/ui/alert/AlertDescription.vue
Normal file
14
components/ui/alert/AlertDescription.vue
Normal 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>
|
||||
14
components/ui/alert/AlertTitle.vue
Normal file
14
components/ui/alert/AlertTitle.vue
Normal 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>
|
||||
23
components/ui/alert/index.ts
Normal file
23
components/ui/alert/index.ts
Normal 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>;
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 “{{ application }}”?</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue