feat: setup auth provider and refine with login, check and logout methods

This commit is contained in:
Derrick Hammer 2024-03-13 13:16:52 -04:00
parent 7032892686
commit 51618a2fda
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
6 changed files with 18990 additions and 195 deletions

View File

@ -1,37 +1,99 @@
import { AuthProvider } from "@refinedev/core" import type {AuthProvider} from "@refinedev/core"
import type {
AuthActionResponse,
CheckResponse,
OnErrorResponse
// @ts-ignore
} from "@refinedev/core/dist/interfaces/bindings/auth"
export const authProvider: AuthProvider = { // @ts-ignore
login: async (params: any) => { import type {AuthActionResponse, CheckResponse, OnErrorResponse} from "@refinedev/core/dist/interfaces/bindings/auth"
return { success: true } satisfies AuthActionResponse import {Sdk} from "@lumeweb/portal-sdk";
},
logout: async (params: any) => { export type AuthFormRequest = {
return { success: true } satisfies AuthActionResponse email: string;
}, password: string;
check: async (params?: any) => { rememberMe: boolean;
return { authenticated: true } satisfies CheckResponse }
},
onError: async (error: any) => { export class PortalAuthProvider implements RequiredAuthProvider {
return { logout: true } satisfies OnErrorResponse private apiUrl: string;
}, private sdk: Sdk;
register: async (params: any) => {
return { success: true } satisfies AuthActionResponse constructor(apiUrl: string) {
}, this.apiUrl = apiUrl;
forgotPassword: async (params: any) => { this.sdk = Sdk.create(apiUrl);
return { success: true } satisfies AuthActionResponse
}, const methods: Array<keyof AuthProvider> = [
updatePassword: async (params: any) => { 'login',
return { success: true } satisfies AuthActionResponse 'logout',
}, 'check',
getPermissions: async (params: any) => { 'onError',
return { success: true } satisfies AuthActionResponse 'register',
}, 'forgotPassword',
getIdentity: async (params: any) => { 'updatePassword',
return { id: "1", fullName: "John Doe", avatar: "https://via.placeholder.com/150" } 'getPermissions',
} 'getIdentity',
];
methods.forEach((method) => {
this[method] = this[method]?.bind(this);
});
}
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const ret = await this.sdk.account().login({
email: params.email,
password: params.password,
})
return {
success: ret,
redirectTo: ret ? "/dashboard" : undefined,
};
}
async logout(params: any): Promise<AuthActionResponse> {
let ret = await this.sdk.account().logout();
return {success: ret, redirectTo: "/login"};
}
async check(params?: any): Promise<CheckResponse> {
const ret = await this.sdk.account().ping();
return {authenticated: ret};
}
async onError(error: any): Promise<OnErrorResponse> {
return {logout: true};
}
async register(params: any): Promise<AuthActionResponse> {
return {success: true};
}
async forgotPassword(params: any): Promise<AuthActionResponse> {
return {success: true};
}
async updatePassword(params: any): Promise<AuthActionResponse> {
return {success: true};
}
async getPermissions(params?: Record<string, any>): Promise<AuthActionResponse> {
return {success: true};
}
async getIdentity(params?: any): Promise<AuthActionResponse> {
return {id: "1", fullName: "John Doe", avatar: "https://via.placeholder.com/150"};
}
public static create(apiUrl: string): AuthProvider {
return new PortalAuthProvider(apiUrl);
}
}
interface RequiredAuthProvider extends AuthProvider {
login: AuthProvider['login'];
logout: AuthProvider['logout'];
check: AuthProvider['check'];
onError: AuthProvider['onError'];
register: AuthProvider['register'];
forgotPassword: AuthProvider['forgotPassword'];
updatePassword: AuthProvider['updatePassword'];
getPermissions: AuthProvider['getPermissions'];
getIdentity: AuthProvider['getIdentity'];
} }

View File

