Files
bknd/app/src/ui/elements/auth/AuthForm.tsx

132 lines
4.1 KiB
TypeScript

import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
import clsx from "clsx";
import { Type } from "core/utils";
import { Form } from "json-schema-form-react";
import { transform } from "lodash-es";
import type { ComponentPropsWithoutRef } from "react";
import { Button } from "ui/components/buttons/Button";
import { Group, Input, Label } from "ui/components/form/Formy/components";
import { SocialLink } from "./SocialLink";
import type { ValueError } from "@sinclair/typebox/value";
import { type TSchema, Value } from "core/utils";
import type { Validator } from "json-schema-form-react";
import { useTheme } from "ui/client/use-theme";
class TypeboxValidator implements Validator<ValueError> {
async validate(schema: TSchema, data: any) {
return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)];
}
}
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
className?: string;
formData?: any;
action: "login" | "register";
method?: "POST" | "GET";
auth?: Partial<Pick<AppAuthSchema, "basepath" | "strategies">>;
buttonLabel?: string;
};
const validator = new TypeboxValidator();
const schema = Type.Object({
email: Type.String({
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
}),
password: Type.String({
minLength: 8, // @todo: this should be configurable
}),
});
export function AuthForm({
formData,
className,
method = "POST",
action,
auth,
buttonLabel = action === "login" ? "Sign in" : "Sign up",
...props
}: LoginFormProps) {
const { theme } = useTheme();
const basepath = auth?.basepath ?? "/api/auth";
const password = {
action: `${basepath}/password/${action}`,
strategy: auth?.strategies?.password ?? ({ type: "password" } as const),
};
const oauth = transform(
auth?.strategies ?? {},
(result, value, key) => {
if (value.type !== "password") {
result[key] = value.config;
}
},
{},
) as Record<string, AppAuthOAuthStrategy>;
const has_oauth = Object.keys(oauth).length > 0;
return (
<div className="flex flex-col gap-4 w-full">
{has_oauth && (
<>
<div>
{Object.entries(oauth)?.map(([name, oauth], key) => (
<SocialLink
provider={name}
method={method}
basepath={basepath}
key={key}
action={action}
/>
))}
</div>
<Or />
</>
)}
<Form
method={method}
action={password.action}
{...(props as any)}
schema={schema}
validator={validator}
validationMode="change"
className={clsx("flex flex-col gap-3 w-full", className)}
>
{({ errors, submitting }) => (
<>
<Group>
<Label htmlFor="email">Email address</Label>
<Input type="email" name="email" />
</Group>
<Group>
<Label htmlFor="password">Password</Label>
<Input type="password" name="password" />
</Group>
<Button
type="submit"
variant="primary"
size="large"
className="w-full mt-2 justify-center"
disabled={errors.length > 0 || submitting}
>
{buttonLabel}
</Button>
</>
)}
</Form>
</div>
);
}
const Or = () => (
<div className="w-full flex flex-row items-center">
<div className="relative flex grow">
<div className="h-px bg-primary/10 w-full absolute top-[50%] z-0" />
</div>
<div className="mx-5">or</div>
<div className="relative flex grow">
<div className="h-px bg-primary/10 w-full absolute top-[50%] z-0" />
</div>
</div>
);