Login/logout properly working.
This commit is contained in:
parent
c97fdc76e5
commit
038b5181dd
66
src/api/login.ts
Normal file
66
src/api/login.ts
Normal 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
|
86
src/auth.tsx
86
src/auth.tsx
@ -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>
|
||||
)
|
||||
|
@ -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 (
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user