reworked admin auth to use form and cookie + adjusted oauth to support API and cookie-based auth

This commit is contained in:
dswbx
2024-11-23 18:12:19 +01:00
parent f70e2b2e10
commit 824ff40133
30 changed files with 630 additions and 483 deletions

View File

@@ -1,117 +1,55 @@
import { type FieldApi, useForm } from "@tanstack/react-form";
import { Type, type TypeInvalidError, parse } from "core/utils";
import { Button } from "ui/components/buttons/Button";
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Type } from "core/utils";
import type { ComponentPropsWithoutRef } from "react";
import { useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { Button } from "ui";
import * as Formy from "ui/components/form/Formy";
type LoginFormProps = {
onSubmitted?: (value: { email: string; password: string }) => Promise<void>;
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit"> & {
className?: string;
formData?: any;
};
export function LoginForm({ onSubmitted }: LoginFormProps) {
const form = useForm({
defaultValues: {
email: "",
password: ""
},
onSubmit: async ({ value }) => {
onSubmitted?.(value);
},
defaultState: {
canSubmit: false,
isValid: false
},
validatorAdapter: () => {
function validate(
{ value, fieldApi }: { value: any; fieldApi: FieldApi<any, any> },
fn: any
): any {
if (fieldApi.form.state.submissionAttempts === 0) return;
const schema = Type.Object({
email: Type.String({
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
}),
password: Type.String({
minLength: 8 // @todo: this should be configurable
})
});
try {
parse(fn, value);
} catch (e) {
return (e as TypeInvalidError).errors
.map((error) => error.schema.error ?? error.message)
.join(", ");
}
}
return { validate, validateAsync: validate };
}
export function LoginForm({ formData, className, method = "POST", ...props }: LoginFormProps) {
const {
register,
formState: { isValid, errors }
} = useForm({
mode: "onChange",
defaultValues: formData,
resolver: typeboxResolver(schema)
});
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-3 w-full" noValidate>
<form.Field
name="email"
validators={{
onChange: Type.String({
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
})
}}
children={(field) => (
<Formy.Group error={field.state.meta.errors.length > 0}>
<Formy.Label htmlFor={field.name}>Email address</Formy.Label>
<Formy.Input
type="email"
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
</Formy.Group>
)}
/>
<form.Field
name="password"
validators={{
onChange: Type.String({
minLength: 8
})
}}
children={(field) => (
<Formy.Group error={field.state.meta.errors.length > 0}>
<Formy.Label htmlFor={field.name}>Password</Formy.Label>
<Formy.Input
type="password"
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
</Formy.Group>
)}
/>
<form.Subscribe
selector={(state) => {
//console.log("state", state, Object.values(state.fieldMeta));
const fieldMeta = Object.values(state.fieldMeta).map((f) => f.isDirty);
const allDirty = fieldMeta.length > 0 ? fieldMeta.reduce((a, b) => a && b) : false;
return [allDirty, state.isSubmitting];
}}
children={([allDirty, isSubmitting]) => {
return (
<Button
type="submit"
variant="primary"
size="large"
className="w-full mt-2 justify-center"
disabled={!allDirty || isSubmitting}
>
Sign in
</Button>
);
}}
/>
<form {...props} method={method} className={twMerge("flex flex-col gap-3 w-full", className)}>
<Formy.Group>
<Formy.Label htmlFor="email">Email address</Formy.Label>
<Formy.Input type="email" {...register("email")} />
</Formy.Group>
<Formy.Group>
<Formy.Label htmlFor="password">Password</Formy.Label>
<Formy.Input type="password" {...register("password")} />
</Formy.Group>
<Button
type="submit"
variant="primary"
size="large"
className="w-full mt-2 justify-center"
disabled={!isValid}
>
Sign in
</Button>
</form>
);
}

View File

@@ -0,0 +1,39 @@
import { getFlashMessage } from "core/server/flash";
import { useEffect, useState } from "react";
import { Alert } from "ui/components/display/Alert";
/**
* Handles flash message from server
* @constructor
*/
export function FlashMessage() {
const [flash, setFlash] = useState<any>();
useEffect(() => {
if (!flash) {
const content = getFlashMessage();
if (content) {
setFlash(content);
}
}
}, []);
if (flash) {
let Component = Alert.Info;
switch (flash.type) {
case "error":
Component = Alert.Exception;
break;
case "success":
Component = Alert.Success;
break;
case "warning":
Component = Alert.Warning;
break;
}
return <Component message={flash.message} className="justify-center" />;
}
return null;
}