Login/logout properly working.

This commit is contained in:
JesseBrault0709 2024-06-23 10:33:19 +02:00
parent c97fdc76e5
commit 038b5181dd
4 changed files with 123 additions and 69 deletions

66
src/api/login.ts Normal file
View File

@ -0,0 +1,66 @@
export type LoginResult = LoginSuccess | LoginFailure
export interface LoginSuccess {
_tag: 'success'
loginView: LoginView
}
export interface LoginFailure {
_tag: 'failure'
error: string
}
export interface LoginView {
username: string
accessToken: string
}
const login = async (
username: string,
password: string
): Promise<LoginResult> => {
try {
const response = await fetch(
import.meta.env.VITE_MME_API_URL + '/auth/login',
{
body: JSON.stringify({ username, password }),
headers: {
'Content-type': 'application/json'
},
method: 'POST',
mode: 'cors'
}
)
if (response.ok) {
const loginView = (await response.json()) as LoginView
return {
_tag: 'success',
loginView
}
} else {
let error: string
if (response.status === 401) {
error = 'Invalid username or password.'
} else if (response.status === 500) {
error =
'There was an internal server error. Please try again later.'
} else {
error = 'Unknown error.'
console.error(
`Unknown error: ${response.status} ${response.statusText}`
)
}
return {
_tag: 'failure',
error
}
}
} catch (fetchError) {
console.error(`Unknown error: ${fetchError}`)
return {
_tag: 'failure',
error: 'Network error. Please try again later.'
}
}
}
export default login

View File

@ -1,70 +1,50 @@
import React, { useContext, createContext, useState } from 'react'
import React, { createContext, useContext, useEffect, useState } from 'react'
export interface AuthContextType {
token: string | null
error: string | null
login(username: string, password: string): Promise<boolean>
logout(): void
putToken(token: string, cb?: () => void): void
clearToken(cb?: () => void): void
}
interface LoginView {
username: string
accessToken: string
interface AuthState {
token: string | null
putCb?: () => void
clearCb?: () => void
}
const AuthContext = createContext<AuthContextType | null>(null)
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
const [token, setToken] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [authState, setAuthState] = useState<AuthState>({ token: null })
const login: AuthContextType['login'] = async (username, password) => {
try {
const response = await fetch(
import.meta.env.VITE_MME_API_URL + '/auth/login',
{
body: JSON.stringify({ username, password }),
headers: {
'Content-type': 'application/json'
},
method: 'POST',
mode: 'cors'
}
)
if (response.ok) {
const body = (await response.json()) as LoginView
setToken(body.accessToken)
setError(null)
return true
} else {
setToken(null)
if (response.status === 401) {
setError('Invalid username or password.')
} else if (response.status === 500) {
setError(
'There was an internal server error. Please try again later.'
)
} else {
setError('Unknown error.')
console.error(
`Unknown error: ${response.status} ${response.statusText}`
)
}
return false
}
} catch (fetchError) {
setError('Network error. Please try again later.')
console.error(`Unknown error: ${fetchError}`)
return false
useEffect(() => {
if (authState.token === null && authState.clearCb !== undefined) {
authState.clearCb()
setAuthState({ ...authState, clearCb: undefined })
} else if (authState.token !== null && authState.putCb !== undefined) {
authState.putCb()
setAuthState({ ...authState, putCb: undefined })
}
}
const logout: AuthContextType['logout'] = () => {
setToken(null)
}
}, [authState.token])
return (
<AuthContext.Provider value={{ token, error, login, logout }}>
<AuthContext.Provider
value={{
token: authState.token,
putToken(token, cb) {
setAuthState({
token,
putCb: cb
})
},
clearToken(cb) {
setAuthState({
token: null,
clearCb: cb
})
}
}}
>
{children}
</AuthContext.Provider>
)

View File

@ -1,23 +1,23 @@
import {
Outlet,
createRootRouteWithContext,
useRouteContext,
useNavigate,
useRouter
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import RouterContext from '../RouterContext'
import { useAuth } from '../auth'
const RootLayout = () => {
const logout = useRouteContext({
from: '__root__',
select: s => s.auth.logout
})
const auth = useAuth()
const router = useRouter()
const navigate = useNavigate()
const onLogout = async () => {
logout()
await router.invalidate()
router.navigate({ to: '/login' })
auth.clearToken(async () => {
await router.invalidate()
await navigate({ to: '/login' })
})
}
return (

View File

@ -1,17 +1,21 @@
import {
createFileRoute,
redirect,
useRouteContext,
useNavigate,
useRouter,
useSearch
} from '@tanstack/react-router'
import { FormEvent } from 'react'
import { FormEvent, useState } from 'react'
import { z } from 'zod'
import login from '../api/login'
import { useAuth } from '../auth'
const Login = () => {
const login = useRouteContext({ from: '/login', select: s => s.auth.login })
const error = useRouteContext({ from: '/login', select: s => s.auth.error })
const auth = useAuth()
const [error, setError] = useState<string | null>(null)
const router = useRouter()
const navigate = useNavigate()
const search = useSearch({ from: '/login' })
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
@ -19,10 +23,14 @@ const Login = () => {
const formData = new FormData(event.currentTarget)
const username = (formData.get('username') as string | null) ?? ''
const password = (formData.get('password') as string | null) ?? ''
const success = await login(username, password)
if (success) {
await router.invalidate()
router.navigate({ to: search.redirect || '/' })
const loginResult = await login(username, password)
if (loginResult._tag === 'success') {
auth.putToken(loginResult.loginView.accessToken, async () => {
await router.invalidate()
await navigate({ to: search.redirect ?? '/' })
})
} else {
setError(loginResult.error)
}
}