From 0a0b9b55f83f87ca2338cd93c77fcf85caba13db Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Sat, 15 Oct 2022 18:50:47 +0200
Subject: [PATCH] added AuthContext; added verify email page; improvements;
bugfixes
---
.eslintrc.json | 1 +
package.json | 6 ++-
src/App.tsx | 7 +++-
src/AuthContext/AuthContext.ts | 37 +++++++++++++++++
src/AuthContext/AuthContextProvider.tsx | 55 +++++++++++++++++++++++++
src/apis/get-server-settings.ts | 2 +-
src/apis/helpers/parse-user.ts | 20 +++++++++
src/apis/index.ts | 4 ++
src/apis/logout.ts | 11 +++++
src/apis/refresh-token.ts | 16 +++++++
src/apis/validate-email.ts | 9 +++-
src/hooks/index.ts | 2 +
src/hooks/use-query-params.ts | 18 ++++++++
src/routes/VerifyEmailRoute.tsx | 39 ++++++++++++------
src/server-types.d.ts | 4 +-
15 files changed, 211 insertions(+), 20 deletions(-)
create mode 100644 src/AuthContext/AuthContext.ts
create mode 100644 src/AuthContext/AuthContextProvider.tsx
create mode 100644 src/apis/helpers/parse-user.ts
create mode 100644 src/apis/logout.ts
create mode 100644 src/apis/refresh-token.ts
create mode 100644 src/hooks/use-query-params.ts
diff --git a/.eslintrc.json b/.eslintrc.json
index a7e87b3..1b59e82 100755
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -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",
{
diff --git a/package.json b/package.json
index 971d6be..68fba8d 100755
--- a/package.json
+++ b/package.json
@@ -49,5 +49,9 @@
"prettier": "^2.7.1",
"typescript": "^4.6.4",
"vite": "^3.1.0"
- }
+ },
+ "browserslist": [
+ "defaults",
+ "not op_mini all"
+ ]
}
diff --git a/src/App.tsx b/src/App.tsx
index 6bc14f4..ddde9a8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 {
-
-
+
+
+
+
diff --git a/src/AuthContext/AuthContext.ts b/src/AuthContext/AuthContext.ts
new file mode 100644
index 0000000..a2fa2a2
--- /dev/null
+++ b/src/AuthContext/AuthContext.ts
@@ -0,0 +1,37 @@
+import {createContext} from "react"
+
+import {User} from "~/server-types"
+
+interface AuthContextTypeBase {
+ user: User | null
+ isAuthenticated: boolean
+ login: (user: User) => Promise
+ logout: () => void
+}
+
+interface AuthContextTypeAuthenticated {
+ user: User
+ isAuthenticated: true
+}
+
+interface AuthContextTypeUnauthenticated {
+ user: null
+ isAuthenticated: false
+}
+
+export type AuthContextType =
+ | AuthContextTypeBase
+ | (AuthContextTypeAuthenticated & AuthContextTypeUnauthenticated)
+
+const AuthContext = createContext({
+ user: null,
+ isAuthenticated: false,
+ login: () => {
+ throw new Error("login() not implemented")
+ },
+ logout: () => {
+ throw new Error("logout() not implemented")
+ },
+})
+
+export default AuthContext
diff --git a/src/AuthContext/AuthContextProvider.tsx b/src/AuthContext/AuthContextProvider.tsx
new file mode 100644
index 0000000..d9b6e19
--- /dev/null
+++ b/src/AuthContext/AuthContextProvider.tsx
@@ -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(
+ "_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(
+ () => ({
+ user: user ?? null,
+ login,
+ logout,
+ isAuthenticated: user !== null,
+ }),
+ [refresh, login, logout],
+ )
+
+ return {children}
+}
diff --git a/src/apis/get-server-settings.ts b/src/apis/get-server-settings.ts
index 81d8ef0..63dab47 100644
--- a/src/apis/get-server-settings.ts
+++ b/src/apis/get-server-settings.ts
@@ -1,6 +1,6 @@
import axios from "axios"
-import {ServerSettings} from "~/types"
+import {ServerSettings} from "~/server-types"
export default async function getServerSettings(): Promise {
return (await axios.get(`${import.meta.env.VITE_SERVER_BASE_URL}/settings`))
diff --git a/src/apis/helpers/parse-user.ts b/src/apis/helpers/parse-user.ts
new file mode 100644
index 0000000..1ece2ed
--- /dev/null
+++ b/src/apis/helpers/parse-user.ts
@@ -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,
+ },
+ }
+}
diff --git a/src/apis/index.ts b/src/apis/index.ts
index 4b7beea..f588252 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -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"
diff --git a/src/apis/logout.ts b/src/apis/logout.ts
new file mode 100644
index 0000000..e949f9a
--- /dev/null
+++ b/src/apis/logout.ts
@@ -0,0 +1,11 @@
+import axios from "axios"
+
+import {MinimumServerResponse} from "~/server-types"
+
+export default async function logout(): Promise {
+ const {data} = await axios.post(
+ `${import.meta.env.VITE_SERVER_BASE_URL}/auth/logout`,
+ )
+
+ return data
+}
diff --git a/src/apis/refresh-token.ts b/src/apis/refresh-token.ts
new file mode 100644
index 0000000..9f80f06
--- /dev/null
+++ b/src/apis/refresh-token.ts
@@ -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 {
+ const {data} = await axios.post(
+ `${import.meta.env.VITE_SERVER_BASE_URL}/api/refresh-token`,
+ )
+
+ return data
+}
diff --git a/src/apis/validate-email.ts b/src/apis/validate-email.ts
index e73a345..5f1b627 100644
--- a/src/apis/validate-email.ts
+++ b/src/apis/validate-email.ts
@@ -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),
+ }
}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index a71313c..5da00ae 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -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"
diff --git a/src/hooks/use-query-params.ts b/src/hooks/use-query-params.ts
new file mode 100644
index 0000000..353e894
--- /dev/null
+++ b/src/hooks/use-query-params.ts
@@ -0,0 +1,18 @@
+import {useMemo} from "react"
+import {useLocation} from "react-router-dom"
+
+export default function useQueryParams(): T {
+ const location = useLocation()
+
+ return useMemo(() => {
+ const params = new URLSearchParams(location.search)
+
+ const result: Record = {}
+
+ for (const [key, value] of params) {
+ result[key] = value
+ }
+
+ return result as T
+ }, [location.search])
+}
diff --git a/src/routes/VerifyEmailRoute.tsx b/src/routes/VerifyEmailRoute.tsx
index db1b87a..1b20b92 100644
--- a/src/routes/VerifyEmailRoute.tsx
+++ b/src/routes/VerifyEmailRoute.tsx
@@ -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.
>
diff --git a/src/server-types.d.ts b/src/server-types.d.ts
index 8b99bc7..77b51ac 100644
--- a/src/server-types.d.ts
+++ b/src/server-types.d.ts
@@ -30,8 +30,8 @@ export interface User {
}
export interface AuthenticationDetails {
- access_token: string
- refresh_token: string
+ user: User
+ detail: string
}
export interface ServerSettings {