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"], "plugins": ["ordered-imports", "react", "@typescript-eslint"],
"rules": { "rules": {
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"compat/compat": "error",
"ordered-imports/ordered-imports": [ "ordered-imports/ordered-imports": [
"error", "error",
{ {

View File

@ -49,5 +49,9 @@
"prettier": "^2.7.1", "prettier": "^2.7.1",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"vite": "^3.1.0" "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 {queryClient} from "~/constants/react-query"
import {lightTheme} from "~/constants/themes" import {lightTheme} from "~/constants/themes"
import {getServerSettings} from "~/apis" import {getServerSettings} from "~/apis"
import AuthContextProvider from "~/AuthContext/AuthContextProvider"
import RootRoute from "~/routes/Root" import RootRoute from "~/routes/Root"
import SignupRoute from "~/routes/SignupRoute" import SignupRoute from "~/routes/SignupRoute"
import SingleElementRoute from "~/routes/SingleElementRoute" import SingleElementRoute from "~/routes/SingleElementRoute"
@ -43,8 +44,10 @@ export default function App(): ReactElement {
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<CssBaseline /> <AuthContextProvider>
<RouterProvider router={router} /> <CssBaseline />
<RouterProvider router={router} />
</AuthContextProvider>
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>
</React.StrictMode> </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 axios from "axios"
import {ServerSettings} from "~/types" import {ServerSettings} from "~/server-types"
export default async function getServerSettings(): Promise<ServerSettings> { export default async function getServerSettings(): Promise<ServerSettings> {
return (await axios.get(`${import.meta.env.VITE_SERVER_BASE_URL}/settings`)) 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 {default as validateEmail} from "./validate-email"
export * from "./resend-email-verification-code" export * from "./resend-email-verification-code"
export {default as resendEmailVerificationCode} 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 axios from "axios"
import {AuthenticationDetails} from "~/types" import {AuthenticationDetails} from "~/server-types"
import parseUser from "~/apis/helpers/parse-user"
export interface ValidateEmailData { export interface ValidateEmailData {
email: string email: string
@ -18,6 +19,10 @@ export default async function validateEmail({
token: token, 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 * from "./use-interval-update"
export {default as useIntervalUpdate} 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 * as yup from "yup"
import {useLoaderData, useParams} from "react-router-dom" import {useLoaderData, useNavigate} from "react-router-dom"
import {useAsync} from "react-use" import {useAsync} from "react-use"
import {MdCancel} from "react-icons/md" 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 {Grid, Paper, Typography, useTheme} from "@mui/material"
import {useMutation} from "@tanstack/react-query"
import {ServerSettings} from "~/server-types" import {AuthenticationDetails, ServerSettings} from "~/server-types"
import {validateEmail} from "~/apis" import {ValidateEmailData, validateEmail} from "~/apis"
import {useQueryParams} from "~/hooks"
import AuthContext from "~/AuthContext/AuthContext"
const emailSchema = yup.string().email() const emailSchema = yup.string().email()
export default function VerifyEmailRoute(): ReactElement { export default function VerifyEmailRoute(): ReactElement {
const theme = useTheme() const theme = useTheme()
const {email, token} = useParams<{ const navigate = useNavigate()
const {login} = useContext(AuthContext)
const {email, token} = useQueryParams<{
email: string email: string
token: string token: string
}>() }>()
@ -29,18 +35,27 @@ export default function VerifyEmailRoute(): ReactElement {
// Check token only contains chars from `serverSettings.email_verification_chars` // Check token only contains chars from `serverSettings.email_verification_chars`
const chars = serverSettings.email_verification_chars.split("") 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 () => { const {loading} = useAsync(async () => {
await emailSchema.validate(email) await emailSchema.validate(email)
await tokenSchema.validate(token) await tokenSchema.validate(token)
await validateEmail({ await verifyEmail({
token: token as string, email,
email: email as string, token,
}) })
return true
}, [email, token]) }, [email, token])
return ( return (
@ -82,7 +97,7 @@ export default function VerifyEmailRoute(): ReactElement {
component="p" component="p"
align="center" align="center"
> >
Sorry, but this token is invalid. Sorry, but this verification code is invalid.
</Typography> </Typography>
</Grid> </Grid>
</> </>

View File

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