added AuthContext; added verify email page; improvements; bugfixes

This commit is contained in:
Myzel394 2022-10-15 18:50:47 +02:00
parent fddf025b08
commit 0a0b9b55f8
15 changed files with 211 additions and 20 deletions

View File

@ -20,6 +20,7 @@
"plugins": ["ordered-imports", "react", "@typescript-eslint"],
"rules": {
"react/react-in-jsx-scope": "off",
"compat/compat": "error",
"ordered-imports/ordered-imports": [
"error",
{

View File

@ -49,5 +49,9 @@
"prettier": "^2.7.1",
"typescript": "^4.6.4",
"vite": "^3.1.0"
}
},
"browserslist": [
"defaults",
"not op_mini all"
]
}

View File

@ -7,6 +7,7 @@ import {CssBaseline, ThemeProvider} from "@mui/material"
import {queryClient} from "~/constants/react-query"
import {lightTheme} from "~/constants/themes"
import {getServerSettings} from "~/apis"
import AuthContextProvider from "~/AuthContext/AuthContextProvider"
import RootRoute from "~/routes/Root"
import SignupRoute from "~/routes/SignupRoute"
import SingleElementRoute from "~/routes/SingleElementRoute"
@ -43,8 +44,10 @@ export default function App(): ReactElement {
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}>
<CssBaseline />
<RouterProvider router={router} />
<AuthContextProvider>
<CssBaseline />
<RouterProvider router={router} />
</AuthContextProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>

View File

@ -0,0 +1,37 @@
import {createContext} from "react"
import {User} from "~/server-types"
interface AuthContextTypeBase {
user: User | null
isAuthenticated: boolean
login: (user: User) => Promise<void>
logout: () => void
}
interface AuthContextTypeAuthenticated {
user: User
isAuthenticated: true
}
interface AuthContextTypeUnauthenticated {
user: null
isAuthenticated: false
}
export type AuthContextType =
| AuthContextTypeBase
| (AuthContextTypeAuthenticated & AuthContextTypeUnauthenticated)
const AuthContext = createContext<AuthContextType>({
user: null,
isAuthenticated: false,
login: () => {
throw new Error("login() not implemented")
},
logout: () => {
throw new Error("logout() not implemented")
},
})
export default AuthContext

View File