@ -12,7 +12,7 @@ import type { LinksFunction } from "@remix-run/node";
// Supports weights 200-800 // Supports weights 200-800
import '@fontsource-variable/manrope'; import '@fontsource-variable/manrope';
import {Refine} from "@refinedev/core"; import {Refine} from "@refinedev/core";
import {authProvider} from "~/data/auth-provider.js"; import {PortalAuthProvider} from "~/data/auth-provider.js";
import routerProvider from "@refinedev/remix-router"; import routerProvider from "@refinedev/remix-router";
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
@ -40,7 +40,7 @@ export function Layout({children}: { children: React.ReactNode }) {
export default function App() { export default function App() {
return ( return (
<Refine <Refine
authProvider={authProvider} authProvider={PortalAuthProvider.create("https://alpha.pinner.xyz")}
routerProvider={routerProvider} routerProvider={routerProvider}
> >
<Outlet/> <Outlet/>

View File

@ -1,25 +1,25 @@
import Login from "./login"; import Login from "./login";
import { useGo, useIsAuthenticated } from "@refinedev/core"; import {useGo, useIsAuthenticated} from "@refinedev/core";
import { useEffect } from "react"; import {useEffect} from "react";
export default function Index() { export default function Index() {
const { isLoading, data } = useIsAuthenticated(); const {isLoading, data} = useIsAuthenticated();
const go = useGo(); const go = useGo();
useEffect(() => { useEffect(() => {
if (!isLoading && data?.authenticated) { if (!isLoading) {
go({ to: "/dashboard", type: "replace" }); if (data?.authenticated) {
go({to: "/dashboard", type: "replace"});
} else {
go({to: "/login", type: "replace"});
}
}
}, [isLoading, data]);
if (isLoading) {
return <>Checking Login Status</> || null;
} }
}, [isLoading, data]);
if (isLoading) { return (<>Redirecting</>) || null;
return <>Checking Login Status</> || null;
}
if (data?.authenticated) {
return <>Redirecting</> || null;
}
return <Login />;
} }

View File

@ -1,179 +1,205 @@
import type { MetaFunction } from "@remix-run/node" import type {MetaFunction} from "@remix-run/node"
import { Link, useLocation } from "@remix-run/react" import {Link, useLocation} from "@remix-run/react"
import { z } from "zod" import {z} from "zod"
import { Button } from "~/components/ui/button" import {Button} from "~/components/ui/button"
import logoPng from "~/images/lume-logo.png?url" import logoPng from "~/images/lume-logo.png?url"
import lumeColorLogoPng from "~/images/lume-color-logo.png?url" import lumeColorLogoPng from "~/images/lume-color-logo.png?url"
import discordLogoPng from "~/images/discord-logo.png?url" import discordLogoPng from "~/images/discord-logo.png?url"
import lumeBgPng from "~/images/lume-bg-image.png?url" import lumeBgPng from "~/images/lume-bg-image.png?url"
import { Field, FieldCheckbox } from "~/components/forms" import {Field, FieldCheckbox} from "~/components/forms"
import { getFormProps, useForm } from "@conform-to/react" import {getFormProps, useForm} from "@conform-to/react"
import { getZodConstraint, parseWithZod } from "@conform-to/zod" import {getZodConstraint, parseWithZod} from "@conform-to/zod"
import {useGo, useIsAuthenticated, useLogin} from "@refinedev/core";
import {AuthFormRequest} from "~/data/auth-provider.js";
import {useEffect} from "react";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
{ title: "Login" }, {title: "Login"},
{ name: "description", content: "Welcome to Lume!" } {name: "description", content: "Welcome to Lume!"}
] ]
} }
export default function Login() { export default function Login() {
const location = useLocation() const location = useLocation()
const hash = location.hash const {isLoading: isAuthLoading, data: authData} = useIsAuthenticated();
const auth = useIsAuthenticated();
const hash = location.hash
const go = useGo();
return ( useEffect(() => {
<div className="p-10 h-screen relative"> if (!isAuthLoading) {
<header> if (authData?.authenticated) {
<img src={logoPng} alt="Lume logo" className="h-10" /> go({to: "/dashboard", type: "replace"});
</header> }
<div className="fixed inset-0 -z-10 overflow-clip"> }
<img }, [isAuthLoading, authData]);
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
/>
</div>
{hash === "" && <LoginForm />} return (
{hash === "#otp" && <OtpForm />} <div className="p-10 h-screen relative">
<header>
<img src={logoPng} alt="Lume logo" className="h-10"/>
</header>
<div className="fixed inset-0 -z-10 overflow-clip">
<img
src={lumeBgPng}
alt="Lume background"
className="absolute top-0 right-0 md:w-2/3 object-cover z-[-1]"
/>
</div>
<footer className="my-5"> {hash === "" && <LoginForm/>}
<ul className="flex flex-row"> {hash === "#otp" && <OtpForm/>}
<li>
<Link to="https://discord.lumeweb.com"> <footer className="my-5">
<Button <ul className="flex flex-row">
variant={"link"} <li>
className="flex flex-row gap-x-2 text-input-placeholder" <Link to="https://discord.lumeweb.com">
> <Button
<img className="h-5" src={discordLogoPng} alt="Discord Logo" /> variant={"link"}
Connect with us className="flex flex-row gap-x-2 text-input-placeholder"
</Button> >
</Link> <img className="h-5" src={discordLogoPng} alt="Discord Logo"/>
</li> Connect with us
<li> </Button>
<Link to="https://lumeweb.com"> </Link>
<Button </li>
variant={"link"} <li>
className="flex flex-row gap-x-2 text-input-placeholder" <Link to="https://lumeweb.com">
> <Button
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" /> variant={"link"}
Connect with us className="flex flex-row gap-x-2 text-input-placeholder"
</Button> >
</Link> <img className="h-5" src={lumeColorLogoPng} alt="Lume Logo"/>
</li> Connect with us
</ul> </Button>
</footer> </Link>
</div> </li>
) </ul>
</footer>
</div>
)
} }
const LoginSchema = z.object({ const LoginSchema = z.object({
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string(),
rememberMe: z.boolean() rememberMe: z.boolean()
}) })
const LoginForm = () => { const LoginForm = () => {
const [form, fields] = useForm({ const login = useLogin<AuthFormRequest>()
id: "login",
constraint: getZodConstraint(LoginSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: LoginSchema })
},
shouldValidate: "onSubmit"
})
return ( const [form, fields] = useForm({
<form id: "login",
className="w-full p-2 max-w-md space-y-3 mt-12 bg-background" constraint: getZodConstraint(LoginSchema),
{...getFormProps(form)} onValidate({formData}) {
> return parseWithZod(formData, {schema: LoginSchema})
<h2 className="text-3xl font-bold !mb-12">Welcome back! 🎉</h2> },
<Field shouldValidate: "onSubmit",
inputProps={{ name: fields.email.name }} onSubmit(e) {
labelProps={{ children: "Email" }} e.preventDefault();
errors={fields.email.errors}
/> const data = Object.fromEntries(new FormData(e.currentTarget).entries());
<Field login.mutate({
inputProps={{ name: fields.password.name, type: "password" }} email: data.email.toString(),
labelProps={{ children: "Password" }} password: data.password.toString(),
errors={fields.password.errors} rememberMe: data.rememberMe.toString() === "on"
/> });
<FieldCheckbox }
inputProps={{ name: fields.rememberMe.name, form: form.id }} })
labelProps={{ children: "Remember Me" }}
errors={fields.rememberMe.errors} return (
/> <form
<Button className="w-full h-14">Login</Button> className="w-full p-2 max-w-md space-y-3 mt-12 bg-background"
<p className="inline-block text-input-placeholder"> {...getFormProps(form)}
Forgot your password?{" "}
<Link
to="/reset-password"
className="text-primary-1 text-md hover:underline hover:underline-offset-4"
> >
Reset Password <h2 className="text-3xl font-bold !mb-12">Welcome back! 🎉</h2>
</Link> <Field
</p> inputProps={{name: fields.email.name}}
<Link to="/sign-up" className="block"> labelProps={{children: "Email"}}
<Button type="button" className="w-full h-14" variant={"outline"}> errors={fields.email.errors}
Create an Account />
</Button> <Field
</Link> inputProps={{name: fields.password.name, type: "password"}}
</form> labelProps={{children: "Password"}}
) errors={fields.password.errors}
/>
<FieldCheckbox
inputProps={{name: fields.rememberMe.name, form: form.id}}
labelProps={{children: "Remember Me"}}
errors={fields.rememberMe.errors}
/>
<Button className="w-full h-14">Login</Button>
<p className="inline-block text-input-placeholder">
Forgot your password?{" "}
<Link
to="/reset-password"
className="text-primary-1 text-md hover:underline hover:underline-offset-4"
>
Reset Password
</Link>
</p>
<Link to="/sign-up" className="block">
<Button type="button" className="w-full h-14" variant={"outline"}>
Create an Account
</Button>
</Link>
</form>
)
} }
const OtpSchema = z.object({ const OtpSchema = z.object({
otp: z.string().length(6, { message: "OTP must be 6 characters" }) otp: z.string().length(6, {message: "OTP must be 6 characters"})
}) })
const OtpForm = () => { const OtpForm = () => {
// TODO: Add support for resending the OTP // TODO: Add support for resending the OTP
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "otp", id: "otp",
constraint: getZodConstraint(OtpSchema), constraint: getZodConstraint(OtpSchema),
onValidate({ formData }) { onValidate({formData}) {
return parseWithZod(formData, { schema: OtpSchema }) return parseWithZod(formData, {schema: OtpSchema})
}, },
shouldValidate: "onSubmit" shouldValidate: "onSubmit"
}) })
const valid = false // TODO: some sort of logic to verify user is on OTP state validly const valid = false // TODO: some sort of logic to verify user is on OTP state validly
if (!valid) { if (!valid) {
location.hash = "" location.hash = ""
return null return null
} }
return ( return (
<form <form
className="w-full p-2 max-w-md mt-12 bg-background" className="w-full p-2 max-w-md mt-12 bg-background"
{...getFormProps(form)} {...getFormProps(form)}
> >
<span className="block !mb-8 space-y-2"> <span className="block !mb-8 space-y-2">
<h2 className="text-3xl font-bold">Check your inbox</h2> <h2 className="text-3xl font-bold">Check your inbox</h2>
<p className="text-input-placeholder"> <p className="text-input-placeholder">
We will need the six digit confirmation code you received in your We will need the six digit confirmation code you received in your
email in order to verify your account and get started. Didnt receive email in order to verify your account and get started. Didnt receive
a code?{" "} a code?{" "}
<Button type="button" variant={"link"} className="text-md h-0"> <Button type="button" variant={"link"} className="text-md h-0">
Resend now Resend now
</Button> </Button>
</p> </p>
</span> </span>
<Field <Field
inputProps={{ name: fields.otp.name }} inputProps={{name: fields.otp.name}}
labelProps={{ children: "Confirmation Code" }} labelProps={{children: "Confirmation Code"}}
errors={fields.otp.errors} errors={fields.otp.errors}
/> />
<Button className="w-full h-14">Verify</Button> <Button className="w-full h-14">Verify</Button>
<p className="text-input-placeholder w-full text-left"> <p className="text-input-placeholder w-full text-left">
<Link <Link
to="/login" to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4" className="text-primary-1 text-md hover:underline hover:underline-offset-4"
> >
Back to Login Back to Login
</Link> </Link>
</p> </p>
</form> </form>
) )
} }

18707
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
"@conform-to/react": "^1.0.2", "@conform-to/react": "^1.0.2",
"@conform-to/zod": "^1.0.2", "@conform-to/zod": "^1.0.2",
"@fontsource-variable/manrope": "^5.0.19", "@fontsource-variable/manrope": "^5.0.19",
"@lumeweb/portal-sdk": "^0.0.0-20240306231947", "@lumeweb/portal-sdk": "0.0.0-20240313171219",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",