Major refactor of auth and login logic.
This commit is contained in:
parent
af683d9ee9
commit
9cc05d0a7a
@ -1,51 +1,47 @@
|
|||||||
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||||
import { useRouter } from '@tanstack/react-router'
|
import { useLocation, useNavigate } from '@tanstack/react-router'
|
||||||
import React, { useState } from 'react'
|
import React, { useCallback, useMemo, useRef } from 'react'
|
||||||
import { ApiError } from './api/ApiError'
|
import { ApiError } from './api/ApiError'
|
||||||
import ExpiredTokenError from './api/ExpiredTokenError'
|
import ExpiredTokenError from './api/ExpiredTokenError'
|
||||||
import refresh, { RefreshTokenError } from './api/refresh'
|
import refresh, { RefreshTokenError } from './api/refresh'
|
||||||
import LoginView from './api/types/LoginView'
|
|
||||||
import { useAuth } from './auth'
|
import { useAuth } from './auth'
|
||||||
|
|
||||||
const AuthAwareQueryClientProvider = ({ children }: React.PropsWithChildren) => {
|
const AuthAwareQueryClientProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
const { putToken, clearToken } = useAuth()
|
const { putToken } = useAuth()
|
||||||
const router = useRouter()
|
const navigate = useNavigate()
|
||||||
const [currentlyRefreshing, setCurrentlyRefreshing] = useState(false)
|
const location = useLocation()
|
||||||
|
const refreshing = useRef(false)
|
||||||
|
|
||||||
const doRefresh = async () => {
|
const doRefresh = useCallback(async () => {
|
||||||
if (!currentlyRefreshing) {
|
putToken(null)
|
||||||
console.log('starting refresh')
|
if (!refreshing.current) {
|
||||||
setCurrentlyRefreshing(true)
|
refreshing.current = true
|
||||||
let refreshResult: LoginView
|
|
||||||
try {
|
try {
|
||||||
refreshResult = await refresh()
|
const { accessToken: token, expires, username } = await refresh()
|
||||||
|
putToken({
|
||||||
|
token,
|
||||||
|
expires,
|
||||||
|
username
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RefreshTokenError) {
|
if (error instanceof RefreshTokenError) {
|
||||||
console.log(`RefreshTokenError: ${error.reason}`)
|
navigate({
|
||||||
setCurrentlyRefreshing(false)
|
|
||||||
clearToken()
|
|
||||||
await router.navigate({
|
|
||||||
to: '/login',
|
to: '/login',
|
||||||
search: {
|
search: {
|
||||||
reason: error.reason,
|
reason: error.reason,
|
||||||
redirect: router.state.location.href
|
redirect: location.href
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log('post-navigate')
|
} else if (error instanceof ApiError) {
|
||||||
return
|
console.error(error)
|
||||||
} else {
|
|
||||||
setCurrentlyRefreshing(false)
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putToken(refreshResult.accessToken, refreshResult.username)
|
refreshing.current = false
|
||||||
setCurrentlyRefreshing(false)
|
|
||||||
console.log('refresh done')
|
|
||||||
}
|
}
|
||||||
}
|
}, [putToken, navigate, location])
|
||||||
|
|
||||||
const [queryClient] = useState<QueryClient>(
|
const queryClient = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new QueryClient({
|
new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@ -83,7 +79,8 @@ const AuthAwareQueryClientProvider = ({ children }: React.PropsWithChildren) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
|
[doRefresh]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
|
import AccessToken from '../types/AccessToken'
|
||||||
import { ApiError } from './ApiError'
|
import { ApiError } from './ApiError'
|
||||||
import ExpiredTokenError from './ExpiredTokenError'
|
import ExpiredTokenError from './ExpiredTokenError'
|
||||||
|
import { addBearer } from './util'
|
||||||
|
|
||||||
export interface AddStarDeps {
|
export interface AddStarDeps {
|
||||||
token: string
|
accessToken: AccessToken
|
||||||
username: string
|
username: string
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const addStar = async ({ slug, token, username }: AddStarDeps): Promise<void> => {
|
const addStar = async ({ slug, accessToken, username }: AddStarDeps): Promise<void> => {
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
headers.set('Authorization', `Bearer ${token}`)
|
addBearer(headers, accessToken)
|
||||||
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, {
|
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, {
|
||||||
headers,
|
headers,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import AccessToken from '../types/AccessToken'
|
||||||
import { ApiError } from './ApiError'
|
import { ApiError } from './ApiError'
|
||||||
import ExpiredTokenError from './ExpiredTokenError'
|
import ExpiredTokenError from './ExpiredTokenError'
|
||||||
|
import { addBearer } from './util'
|
||||||
|
|
||||||
export interface GetImageDeps {
|
export interface GetImageDeps {
|
||||||
accessToken: string | null
|
accessToken: AccessToken | null
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
@ -10,7 +12,7 @@ export interface GetImageDeps {
|
|||||||
const getImage = async ({ accessToken, signal, url }: GetImageDeps): Promise<string> => {
|
const getImage = async ({ accessToken, signal, url }: GetImageDeps): Promise<string> => {
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
if (accessToken !== null) {
|
if (accessToken !== null) {
|
||||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
addBearer(headers, accessToken)
|
||||||
}
|
}
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers,
|
headers,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AuthContextType } from '../auth'
|
import AccessToken from '../types/AccessToken'
|
||||||
import { ApiError } from './ApiError'
|
import { ApiError } from './ApiError'
|
||||||
import ExpiredTokenError from './ExpiredTokenError'
|
import ExpiredTokenError from './ExpiredTokenError'
|
||||||
import GetRecipeView, {
|
import GetRecipeView, {
|
||||||
@ -8,9 +8,10 @@ import GetRecipeView, {
|
|||||||
toGetRecipeView,
|
toGetRecipeView,
|
||||||
toGetRecipeViewWithRawText
|
toGetRecipeViewWithRawText
|
||||||
} from './types/GetRecipeView'
|
} from './types/GetRecipeView'
|
||||||
|
import { addBearer } from './util'
|
||||||
|
|
||||||
export interface GetRecipeCommonDeps {
|
export interface GetRecipeCommonDeps {
|
||||||
authContext: AuthContextType
|
accessToken: AccessToken | null
|
||||||
username: string
|
username: string
|
||||||
slug: string
|
slug: string
|
||||||
abortSignal: AbortSignal
|
abortSignal: AbortSignal
|
||||||
@ -30,15 +31,15 @@ export interface GetRecipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getRecipe = (async ({
|
const getRecipe = (async ({
|
||||||
authContext,
|
accessToken,
|
||||||
username,
|
username,
|
||||||
slug,
|
slug,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
includeRawText
|
includeRawText
|
||||||
}: GetRecipeDeps | GetRecipeDepsIncludeRawText): Promise<GetRecipeView | GetRecipeViewWithRawText> => {
|
}: GetRecipeDeps | GetRecipeDepsIncludeRawText): Promise<GetRecipeView | GetRecipeViewWithRawText> => {
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
if (authContext.token !== null) {
|
if (accessToken !== null) {
|
||||||
headers.set('Authorization', `Bearer ${authContext.token}`)
|
addBearer(headers, accessToken)
|
||||||
}
|
}
|
||||||
const query = includeRawText ? '?includeRawText=true' : ''
|
const query = includeRawText ? '?includeRawText=true' : ''
|
||||||
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}${query}`, {
|
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}${query}`, {
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
|
import AccessToken from '../types/AccessToken'
|
||||||
import { ApiError } from './ApiError'
|
import { ApiError } from './ApiError'
|
||||||
import ExpiredTokenError from './ExpiredTokenError'
|
import ExpiredTokenError from './ExpiredTokenError'
|
||||||
import { toImageView } from './types/ImageView'
|
import { toImageView } from './types/ImageView'
|
||||||
import RecipeInfosView, { RawRecipeInfosView } from './types/RecipeInfosView'
|
import RecipeInfosView, { RawRecipeInfosView } from './types/RecipeInfosView'
|
||||||
|
import { addBearer } from './util'
|
||||||
|
|
||||||
export interface GetRecipeInfosDeps {
|
export interface GetRecipeInfosDeps {
|
||||||
abortSignal: AbortSignal
|
abortSignal: AbortSignal
|
||||||
token: string | null
|
accessToken: AccessToken | null
|
||||||
pageNumber: number
|
pageNumber: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRecipeInfos = async ({
|
const getRecipeInfos = async ({
|
||||||
abortSignal,
|
abortSignal,
|
||||||
token,
|
accessToken,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
pageSize
|
pageSize
|
||||||
}: GetRecipeInfosDeps): Promise<RecipeInfosView> => {
|
}: GetRecipeInfosDeps): Promise<RecipeInfosView> => {
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
if (token !== null) {
|
if (accessToken !== null) {
|
||||||
headers.set('Authorization', `Bearer ${token}`)
|
addBearer(headers, accessToken)
|
||||||
}
|
}
|
||||||
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes?page=${pageNumber}&size=${pageSize}`, {
|
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes?page=${pageNumber}&size=${pageSize}`, {
|
||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
|
import AccessToken from '../types/AccessToken'
|
||||||
import { ApiError } from './ApiError'
|
import { ApiError } from './ApiError'
|
||||||
import ExpiredTokenError from './ExpiredTokenError'
|
import ExpiredTokenError from './ExpiredTokenError'
|
||||||
|
import { addBearer } from './util'
|
||||||
|
|
||||||
export interface RemoveStarDeps {
|
export interface RemoveStarDeps {
|
||||||
token: string
|
accessToken: AccessToken
|
||||||
username: string
|
username: string
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeStar = async ({ token, username, slug }: RemoveStarDeps) => {
|
const removeStar = async ({ accessToken, username, slug }: RemoveStarDeps) => {
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
headers.set('Authorization', `Bearer ${token}`)
|
addBearer(headers, accessToken)
|
||||||
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, {
|
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, {
|
||||||
headers,
|
headers,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import AccessToken from '../types/AccessToken'
|
||||||
import { ApiError } from './ApiError'
|
import { ApiError } from './ApiError'
|
||||||
import ExpiredTokenError from './ExpiredTokenError'
|
import ExpiredTokenError from './ExpiredTokenError'
|
||||||
import {
|
import {
|
||||||
@ -6,17 +7,23 @@ import {
|
|||||||
toGetRecipeViewWithRawText
|
toGetRecipeViewWithRawText
|
||||||
} from './types/GetRecipeView'
|
} from './types/GetRecipeView'
|
||||||
import UpdateRecipeSpec from './types/UpdateRecipeSpec'
|
import UpdateRecipeSpec from './types/UpdateRecipeSpec'
|
||||||
|
import { addBearer } from './util'
|
||||||
|
|
||||||
export interface UpdateRecipeDeps {
|
export interface UpdateRecipeDeps {
|
||||||
spec: UpdateRecipeSpec
|
spec: UpdateRecipeSpec
|
||||||
token: string
|
accessToken: AccessToken
|
||||||
username: string
|
username: string
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateRecipe = async ({ spec, token, username, slug }: UpdateRecipeDeps): Promise<GetRecipeViewWithRawText> => {
|
const updateRecipe = async ({
|
||||||
|
spec,
|
||||||
|
accessToken,
|
||||||
|
username,
|
||||||
|
slug
|
||||||
|
}: UpdateRecipeDeps): Promise<GetRecipeViewWithRawText> => {
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
headers.set('Authorization', `Bearer ${token}`)
|
addBearer(headers, accessToken)
|
||||||
headers.set('Content-type', 'application/json')
|
headers.set('Content-type', 'application/json')
|
||||||
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}`, {
|
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}`, {
|
||||||
headers,
|
headers,
|
||||||
|
5
src/api/util.ts
Normal file
5
src/api/util.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import AccessToken from '../types/AccessToken'
|
||||||
|
|
||||||
|
export const addBearer = (headers: Headers, accessToken: AccessToken) => {
|
||||||
|
headers.set('Authorization', `Bearer ${accessToken.token}`)
|
||||||
|
}
|
77
src/auth.tsx
77
src/auth.tsx
@ -1,55 +1,54 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
import React, { createContext, useContext, useReducer } from 'react'
|
||||||
|
import AccessToken from './types/AccessToken'
|
||||||
|
|
||||||
export interface AuthContextType {
|
export interface AuthContextType {
|
||||||
token: string | null
|
accessToken: AccessToken | null
|
||||||
username: string | null
|
putToken: (token: AccessToken | null) => void
|
||||||
putToken(token: string, username: string, cb?: () => void): void
|
|
||||||
clearToken(cb?: () => void): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthReducerState {
|
||||||
token: string | null
|
accessToken: AccessToken | null
|
||||||
username: string | null
|
}
|
||||||
putCb?: () => void
|
|
||||||
clearCb?: () => void
|
const initialState: AuthReducerState = {
|
||||||
|
accessToken: null
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthReducerAction = PutTokenAction | ClearTokenAction
|
||||||
|
|
||||||
|
interface PutTokenAction {
|
||||||
|
tag: 'putToken'
|
||||||
|
accessToken: AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClearTokenAction {
|
||||||
|
tag: 'clearToken'
|
||||||
|
}
|
||||||
|
|
||||||
|
const authReducer = (_state: AuthReducerState, action: AuthReducerAction): AuthReducerState => {
|
||||||
|
switch (action.tag) {
|
||||||
|
case 'putToken':
|
||||||
|
return { accessToken: action.accessToken }
|
||||||
|
case 'clearToken':
|
||||||
|
return { accessToken: null }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [authState, setAuthState] = useState<AuthState>({
|
const [state, dispatch] = useReducer(authReducer, initialState)
|
||||||
token: null,
|
|
||||||
username: null
|
|
||||||
})
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}, [authState.token])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
token: authState.token,
|
accessToken: state.accessToken,
|
||||||
username: authState.username,
|
putToken: token => {
|
||||||
putToken(token, username, cb) {
|
if (token === null) {
|
||||||
setAuthState({
|
dispatch({ tag: 'clearToken' })
|
||||||
token,
|
} else {
|
||||||
username,
|
dispatch({ tag: 'putToken', accessToken: token })
|
||||||
putCb: cb
|
}
|
||||||
})
|
|
||||||
},
|
|
||||||
clearToken(cb) {
|
|
||||||
setAuthState({
|
|
||||||
token: null,
|
|
||||||
username: null,
|
|
||||||
clearCb: cb
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useLocation, useNavigate, useRouter } from '@tanstack/react-router'
|
import { useLocation, useNavigate } from '@tanstack/react-router'
|
||||||
import { useAuth } from '../../auth'
|
import { useAuth } from '../../auth'
|
||||||
import classes from './header.module.css'
|
import classes from './header.module.css'
|
||||||
|
|
||||||
@ -7,20 +7,17 @@ export interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Header = ({ username }: HeaderProps) => {
|
const Header = ({ username }: HeaderProps) => {
|
||||||
const auth = useAuth()
|
const { putToken } = useAuth()
|
||||||
const router = useRouter()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const onLogin = async () => {
|
const onLogin = () => {
|
||||||
navigate({ to: '/login', search: { redirect: location.href } })
|
navigate({ to: '/login', search: { redirect: location.href } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLogout = async () => {
|
const onLogout = () => {
|
||||||
auth.clearToken(async () => {
|
putToken(null)
|
||||||
await router.invalidate()
|
navigate({ to: '/login' })
|
||||||
await navigate({ to: '/login' })
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -86,7 +86,7 @@ export interface EditRecipeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
||||||
const auth = useAuth()
|
const { accessToken } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@ -105,7 +105,7 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
|||||||
queryKey: ['recipes', username, slug],
|
queryKey: ['recipes', username, slug],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
getRecipe({
|
getRecipe({
|
||||||
authContext: auth,
|
accessToken,
|
||||||
username,
|
username,
|
||||||
slug,
|
slug,
|
||||||
abortSignal: signal,
|
abortSignal: signal,
|
||||||
@ -140,10 +140,10 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
|||||||
const mutation = useMutation(
|
const mutation = useMutation(
|
||||||
{
|
{
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
if (auth.token !== null) {
|
if (accessToken !== null) {
|
||||||
return updateRecipe({
|
return updateRecipe({
|
||||||
spec,
|
spec,
|
||||||
token: auth.token,
|
accessToken,
|
||||||
username,
|
username,
|
||||||
slug
|
slug
|
||||||
})
|
})
|
||||||
|
@ -51,14 +51,14 @@ interface RecipeStarButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RecipeStarButton = ({ username, slug, isStarred, starCount }: RecipeStarButtonProps) => {
|
const RecipeStarButton = ({ username, slug, isStarred, starCount }: RecipeStarButtonProps) => {
|
||||||
const authContext = useAuth()
|
const { accessToken } = useAuth()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const addStarMutation = useMutation({
|
const addStarMutation = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
if (authContext.token !== null) {
|
if (accessToken !== null) {
|
||||||
return addStar({
|
return addStar({
|
||||||
token: authContext.token,
|
accessToken,
|
||||||
slug,
|
slug,
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
@ -73,9 +73,9 @@ const RecipeStarButton = ({ username, slug, isStarred, starCount }: RecipeStarBu
|
|||||||
|
|
||||||
const removeStarMutation = useMutation({
|
const removeStarMutation = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
if (authContext.token !== null) {
|
if (accessToken !== null) {
|
||||||
return removeStar({
|
return removeStar({
|
||||||
token: authContext.token,
|
accessToken,
|
||||||
slug,
|
slug,
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
@ -111,7 +111,7 @@ export interface RecipeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Recipe = ({ username, slug }: RecipeProps) => {
|
const Recipe = ({ username, slug }: RecipeProps) => {
|
||||||
const authContext = useAuth()
|
const { accessToken } = useAuth()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const recipeQuery = useQuery(
|
const recipeQuery = useQuery(
|
||||||
@ -120,7 +120,7 @@ const Recipe = ({ username, slug }: RecipeProps) => {
|
|||||||
queryFn: ({ signal: abortSignal }) =>
|
queryFn: ({ signal: abortSignal }) =>
|
||||||
getRecipe({
|
getRecipe({
|
||||||
abortSignal,
|
abortSignal,
|
||||||
authContext,
|
accessToken,
|
||||||
username,
|
username,
|
||||||
slug
|
slug
|
||||||
})
|
})
|
||||||
@ -138,7 +138,7 @@ const Recipe = ({ username, slug }: RecipeProps) => {
|
|||||||
],
|
],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
getImage({
|
getImage({
|
||||||
accessToken: authContext.token,
|
accessToken,
|
||||||
signal,
|
signal,
|
||||||
url: recipeQuery.data!.recipe.mainImage!.url
|
url: recipeQuery.data!.recipe.mainImage!.url
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,7 @@ const Recipes = () => {
|
|||||||
const [pageNumber, setPageNumber] = useState(0)
|
const [pageNumber, setPageNumber] = useState(0)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
|
||||||
const { token } = useAuth()
|
const { accessToken } = useAuth()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { data, isPending, error } = useQuery(
|
const { data, isPending, error } = useQuery(
|
||||||
@ -22,7 +22,7 @@ const Recipes = () => {
|
|||||||
abortSignal: signal,
|
abortSignal: signal,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
pageSize,
|
pageSize,
|
||||||
token
|
accessToken
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
queryClient
|
queryClient
|
||||||
@ -42,7 +42,7 @@ const Recipes = () => {
|
|||||||
queryFn: async ({ signal }: any) => {
|
queryFn: async ({ signal }: any) => {
|
||||||
// any needed in the params
|
// any needed in the params
|
||||||
const imgUrl = await getImage({
|
const imgUrl = await getImage({
|
||||||
accessToken: token,
|
accessToken,
|
||||||
signal,
|
signal,
|
||||||
url: recipeInfoView.mainImage!.url
|
url: recipeInfoView.mainImage!.url
|
||||||
})
|
})
|
||||||
|
@ -8,11 +8,11 @@ import classes from './__root.module.css'
|
|||||||
import MainNav from '../components/main-nav/MainNav'
|
import MainNav from '../components/main-nav/MainNav'
|
||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
const { username } = useAuth()
|
const { accessToken } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header username={username ?? undefined} />
|
<Header username={accessToken?.username} />
|
||||||
<div className={classes.mainWrapper}>
|
<div className={classes.mainWrapper}>
|
||||||
<MainNav />
|
<MainNav />
|
||||||
<main>
|
<main>
|
||||||
|
@ -2,7 +2,7 @@ import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'
|
|||||||
|
|
||||||
export const Route = createFileRoute('/_auth')({
|
export const Route = createFileRoute('/_auth')({
|
||||||
beforeLoad({ context, location }) {
|
beforeLoad({ context, location }) {
|
||||||
if (!context.auth.token) {
|
if (!context.auth.accessToken) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: '/login',
|
to: '/login',
|
||||||
search: {
|
search: {
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { createFileRoute, redirect, useNavigate, useRouter, useSearch } from '@tanstack/react-router'
|
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
|
||||||
import { FormEvent, useState } from 'react'
|
import { FormEvent, useState } from 'react'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import login from '../api/login'
|
import login from '../api/login'
|
||||||
import { useAuth } from '../auth'
|
import { useAuth } from '../auth'
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const auth = useAuth()
|
const { putToken } = useAuth()
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { redirect, reason } = useSearch({ from: '/login' })
|
const { redirect, reason } = useSearch({ from: '/login' })
|
||||||
|
|
||||||
@ -19,12 +17,15 @@ const Login = () => {
|
|||||||
const password = (formData.get('password') as string | null) ?? ''
|
const password = (formData.get('password') as string | null) ?? ''
|
||||||
const loginResult = await login(username, password)
|
const loginResult = await login(username, password)
|
||||||
if (loginResult._tag === 'success') {
|
if (loginResult._tag === 'success') {
|
||||||
auth.putToken(loginResult.loginView.accessToken, loginResult.loginView.username, async () => {
|
const { accessToken: token, expires, username } = loginResult.loginView
|
||||||
await router.invalidate()
|
putToken({
|
||||||
await navigate({
|
token,
|
||||||
to: redirect ?? '/recipes',
|
expires,
|
||||||
search: {}
|
username
|
||||||
})
|
})
|
||||||
|
navigate({
|
||||||
|
to: redirect ?? '/recipes',
|
||||||
|
search: {}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setError(loginResult.error)
|
setError(loginResult.error)
|
||||||
@ -72,10 +73,5 @@ export const Route = createFileRoute('/login')({
|
|||||||
.optional(),
|
.optional(),
|
||||||
redirect: z.string().optional().catch('')
|
redirect: z.string().optional().catch('')
|
||||||
}),
|
}),
|
||||||
beforeLoad({ context, search }) {
|
|
||||||
if (search.reason === undefined && context.auth.token !== null) {
|
|
||||||
throw redirect({ to: '/recipes' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
component: Login
|
component: Login
|
||||||
})
|
})
|
||||||
|
7
src/types/AccessToken.ts
Normal file
7
src/types/AccessToken.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
interface AccessToken {
|
||||||
|
token: string
|
||||||
|
username: string
|
||||||
|
expires: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccessToken
|
Loading…
Reference in New Issue
Block a user