@ -0,0 +1,55 @@
import {ReactElement, ReactNode, useCallback, useMemo} from "react"
import {AxiosError} from "axios"
import {useLocalStorage} from "react-use"
import {useMutation} from "@tanstack/react-query"
import {User} from "~/server-types"
import {RefreshTokenResult, logout as logoutUser, refreshToken} from "~/apis"
import AuthContext, {AuthContextType} from "./AuthContext"
export interface AuthContextProviderProps {
children: ReactNode
}
export default function AuthContextProvider({
children,
}: AuthContextProviderProps): ReactElement {
const [user, setUser] = useLocalStorage<User | null>(
"_global-context-auth-user",
null,
)
const logout = useCallback(async (forceLogout = true) => {
setUser(null)
if (forceLogout) {
await logoutUser()
}
}, [])
const login = useCallback(async (user: User) => {
setUser(user)
}, [])
const {mutateAsync: refresh} = useMutation<
RefreshTokenResult,
AxiosError,
void
>(refreshToken, {
onError: () => logout(false),
})
const value = useMemo<AuthContextType>(
() => ({
user: user ?? null,
login,
logout,
isAuthenticated: user !== null,
}),
[refresh, login, logout],
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}

View File

@ -1,6 +1,6 @@
import axios from "axios"
import {ServerSettings} from "~/types"
import {ServerSettings} from "~/server-types"
export default async function getServerSettings(): Promise<ServerSettings> {
return (await axios.get(`${import.meta.env.VITE_SERVER_BASE_URL}/settings`))

View File

@ -0,0 +1,20 @@
import {User} from "~/server-types"
export default function parseUser(user: any): User {
return {
id: user.id,
createdAt: new Date(user.created_at),
email: {
address: user.email.address,
isVerified: user.email.is_verified,
},
preferences: {
aliasRemoveTrackers: user.preferences.alias_remove_trackers,
aliasCreateMailReport: user.preferences.alias_create_mail_report,
aliasProxyImages: user.preferences.alias_proxy_images,
aliasImageProxyFormat: user.preferences.alias_image_proxy_format,
aliasImageProxyUserAgent:
user.preferences.alias_image_proxy_user_agent,
},
}
}

View File

@ -8,3 +8,7 @@ export * from "./validate-email"
export {default as validateEmail} from "./validate-email"
export * from "./resend-email-verification-code"
export {default as resendEmailVerificationCode} from "./resend-email-verification-code"
export * from "./refresh-token"
export {default as refreshToken} from "./refresh-token"
export * from "./logout"
export {default as logout} from "./logout"

11
src/apis/logout.ts Normal file
View File

@ -0,0 +1,11 @@
import axios from "axios"
import {MinimumServerResponse} from "~/server-types"
export default async function logout(): Promise<MinimumServerResponse> {
const {data} = await axios.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/logout`,
)
return data
}

16
src/apis/refresh-token.ts Normal file
View File

@ -0,0 +1,16 @@
import axios from "axios"
import {User} from "~/server-types"
export interface RefreshTokenResult {
user: User
detail: string
}
export default async function refreshToken(): Promise<RefreshTokenResult> {
const {data} = await axios.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/api/refresh-token`,
)
return data
}

View File

@ -1,6 +1,7 @@
import axios from "axios"
import {AuthenticationDetails} from "~/types"
import {AuthenticationDetails} from "~/server-types"
import parseUser from "~/apis/helpers/parse-user"
export interface ValidateEmailData {
email: string
@ -18,6 +19,10 @@ export default async function validateEmail({
token: token,
},
)
console.log(data)
return data
return {
...data,
user: parseUser(data.user),
}
}

View File

@ -1,2 +1,4 @@
export * from "./use-interval-update"
export {default as useIntervalUpdate} from "./use-interval-update"
export * from "./use-query-params"
export {default as useQueryParams} from "./use-query-params"

View File

@ -0,0 +1,18 @@
import {useMemo} from "react"
import {useLocation} from "react-router-dom"
export default function useQueryParams<T>(): T {
const location = useLocation()
return useMemo(() => {
const params = new URLSearchParams(location.search)
const result: Record<string, string> = {}
for (const [key, value] of params) {
result[key] = value
}
return result as T
}, [location.search])
}

View File

@ -1,19 +1,25 @@
import * as yup from "yup"
import {useLoaderData, useParams} from "react-router-dom"
import {useLoaderData, useNavigate} from "react-router-dom"
import {useAsync} from "react-use"
import {MdCancel} from "react-icons/md"
import React, {ReactElement} from "react"
import {AxiosError} from "axios"
import React, {ReactElement, useContext} from "react"
import {Grid, Paper, Typography, useTheme} from "@mui/material"
import {useMutation} from "@tanstack/react-query"
import {ServerSettings} from "~/server-types"
import {validateEmail} from "~/apis"
import {AuthenticationDetails, ServerSettings} from "~/server-types"
import {ValidateEmailData, validateEmail} from "~/apis"
import {useQueryParams} from "~/hooks"
import AuthContext from "~/AuthContext/AuthContext"
const emailSchema = yup.string().email()
export default function VerifyEmailRoute(): ReactElement {
const theme = useTheme()
const {email, token} = useParams<{
const navigate = useNavigate()
const {login} = useContext(AuthContext)
const {email, token} = useQueryParams<{
email: string
token: string
}>()
@ -29,18 +35,27 @@ export default function VerifyEmailRoute(): ReactElement {
// Check token only contains chars from `serverSettings.email_verification_chars`
const chars = serverSettings.email_verification_chars.split("")
return token.split("").every(chars.includes)
return token.split("").every(char => chars.includes(char))
})
const {mutateAsync: verifyEmail} = useMutation<
AuthenticationDetails,
AxiosError,
ValidateEmailData
>(validateEmail, {
onSuccess: async ({user}) => {
await login(user)
navigate("/")
},
})
const {loading} = useAsync(async () => {
await emailSchema.validate(email)
await tokenSchema.validate(token)
await validateEmail({
token: token as string,
email: email as string,
await verifyEmail({
email,
token,
})
return true
}, [email, token])
return (
@ -82,7 +97,7 @@ export default function VerifyEmailRoute(): ReactElement {
component="p"
align="center"
>
Sorry, but this token is invalid.
Sorry, but this verification code is invalid.
</Typography>
</Grid>
</>

View File

@ -30,8 +30,8 @@ export interface User {
}
export interface AuthenticationDetails {
access_token: string
refresh_token: string
user: User
detail: string
}
export interface ServerSettings {