mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
added AuthContext; added verify email page; improvements; bugfixes
This commit is contained in:
parent
fddf025b08
commit
0a0b9b55f8
@ -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",
|
||||
{
|
||||
|
@ -49,5 +49,9 @@
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
|
@ -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>
|
||||
|
37
src/AuthContext/AuthContext.ts
Normal file
37
src/AuthContext/AuthContext.ts
Normal 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
|
55
src/AuthContext/AuthContextProvider.tsx
Normal file
55
src/AuthContext/AuthContextProvider.tsx
Normal 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>
|
||||
}
|
@ -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`))
|
||||
|
20
src/apis/helpers/parse-user.ts
Normal file
20
src/apis/helpers/parse-user.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
@ -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
11
src/apis/logout.ts
Normal 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
16
src/apis/refresh-token.ts
Normal 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
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
18
src/hooks/use-query-params.ts
Normal file
18
src/hooks/use-query-params.ts
Normal 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])
|
||||
}
|
@ -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>
|
||||
</>
|
||||
|
4
src/server-types.d.ts
vendored
4
src/server-types.d.ts
vendored
@ -30,8 +30,8 @@ export interface User {
|
||||
}
|
||||
|
||||
export interface AuthenticationDetails {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
user: User
|
||||
detail: string
|
||||
}
|
||||
|
||||
export interface ServerSettings {
|
||||
|
Loading…
x
Reference in New Issue
Block a user