Added login flow

This commit is contained in:
strNophix 2022-10-16 21:02:41 +02:00
parent 4afe2f906d
commit a627addad5
14 changed files with 303 additions and 80 deletions

View File

@ -47,10 +47,6 @@ selfservice:
registration:
lifespan: 10m
ui_url: http://127.0.0.1:3000/signup
after:
password:
hooks:
- hook: session
log:
level: debug

View File

@ -5,34 +5,66 @@ import FormField from "../common/form/FormField"
import InlineLink from "../common/InlineLink"
import SubmitButton from "../common/form/SubmitButton"
interface LoginFormValues {
username: string
password: string
}
import * as validation from "../../config/validation"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import useLogInFlow from "../../hooks/useLogInFlow"
import Input from "../common/Input"
const LogInFormSchema = z.object({
csrfToken: z.string(),
password: validation.password,
email: z.string().email({ message: "Not a valid email address" }).trim(),
})
type LogInFormValues = z.infer<typeof LogInFormSchema>
const formFields = [
{ id: "email", label: "Email", type: "email" },
{ id: "password", label: "Password", type: "password" },
]
const LoginForm: FC = () => {
const { register, handleSubmit } = useForm<LoginFormValues>()
const onSubmit: SubmitHandler<LoginFormValues> = (data) => console.log(data)
const logInFlow = useLogInFlow()
const { register, handleSubmit, formState } = useForm<LogInFormValues>({
resolver: zodResolver(LogInFormSchema),
})
const onSubmit: SubmitHandler<LogInFormValues> = (data) =>
logInFlow.submitData({
csrf_token: data.csrfToken,
method: "password",
identifier: data.email,
password: data.password,
})
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<FormField
label="Username"
className="py-2 px-2 outline-2 w-full"
{...register("username")}
/>
<FormField
label="Password"
type="password"
{...register("password")}
className="py-2 px-2 outline-2 w-full"
bottomElement={
<InlineLink to="#" className="block mt-2">
Trouble logging in?
</InlineLink>
}
/>
{logInFlow.flow && (
<Input
hidden={true}
value={logInFlow.flow.ui.nodes[0].attributes.value}
{...register("csrfToken")}
/>
)}
{formFields.map((field) => (
<FormField
key={field.id}
id={field.id}
type={field.type}
label={field.label}
{...register(field.id as any)}
className="py-2 px-2 outline-2 w-full"
bottomElement={
<p className="text-xs">
{formState.errors[(field.id as any) || ""]?.message}
</p>
}
/>
))}
<SubmitButton className="w-full" value="Log In" />
<div className="text-center">
<InlineLink to="#">Trouble logging in?</InlineLink>
</div>
</form>
)
}

View File

@ -7,24 +7,14 @@ import FormField from "../common/form/FormField"
import InlineLink from "../common/InlineLink"
import Input from "../common/Input"
import SubmitButton from "../common/form/SubmitButton"
import { PASSWORD_REGEX } from "../../config"
import useSignUpFlow from "../../hooks/useSignUpFlow"
import * as validation from "../../config/validation"
const SignupFormSchema = z
.object({
csrfToken: z.string(),
username: z
.string()
.trim()
.min(1, { message: "Username must be at least 1 character long." })
.max(16, { message: "Username can't be longer than 16 characters.." }),
password: z
.string()
.trim()
.regex(
PASSWORD_REGEX,
"Password must be 8-64 long and must contain a number, uppercase, lowercase and special character.",
),
username: validation.username,
password: validation.password,
passwordRepeat: z.string().trim(),
email: z.string().email({ message: "Not a valid email address" }).trim(),
})

View File

