Skip to content

Commit

Permalink
feat: add password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
GalvinGao committed Aug 28, 2023
1 parent 3d94a41 commit e8ebf10
Show file tree
Hide file tree
Showing 19 changed files with 738 additions and 121 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"deepmerge": "^4.3.1",
"fs": "^0.0.1-security",
"fuse.js": "^6.6.2",
"pinyin": "^2",
"pinyin": "^3.0.0-alpha.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
Expand All @@ -41,7 +41,6 @@
},
"devDependencies": {
"@types/node": "^20.5.6",
"@types/pinyin": "^2.10.0",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-relay": "^14.1.4",
Expand Down
30 changes: 30 additions & 0 deletions src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button } from "@mui/material"
import { FC } from "react"
import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"
import { Cover } from "./Tegami"

export const ErrorBoundaryFallback: ErrorBoundaryProps["FallbackComponent"] = ({
error,
resetErrorBoundary,
}) => {
return (
<Cover>
<h4 className="text-xl font-typing1 mb-2">Encounter</h4>
<h1 className="text-4xl font-bold font-typing0">Unexpected Error</h1>

<p className="text-lg mt-4 mb-8">{error?.message}</p>

<Button variant="contained" onClick={resetErrorBoundary}>
Retry
</Button>
</Cover>
)
}

