Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auth through kratos #1278

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"lana/events",
"lana/ids",
"lana/dashboard",
"lana/user-onboarding",

"core/user",
"core/governance",
Expand Down
1 change: 1 addition & 0 deletions apps/admin-panel/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ results
build_artifacts
screenshots
downloads
videos
60 changes: 0 additions & 60 deletions apps/admin-panel/app/api/auth/[...nextauth]/options.ts

This file was deleted.

6 changes: 0 additions & 6 deletions apps/admin-panel/app/api/auth/[...nextauth]/route.ts

This file was deleted.

37 changes: 24 additions & 13 deletions apps/admin-panel/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
"use client"

import { useState, useEffect } from "react"
import { getCsrfToken } from "next-auth/react"
import { useState } from "react"
import { useRouter } from "next/navigation"

import { loginUser } from "../ory"

import { Input } from "@/components/input"
import { basePath } from "@/env"
import { Button } from "@/ui/button"

const Login: React.FC = () => {
const [csrfToken, setCsrfToken] = useState<string | null>(null)
useEffect(() => {
getCsrfToken().then((token) => token && setCsrfToken(token))
})
const router = useRouter()

const [email, setEmail] = useState("")
const [error, setError] = useState("")

const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setError("")

try {
const flowId = await loginUser(email)
router.push(`/auth/verify?flow=${flowId}`)
} catch {
setError("Please check your credentials and try again.")
}
}

return (
<>
Expand All @@ -20,20 +33,18 @@ const Login: React.FC = () => {
<div className="text-md">Welcome to Lana Bank Admin Panel</div>
<div className="text-md font-light">Enter your email address to continue</div>
</div>
<form
className="space-y-[20px] w-full"
action={`${basePath}/api/auth/signin/email`}
method="POST"
>
<input name="csrfToken" type="hidden" defaultValue={csrfToken || ""} />
<form className="space-y-[20px] w-full" onSubmit={onSubmit}>
<Input
label="Your email"
type="email"
name="email"
autofocus
placeholder="Please enter your email address"
defaultValue={email}
onChange={setEmail}
/>
<Button type="submit">Submit</Button>
{error && <div className="text-red-500">{error}</div>}
</form>
</>
)
Expand Down
106 changes: 106 additions & 0 deletions apps/admin-panel/app/auth/ory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client"

/* eslint-disable camelcase */ // Many Ory Kratos request body params are snake_case

import { Configuration, FrontendApi, UiNodeInputAttributes } from "@ory/client"
import axios, { AxiosError } from "axios"

import { basePath } from "@/env"

export const getOryClient = () =>
new FrontendApi(
new Configuration({
basePath,
baseOptions: {
withCredentials: true,
timeout: 10000,
},
}),
"",
axios,
)

export const getSession = () => {
const kratos = getOryClient()
return kratos.toSession()
}

export const loginUser = async (email: string) => {
const oryClient = getOryClient()
let flowId: string = ""

try {
const { data: flow } = await oryClient.createBrowserLoginFlow()
flowId = flow.id

const { data: loginData } = await oryClient.updateLoginFlow({
flow: flow.id,
updateLoginFlowBody: {
method: "code",
identifier: email,
csrf_token:
(
flow.ui.nodes.find(
(node) =>
node.attributes.node_type === "input" &&
node.attributes.name === "csrf_token",
)?.attributes as UiNodeInputAttributes
).value || "",
},
})
return loginData
} catch (error) {
if (
error instanceof AxiosError &&
error.code === AxiosError.ERR_BAD_REQUEST &&
error.response?.data.ui.messages[0].id === 1010014
) {
return flowId
}
throw error
}
}

export const loginUserWithOtp = async (flowId: string, otp: string) => {
const oryClient = getOryClient()

const { data: loginFlow } = await oryClient.getLoginFlow({
id: flowId,
})

const csrf_token =
(
loginFlow.ui.nodes.find(
(node) =>
node.attributes.node_type === "input" && node.attributes.name === "csrf_token",
)?.attributes as UiNodeInputAttributes
).value || ""

const identifier =
(
loginFlow.ui.nodes.find(
(node) =>
node.attributes.node_type === "input" && node.attributes.name === "identifier",
)?.attributes as UiNodeInputAttributes
).value || ""

const { data: loginData } = await oryClient.updateLoginFlow({
flow: flowId,
updateLoginFlowBody: {
method: "code",
identifier,
csrf_token,
code: otp,
},
})

return loginData
}

export const logoutUser = async () => {
const oryClient = getOryClient()
const { data } = await oryClient.createBrowserLogoutFlow()
await oryClient.updateLogoutFlow({
token: data.logout_token,
})
}
45 changes: 45 additions & 0 deletions apps/admin-panel/app/auth/session.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client"

import { useEffect, useState, useCallback } from "react"
import { usePathname, useRouter } from "next/navigation"

import { getSession, logoutUser } from "./ory"

type Props = {
appChildren: React.ReactNode
children: React.ReactNode
}

export const Authenticated: React.FC<Props> = ({ appChildren, children }) => {
const router = useRouter()
const pathName = usePathname()

const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
useEffect(() => {
;(async () => {
try {
await getSession()
setIsAuthenticated(true)
if (pathName === "/") router.push("/dashboard")
} catch (error) {
setIsAuthenticated(false)
if (!pathName.startsWith("/auth")) router.push("/auth/login")
}
})()
}, [pathName, router])

if (!isAuthenticated)
return <main className="h-screen w-full flex flex-col">{appChildren}</main>
else return <>{children}</>
}

export const useLogout = () => {
const router = useRouter()

const logout = useCallback(async () => {
await logoutUser()
router.push("/")
}, [router])

return { logout }
}
Loading
Loading