@ -1,11 +1,19 @@
import { UserIcon } from "@heroicons/react/24/outline"
import {
ArrowRightIcon,
ArrowRightOnRectangleIcon,
UserIcon,
} from "@heroicons/react/24/outline"
import { FC, useState } from "react"
import { useLogout } from "../../hooks/useLogout"
import useSession from "../../hooks/useSession"
import Button from "../common/Button"
import Logo from "../common/Logo"
import LoginModal, { LoginModelProps } from "../login/LoginModal"
const NavBar: FC = () => {
const logout = useLogout()
const session = useSession((state) => state.session)
const [modalProps, setModalProps] = useState<LoginModelProps>({
isOpen: false,
defaultPage: 0,
@ -23,6 +31,7 @@ const NavBar: FC = () => {
isOpen: true,
})
console.log({ session })
return (
<nav className="bg-zinc-800 w-screen font-semibold border-b border-b-black">
<div className="flex flex-row justify-between items-center h-12 mx-2">
@ -35,27 +44,41 @@ const NavBar: FC = () => {
</div>
<div>
<ul className="justify-end flex flex-row space-x-3 items-center">
<li>
<Button
className="text-sm px-3 py-2 bg-neutral-700"
onClick={showLoginTab}
>
Log In
</Button>
</li>
<li>
<Button
className="text-sm px-3 py-2 bg-violet-500"
onClick={showSignupTab}
>
Sign Up
</Button>
</li>
<li>
<Button variant="subtle" className="p-[0.4rem]">
<UserIcon className="h-5 w-5 inline-block" />
</Button>
</li>
{session ? (
<>
<li>
<UserIcon className="h-5 w-5 inline-block" />
</li>
<li>
<Button
variant="subtle"
className="p-[0.4rem]"
onClick={logout}
>
<ArrowRightOnRectangleIcon className="h-5 w-5 inline-block" />
</Button>
</li>
</>
) : (
<>
<li>
<Button
className="text-sm px-3 py-2 bg-neutral-700"
onClick={showLoginTab}
>
Log In
</Button>
</li>
<li>
<Button
className="text-sm px-3 py-2 bg-violet-500"
onClick={showSignupTab}
>
Sign Up
</Button>
</li>
</>
)}
</ul>
</div>
</div>

View File

@ -1,3 +1 @@
export const KRATOS_URL = "http://127.0.0.1:4433"
export const PASSWORD_REGEX =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/

View File

@ -0,0 +1,18 @@
import { z } from "zod"
export const PASSWORD_REGEX =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$/
export const username = z
.string()
.trim()
.min(3, { message: "Username must be at least 3 character long." })
.max(16, { message: "Username can't be longer than 16 characters.." })
export const password = z
.string()
.trim()
.regex(
PASSWORD_REGEX,
"Password must be 8-64 long and must contain a number, uppercase, lowercase and special character.",
)

View File

@ -0,0 +1,53 @@
import {
SelfServiceRegistrationFlow,
SubmitSelfServiceLoginFlowBody,
} from "@ory/client"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import ory from "../services/ory"
import useSession from "./useSession"
const useLogInFlow = () => {
const router = useRouter()
const [flow, setFlow] = useState<SelfServiceRegistrationFlow>()
const { flow: flowId } = router.query
const updateSession = useSession((state) => state.update)
useEffect(() => {
const func = async () => {
if (!router.isReady || flow) {
return
}
let serviceFlow
if (flowId) {
serviceFlow = await ory.getSelfServiceLoginFlow(String(flowId))
} else {
serviceFlow = await ory.initializeSelfServiceLoginFlowForBrowsers()
}
setFlow(serviceFlow.data)
}
func()
}, [flowId, router, router.isReady, flow])
const submitData = async (data: SubmitSelfServiceLoginFlowBody) => {
await router.push(`/login?flow=${flow?.id}`, undefined, {
shallow: true,
})
ory
.submitSelfServiceLoginFlow(String(flow?.id), undefined, data)
.then(async ({ data }) => {
updateSession(data.session)
await router.push(flow?.return_to || "/")
})
.catch((err) => {
console.log({ err })
})
}
return { flow, submitData }
}
export default useLogInFlow

38
client/hooks/useLogout.ts Normal file
View File

@ -0,0 +1,38 @@
import { AxiosError } from "axios"
import { useRouter } from "next/router"
import { useState, useEffect, DependencyList } from "react"
import ory from "../services/ory"
import useSession from "./useSession"
export function useLogout(deps?: DependencyList) {
const session = useSession()
const [logoutToken, setLogoutToken] = useState<string>("")
const router = useRouter()
useEffect(() => {
ory
.createSelfServiceLogoutFlowUrlForBrowsers()
.then(({ data }) => {
setLogoutToken(data.logout_token)
})
.catch((err: AxiosError) => {
switch (err.response?.status) {
case 401:
return
}
return Promise.reject(err)
})
}, deps)
return () => {
if (logoutToken) {
session.drop()
ory
.submitSelfServiceLogoutFlow(logoutToken)
.then(() => router.push("/"))
.then(() => router.reload())
}
}
}

View File

@ -0,0 +1,31 @@
import create from "zustand"
import { Session } from "@ory/client"
import ory from "../services/ory"
import { AxiosError } from "axios"
export interface SessionState {
session?: Session
load: () => void
update: (data: Session) => void
drop: () => void
}
const useSession = create<SessionState>((set) => ({
session: undefined,
load: () => {
ory
.toSession()
.then(({ data }) => {
set({ session: data })
})
.catch((err: AxiosError) => {})
},
update: (session) => set({ session }),
drop: () => {
set({ session: undefined })
},
}))
export default useSession

View File

@ -1,15 +1,18 @@
import {
SelfServiceRegistrationFlow,
Session,
SubmitSelfServiceRegistrationFlowBody,
} from "@ory/client"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import ory from "../services/ory"
import useSession from "./useSession"
export const useSignUpFlow = () => {
const router = useRouter()
const [flow, setFlow] = useState<SelfServiceRegistrationFlow>()
const { flow: flowId, return_to: returnTo } = router.query
const { flow: flowId } = router.query
const updateSession = useSession((state) => state.update)
useEffect(() => {
const func = async () => {
@ -22,30 +25,28 @@ export const useSignUpFlow = () => {
serviceFlow = await ory.getSelfServiceRegistrationFlow(String(flowId))
} else {
serviceFlow =
await ory.initializeSelfServiceRegistrationFlowForBrowsers(
returnTo ? String(returnTo) : undefined,
)
await ory.initializeSelfServiceRegistrationFlowForBrowsers()
}
setFlow(serviceFlow.data)
}
func()
}, [flowId, router, router.isReady, returnTo, flow])
}, [flowId, router, router.isReady, flow])
const submitData = async (data: SubmitSelfServiceRegistrationFlowBody) => {
await router.push(`/signup?flow=${flow?.id}`, undefined, {
shallow: true,
})
try {
const resp = await ory.submitSelfServiceRegistrationFlow(
String(flow?.id),
data,
)
// TODO: handle registration
} catch (e) {
// TODO: handle errors
}
ory
.submitSelfServiceRegistrationFlow(String(flow?.id), data)
.then(async ({ data }) => {
updateSession(data.session as Session)
await router.push(flow?.return_to || "/")
})
.catch((err) => {
console.log({ err })
})
}
return { flow, submitData }

View File

@ -28,7 +28,8 @@
"react-dom": "17.0.2",
"react-hook-form": "^7.37.0",
"tailwind-scrollbar": "2.1.0-preview.0",
"zod": "^3.19.1"
"zod": "^3.19.1",
"zustand": "^4.1.2"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^2.0.2",

View File

@ -1,7 +1,14 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import "../styles/globals.css"
import type { AppProps } from "next/app"
import useSession from "../hooks/useSession"
import { useEffect } from "react"
function MyApp({ Component, pageProps }: AppProps) {
const loadSession = useSession((state) => state.load)
useEffect(() => {
loadSession()
}, [loadSession])
return <Component {...pageProps} />
}

9
client/pages/index.tsx Normal file
View File

@ -0,0 +1,9 @@
import { NextPage } from "next"
import useSession from "../hooks/useSession"
const IndexPage: NextPage = () => {
const session = useSession()
return <div>{JSON.stringify(session.session || {})}</div>
}
export default IndexPage

26
client/pnpm-lock.yaml generated
View File

@ -26,6 +26,7 @@ specifiers:
tailwindcss: ^3.1.8
typescript: 4.4.4
zod: ^3.19.1
zustand: ^4.1.2
dependencies:
'@headlessui/react': 1.7.3_sfoxds7t5ydpegc3knd667wn6m
@ -41,6 +42,7 @@ dependencies:
react-hook-form: 7.37.0_react@17.0.2
tailwind-scrollbar: 2.1.0-preview.0_tailwindcss@3.1.8
zod: 3.19.1
zustand: 4.1.2_react@17.0.2
devDependencies:
'@trivago/prettier-plugin-sort-imports': 2.0.4_prettier@2.7.1
@ -3479,6 +3481,14 @@ packages:
dependencies:
punycode: 2.1.1
/use-sync-external-store/1.2.0_react@17.0.2:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 17.0.2
dev: false
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -3572,3 +3582,19 @@ packages:
/zod/3.19.1:
resolution: {integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==}
dev: false
/zustand/4.1.2_react@17.0.2:
resolution: {integrity: sha512-gcRaKchcxFPbImrBb/BKgujOhHhik9YhVpIeP87ETT7uokEe2Szu7KkuZ9ghjtD+/KKkcrRNktR2AiLXPIbKIQ==}
engines: {node: '>=12.7.0'}
peerDependencies:
immer: '>=9.0'
react: '>=16.8'
peerDependenciesMeta:
immer:
optional: true
react:
optional: true
dependencies:
react: 17.0.2
use-sync-external-store: 1.2.0_react@17.0.2
dev: false