mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +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"],
|
"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",
|
||||||
{
|
{
|
||||||
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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}>
|
||||||
|
<AuthContextProvider>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
|
</AuthContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</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 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`))
|
||||||
|
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 {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
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 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
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 * 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>
|
||||||
</>
|
</>
|
||||||
|
4
src/server-types.d.ts
vendored
4
src/server-types.d.ts
vendored
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user