Skip to content

Commit

Permalink
Changes to auth code
Browse files Browse the repository at this point in the history
  • Loading branch information
wbazant committed Nov 27, 2024
1 parent 03ff925 commit 13b4fca
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 108 deletions.
7 changes: 5 additions & 2 deletions src/components/auth/ConfirmationPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ const ConfirmationPage = () => {
const { email } = await confirmUser(token)
toast.success(t('devise.confirmations.confirmed'))
history.push({ pathname: '/users/sign_in', state: { email } })
} catch (e) {
toast.error(e.response?.data.error, { autoClose: 5000 })
} catch (error) {
toast.error(
`Account confirmation failed: ${error.message || 'Unknown error'}`,
{ autoClose: 5000 },
)
history.push('/users/confirmation/new')
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/auth/ConfirmationResendPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ const ConfirmationResendPage = () => {
autoClose: 5000,
})
history.push('/users/sign_in')
} catch (e) {
// Should not happen since API silently accepts any email
toast.error(e.response?.data.error)
console.error(e.response)
} catch (error) {
toast.error(
`Resending confirmation failed: ${error.message || 'Unknown error'}`,
)
recaptchaRef.current.reset()
}
}
Expand Down
5 changes: 0 additions & 5 deletions src/components/auth/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import Button from '../ui/Button'
import LabeledRow from '../ui/LabeledRow'
import {
Column,
ErrorMessage,
FormButtonWrapper,
FormCheckboxWrapper,
FormInputWrapper,
} from './AuthWrappers'

