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 {
|
export interface AuthContextType {
|
||||||
token: string | null
|
token: string | null
|
||||||
error: string | null
|
putToken(token: string, cb?: () => void): void
|
||||||
login(username: string, password: string): Promise<boolean>
|
clearToken(cb?: () => void): void
|
||||||
logout(): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginView {
|
interface AuthState {
|
||||||
username: string
|
token: string | null
|
||||||
accessToken: string
|
putCb?: () => void
|
||||||
|
clearCb?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | null>(null)
|
const AuthContext = createContext<AuthContextType | null>(null)
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
|
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
const [token, setToken] = useState<string | null>(null)
|
const [authState, setAuthState] = useState<AuthState>({ token: null })
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const login: AuthContextType['login'] = async (username, password) => {
|
useEffect(() => {
|
||||||
try {
|
if (authState.token === null && authState.clearCb !== undefined) {
|
||||||
const response = await fetch(
|
authState.clearCb()
|
||||||
import.meta.env.VITE_MME_API_URL + '/auth/login',
|
setAuthState({ ...authState, clearCb: undefined })
|
||||||
{
|
} else if (authState.token !== null && authState.putCb !== undefined) {
|
||||||
body: JSON.stringify({ username, password }),
|
authState.putCb()
|
||||||
headers: {
|
setAuthState({ ...authState, putCb: undefined })
|
||||||
'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
|
|
||||||
}
|
}
|
||||||
}
|
}, [authState.token])
|
||||||
|
|
||||||
const logout: AuthContextType['logout'] = () => {
|
|
||||||
setToken(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
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}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
useRouteContext,
|
useNavigate,
|
||||||
useRouter
|
useRouter
|
||||||
} from '@tanstack/react-router'
|
} from '@tanstack/react-router'
|
||||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
|
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
|
||||||
import RouterContext from '../RouterContext'
|
import RouterContext from '../RouterContext'
|
||||||
|
import { useAuth } from '../auth'
|
||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
const logout = useRouteContext({
|
const auth = useAuth()
|
||||||
from: '__root__',
|
|
||||||
select: s => s.auth.logout
|
|
||||||
})
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const onLogout = async () => {
|
const onLogout = async () => {
|
||||||
logout()
|
auth.clearToken(async () => {
|
||||||
await router.invalidate()
|
await router.invalidate()
|
||||||
router.navigate({ to: '/login' })
|
await navigate({ to: '/login' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
createFileRoute,
|
createFileRoute,
|
||||||
redirect,
|
redirect,
|
||||||
useRouteContext,
|
useNavigate,
|
||||||
useRouter,
|
useRouter,
|
||||||
useSearch
|
useSearch
|
||||||
} from '@tanstack/react-router'
|
} from '@tanstack/react-router'
|
||||||
import { FormEvent } from 'react'
|
import { FormEvent, useState } from 'react'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import login from '../api/login'
|
||||||
|
import { useAuth } from '../auth'
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const login = useRouteContext({ from: '/login', select: s => s.auth.login })
|
const auth = useAuth()
|
||||||
const error = useRouteContext({ from: '/login', select: s => s.auth.error })
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const navigate = useNavigate()
|
||||||
const search = useSearch({ from: '/login' })
|
const search = useSearch({ from: '/login' })
|
||||||
|
|
||||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
@ -19,10 +23,14 @@ const Login = () => {
|
|||||||
const formData = new FormData(event.currentTarget)
|
const formData = new FormData(event.currentTarget)
|
||||||
const username = (formData.get('username') as string | null) ?? ''
|
const username = (formData.get('username') as string | null) ?? ''
|
||||||
const password = (formData.get('password') as string | null) ?? ''
|
const password = (formData.get('password') as string | null) ?? ''
|
||||||
const success = await login(username, password)
|
const loginResult = await login(username, password)
|
||||||
if (success) {
|
if (loginResult._tag === 'success') {
|
||||||
await router.invalidate()
|
auth.putToken(loginResult.loginView.accessToken, async () => {
|
||||||
router.navigate({ to: search.redirect || '/' })
|
await router.invalidate()
|
||||||
|
await navigate({ to: search.redirect ?? '/' })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setError(loginResult.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user