export const withErrorBoundary = <T extends object>(Component: FC<T>) => {
return (props: T) => (
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<Component {...props} />
</ErrorBoundary>
)
}
File renamed without changes.
2 changes: 1 addition & 1 deletion src/components/Tegami.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const Footer: FC<{ className?: string }> = ({ className }) => {

export const WhiteRootLayout: FC<PropsWithChildren> = ({ children }) => {
return (
<div className="p-40 h-full w-full flex justify-center items-center">
<div className="p-20 h-full w-full flex justify-center items-center">
{children}
<Footer className="absolute bottom-12" />
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/layouts/AuthLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FC } from "react"
import { Outlet } from "react-router-dom"
import { WhiteRootLayout } from "../components/Tegami"

export const AuthLayout: FC = () => {
return (
<WhiteRootLayout>
<Outlet />
</WhiteRootLayout>
)
}
36 changes: 26 additions & 10 deletions src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AccountCircle, Logout } from "@mui/icons-material"
import { AccountCircle, Logout, Password } from "@mui/icons-material"
import {
AppBar,
CircularProgress,
Expand All @@ -12,6 +12,7 @@ import {
Typography,
} from "@mui/material"
import { FC, Suspense, useState } from "react"
import { ErrorBoundary } from "react-error-boundary"
import { toast } from "react-hot-toast"
import { graphql, useLazyLoadQuery } from "react-relay"
import { Outlet, useNavigate } from "react-router-dom"
Expand Down Expand Up @@ -54,20 +55,22 @@ export const RootLayout: FC = () => {
<div>{envBuildCommit || "未知构建"}</div>
</Tooltip>
<div className="flex-1" />
<Suspense
fallback={
<CircularProgress color="inherit" size={24} className="mr-3" />
}
>
{token && <AccountButton />}
</Suspense>
<ErrorBoundary FallbackComponent={() => <></>}>
<Suspense
fallback={
<CircularProgress color="inherit" size={24} className="mr-3" />
}
>
{token && <AccountButton />}
</Suspense>
</ErrorBoundary>
</Toolbar>
</AppBar>

<Container maxWidth="lg" className="py-24 h-full">
<Outlet />

<div className="w-full flex items-center jcustify-enter py-24">
<div className="w-full flex items-center justify-center py-24">
<Footer />
</div>
</Container>
Expand Down Expand Up @@ -127,9 +130,22 @@ const AccountButton: FC = () => {
您的用户 ID: <span className="font-mono">{data.me?.id}</span>
</div>
<div className="text-xs text-slate-500">
若需要更改账户信息,烦请联系开发组
若需要更改账户其他信息,烦请联系开发组
</div>
</div>

<MenuItem
onClick={() => {
setAnchorEl(null)
navigate("/auth/request-password-reset")
}}
>
<ListItemIcon>
<Password />
</ListItemIcon>
<div className="flex-1">修改密码</div>
</MenuItem>

<MenuItem
onClick={() => {
setToken("")
Expand Down
12 changes: 7 additions & 5 deletions src/pages/Login.tsx → src/pages/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { useMutation } from "react-relay"
import { useNavigate, useSearchParams } from "react-router-dom"
import { useEffectOnce, useUnmount } from "react-use"
import { graphql } from "relay-runtime"
import { Cover, WhiteRootLayout } from "../components/Tegami"
import { getToken } from "../utils/storage"
import { Cover } from "../../components/Tegami"
import { formatError } from "../../utils/friendlyError"
import { getToken } from "../../utils/storage"
import { LoginMutation } from "./__generated__/LoginMutation.graphql"

const StyledTextField = styled(TextField)<TextFieldProps>`
Expand Down Expand Up @@ -90,15 +91,15 @@ export const LoginPage: FC = () => {
},
onError: error => {
console.debug(error)
toast.error(`登录失败:${error.message}`)
toast.error(`登录失败:${formatError(error)}`)
turnstileRef.current?.reset()
setTurnstileResponse(undefined)
},
})
}

return (
<WhiteRootLayout>
<>
<Cover underOverlay={loading}>
<h4 className="text-xl font-typing1 mb-2">RogueStats</h4>
<h1 className="text-4xl font-bold font-typing0 mb-4">
Expand Down Expand Up @@ -134,6 +135,7 @@ export const LoginPage: FC = () => {
ref={turnstileRef}
siteKey="0x4AAAAAAAI_htC0Nx9f7D66"
onSuccess={token => setTurnstileResponse(token)}
onError={() => toast.error("人机验证失败,请刷新页面并重试")}
options={{
action: "login",
theme: "dark",
Expand Down Expand Up @@ -163,6 +165,6 @@ export const LoginPage: FC = () => {
<CircularProgress color="inherit" />
</div>
)}
</WhiteRootLayout>
</>
)
}
133 changes: 133 additions & 0 deletions src/pages/auth/RequestPasswordReset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Turnstile, TurnstileInstance } from "@marsidev/react-turnstile"
import {
CircularProgress,
TextField,
TextFieldProps,
styled,
} from "@mui/material"
import clsx from "clsx"
import { FC, useRef, useState } from "react"
import { toast } from "react-hot-toast"
import { useMutation } from "react-relay"
import { useUnmount } from "react-use"
import { graphql } from "relay-runtime"
import { Cover } from "../../components/Tegami"
import { formatError } from "../../utils/friendlyError"
import { RequestPasswordResetMutation } from "./__generated__/RequestPasswordResetMutation.graphql"

const StyledTextField = styled(TextField)<TextFieldProps>`
width: 100%;
font-family: "Typing0", sans-serif !important;
& .MuiOutlinedInput-root,
& .MuiOutlinedInput-input,
& .MuiInputLabel-root {
font-family: "Typing0", sans-serif !important;
}
`

export const RequestPasswordResetPage: FC = () => {
const [email, setEmail] = useState<string>("")
const [turnstileResponse, setTurnstileResponse] = useState<string>()
const [commitMutation, loading] =
useMutation<RequestPasswordResetMutation>(graphql`
mutation RequestPasswordResetMutation(
$input: RequestPasswordResetInput!
) {
requestPasswordReset(input: $input)
}
`)
const turnstileRef = useRef<TurnstileInstance>()

useUnmount(() => {
turnstileRef.current?.remove()
})

const handleLogin = () => {
if (!turnstileResponse) {
toast.error(
"人机验证仍在进行中,请稍等片刻。如果长时间未响应,请刷新页面重试。",
)
return
}

commitMutation({
variables: {
input: {
email,
turnstileResponse,
},
},
onCompleted: () => {
toast.success(
`登录信息重置请求已成功发起;请前往你的用户 E-mail 进行下一步操作。`,
)
},
onError: error => {
console.debug(error)
toast.error(`登录信息重置请求发起失败:${formatError(error)}`)
turnstileRef.current?.reset()
setTurnstileResponse(undefined)
},
})
}

return (
<>
<Cover underOverlay={loading}>
<h4 className="text-xl font-typing1 mb-2">RogueStats</h4>
<h1 className="text-4xl font-bold font-typing0 mb-4">
Request Password Reset
</h1>

<form
className="flex flex-col gap-4 font-typing0"
onSubmit={e => {
e.preventDefault()
}}
action=""
>
<StyledTextField
label="账户 E-mail"
variant="outlined"
autoComplete="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
/>
<Turnstile
ref={turnstileRef}
siteKey="0x4AAAAAAAI_htC0Nx9f7D66"
onSuccess={token => setTurnstileResponse(token)}
options={{
action: "reset-password",
theme: "dark",
size: "invisible",
tabIndex: -1,
}}
/>

<button
type="submit"
className={clsx(
"absolute bottom-12 left-[14%] right-0 w-[50%] h-24 flex items-center text-left pl-12 font-typing0",
turnstileResponse
? "text-2xl hover:after:content-['>>>'] hover:opacity-70 hover:underline active:after:content-['>>>>'] active:opacity-100 active:underline"
: "text-lg cursor-wait opacity-40",
)}
onClick={handleLogin}
disabled={!turnstileResponse}
>
{turnstileResponse ? "Request" : "Waiting for CAPTCHA response..."}
</button>
</form>
</Cover>

{loading && (
<div className="absolute inset-0 flex items-center justify-center text-white">
<CircularProgress color="inherit" />
</div>
)}
</>
)
}
Loading

0 comments on commit e8ebf10

Please sign in to comment.