const LoginPage = () => {
const { user, isLoading } = useSelector((state) => state.auth)
const error = useSelector((state) => state.auth.error)
const { state, search } = useLocation()
const { t } = useTranslation()
const params = new URLSearchParams(search)
Expand Down Expand Up @@ -62,9 +60,6 @@ const LoginPage = () => {
label={t('glossary.password')}
/>
</FormInputWrapper>
{dirty && error && (
<ErrorMessage>{error.response.data.error}</ErrorMessage>
)}

<FormCheckboxWrapper>
<LabeledRow
Expand Down
8 changes: 4 additions & 4 deletions src/components/auth/PasswordResetPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ const PasswordResetPage = () => {
autoClose: 5000,
})
history.push('/users/sign_in')
} catch (e) {
// Should not happen since API silently accepts any email
toast.error(e.response?.data?.error)
console.error(e.response)
} catch (error) {
toast.error(
`Resetting password failed: ${error.message || 'Unknown error'}`,
)
recaptchaRef.current.reset()
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/auth/PasswordSetPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const PasswordSetPage = () => {

useEffect(() => {
if (!getResetToken()) {
// TODO: display this (among other toasts) on login page in permanent red error text rather than toast
toast.error(t('devise.passwords.no_token'), { autoClose: 5000 })
history.push('/users/sign_in')
}
Expand All @@ -49,9 +48,10 @@ const PasswordSetPage = () => {
autoClose: 5000,
})
history.push({ pathname: '/users/sign_in', state: { email } })
} catch (e) {
toast.error(e.response?.data.error)
console.error(e.response)
} catch (error) {
toast.error(
`Setting new password failed: ${error.message || 'Unknown error'}`,
)
history.push('/users/sign_in')
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/components/auth/SignupPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ const SignupPage = () => {
},
)
history.push('/map')
} catch (e) {
toast.error(e.response?.data?.error)
console.error(e.response)
} catch (error) {
toast.error(`Sign up failed: ${error.message || 'Unknown error'}`)
recaptchaRef.current.reset()
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/form/ReportModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const ReportModal = ({ locationId, name, onDismiss, ...props }) => {
try {
response = await addReport(reportValues)
toast.success('Report submitted successfully!')
} catch (e) {
toast.error(`Report submission failed: ${e.message}`)
} catch (error) {
toast.error(`Report submission failed: ${error.message}`)
}

if (response && !response.error) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/map/ConnectGeolocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const ConnectGeolocation = () => {
} else if (geolocation.error) {
dispatch(geolocationError(geolocation.error))
} else if (!geolocation.latitude || !geolocation.longitude) {
dispatch(geolocationError({ message: 'Unknown Error' }))
dispatch(geolocationError({ message: 'Unknown error' }))
} else if (isMapMoving) {
// Do nothing
} else {
Expand Down
126 changes: 64 additions & 62 deletions src/redux/authSlice.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,91 @@
import { createAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import i18next from 'i18next'
import { toast } from 'react-toastify'

import { editUser, getUser, getUserToken } from '../utils/api'
import { editUser, getUser, getUserToken, refreshUserToken } from '../utils/api'
import authStore from '../utils/authStore'

export const checkAuth = createAsyncThunk(
'auth/checkAuth',
async (_data, { rejectWithValue }) => {
let token
try {
token = authStore.getToken()
} catch (err) {
return rejectWithValue(err)
}
export const checkAuth = createAsyncThunk('auth/checkAuth', async (_data) => {
const token = authStore.initFromStorage()

if (!token?.access_token || !token?.refresh_token) {
return null
}

if (token?.access_token) {
return await getUser()
} else {
return null
try {
const user = await getUser(token.access_token)
authStore.setToken(token)
return user
} catch (error) {
if (
error.response?.status === 401 &&
error.response?.data?.error === 'Expired access token'
) {
let newToken
try {
newToken = await refreshUserToken(token.refresh_token)
} catch (refreshError) {
authStore.removeToken()
throw refreshError
}
const user = await getUser(newToken.access_token)
authStore.setToken(newToken)
return user
} else if (error.response?.status === 401) {
// We failed to log in with our token but can't fix the error based on response
authStore.removeToken()
}
},
)
throw error
}
})

export const login = createAsyncThunk(
'auth/login',
async ({ email, password, rememberMe }, { rejectWithValue }) => {
let token
try {
token = await getUserToken(email, password)
} catch (err) {
return rejectWithValue(err)
}
authStore.setToken(token, rememberMe)

return await getUser()
async ({ email, password, rememberMe }) => {
const token = await getUserToken(email, password)
const user = await getUser(token.access_token)
authStore.setRememberMe(rememberMe)
authStore.setToken(token)
return user
},
)

export const editProfile = createAsyncThunk(
'auth/editProfile',
async (userData, { rejectWithValue, getState }) => {
try {
const currentUser = getState().auth.user
const isEmailChanged = userData.email !== currentUser.email
const response = await editUser({ ...userData, range: currentUser.range })
return { response, isEmailChanged }
} catch (err) {
return rejectWithValue(err.response?.data?.error || err.message)
}
async (userData, { getState }) => {
const currentUser = getState().auth.user
const isEmailChanged = userData.email !== currentUser.email
const response = await editUser({ ...userData, range: currentUser.range })
return { response, isEmailChanged }
},
)

export const logout = createAction('auth/logout', () => {
authStore.removeToken()
return {}
})

const initialState = {
user: null,
error: null,
isLoading: true,
token: null,
}

export const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
isLoading: true,
},
reducers: {
setToken: (state, action) => {
state.token = action.payload
logout: (state) => {
authStore.removeToken()
state.user = null
},
},
initialState,
extraReducers: {
[checkAuth.pending]: (state) => {
state.isLoading = true
},
[checkAuth.fulfilled]: (state, action) => {
state.user = action.payload
state.error = null
state.isLoading = false
},
[checkAuth.rejected]: (state, action) => {
state.error = action.payload
toast.error(
`Authentication check failed: ${
action.error.message || 'Unknown error'
}`,
)
state.isLoading = false
},

Expand All @@ -91,25 +94,22 @@ export const authSlice = createSlice({
},
[login.fulfilled]: (state, action) => {
state.user = action.payload
state.error = null
state.isLoading = false
},
[login.rejected]: (state, action) => {
state.error = action.payload
toast.error(
`Sign in failed: ${action.error.message || 'Unknown error'}`,
{ autoClose: 5000 },
)
state.isLoading = false
},

[logout]: (state) => {
state.user = null
},

[editProfile.pending]: (state) => {
state.isLoading = true
},
[editProfile.fulfilled]: (state, action) => {
const { response, isEmailChanged } = action.payload
state.user = response
state.error = null
state.isLoading = false

if (isEmailChanged && response.unconfirmed_email) {
Expand All @@ -121,11 +121,13 @@ export const authSlice = createSlice({
}
},
[editProfile.rejected]: (state, action) => {
state.error = action.payload
toast.error(
`Profile update failed: ${action.error.message || 'Unknown error'}`,
)
state.isLoading = false
},
},
})

export const { setToken } = authSlice.actions
export const { logout } = authSlice.actions
export default authSlice.reducer
2 changes: 1 addition & 1 deletion src/redux/geolocationSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const geolocationSlice = createSlice({
// @see src/components/map/ConnectedGeolocation.js
state.geolocationState = GeolocationState.INITIAL
toast.error(
`Geolocation failed: ${action.payload.message || 'Unknown Error'}`,
`Geolocation failed: ${action.payload.message || 'Unknown error'}`,
)
break
}
Expand Down
31 changes: 18 additions & 13 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable no-console */

import axios from 'axios'
import { matchPath } from 'react-router'

Expand Down Expand Up @@ -36,9 +34,11 @@ instance.interceptors.request.use((config) => {
config.method === 'get' &&
config.url &&
matchPath(config.url, { path: anonymousGetUrls })
const token = authStore.getToken()
if (token && !isAnonymous) {
config.headers.Authorization = `Bearer ${token.access_token}`

const accessToken = authStore.getAccessToken()

if (accessToken && !isAnonymous) {
config.headers.Authorization = `Bearer ${accessToken}`
}

return config
Expand All @@ -54,19 +54,22 @@ instance.interceptors.response.use(
error.response.data.error === 'Expired access token' &&
!originalRequest._retry
) {
const token: any = authStore.getToken()
const refreshToken = authStore.getRefreshToken()

if (token) {
if (refreshToken) {
originalRequest._retry = true

const newToken = await refreshUserToken(token.refresh_token)
authStore.setToken(newToken, token.rememberMe)
const newToken = await refreshUserToken(refreshToken)
authStore.setToken(newToken)

return instance(originalRequest)
}
}

throw error
if (error?.response?.data?.error) {
throw { ...error, message: error.response.data.error }
} else {
throw error
}
},
)

Expand All @@ -78,7 +81,10 @@ export const editUser = (
data: paths['/user']['put']['requestBody']['content']['application/json'],
) => instance.put('/user', data)

export const getUser = () => instance.get('/user')
export const getUser = (accessToken: string) =>
instance.get('/user', {
headers: { Authorization: `Bearer ${accessToken}` },
})

export const deleteUser = () => instance.delete('/user')

Expand All @@ -104,7 +110,6 @@ export const getUserToken = (username: string, password: string) => {
export const refreshUserToken = (refreshToken: string) => {
const formData = new FormData()
formData.append('refresh_token', refreshToken)

return instance.post('/user/token/refresh', formData)
}

Expand Down
Loading

0 comments on commit 13b4fca

Please sign in to comment.