mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 02:12:38 +01:00
implement hcaptcha
This commit is contained in:
parent
092de4eac5
commit
32671b9ebc
@ -6,6 +6,7 @@
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@hcaptcha/react-hcaptcha": "^1.8.0",
|
||||
"@mattjennings/react-modal-stack": "^1.0.4",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@puyodead1/fosscord-ts": "github:Puyodead1/fosscord.ts",
|
||||
|
12008
pnpm-lock.yaml
12008
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
35
src/components/captcha/HCaptchaModal.tsx
Normal file
35
src/components/captcha/HCaptchaModal.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import HCaptchaLib from "@hcaptcha/react-hcaptcha";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
siteKey: string;
|
||||
onVerify: (token: string) => void;
|
||||
}
|
||||
|
||||
const Wrapper = styled.form`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
function HCaptchaModal({ open, siteKey, onVerify }: Props) {
|
||||
const ref = React.useRef<HCaptchaLib>(null);
|
||||
|
||||
const onLoad = () => {
|
||||
ref.current?.execute();
|
||||
};
|
||||
|
||||
return open ? (
|
||||
<Wrapper>
|
||||
<HCaptchaLib
|
||||
sitekey={siteKey}
|
||||
onLoad={onLoad}
|
||||
onVerify={onVerify}
|
||||
ref={ref}
|
||||
/>
|
||||
</Wrapper>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default HCaptchaModal;
|
@ -2,6 +2,7 @@ import "@fontsource/roboto/300.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/500.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
import { ModalStack } from "@mattjennings/react-modal-stack";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
@ -13,7 +14,9 @@ const root = ReactDOM.createRoot(
|
||||
);
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
<Theme />
|
||||
<ModalStack>
|
||||
<App />
|
||||
<Theme />
|
||||
</ModalStack>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
148
src/pages/LoginPage.components.tsx
Normal file
148
src/pages/LoginPage.components.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import styled from "styled-components";
|
||||
import Button from "../components/Button";
|
||||
import Container from "../components/Container";
|
||||
|
||||
export const Wrapper = styled(Container)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: var(--secondary);
|
||||
`;
|
||||
|
||||
export const LoginBox = styled(Container)`
|
||||
background-color: var(--primary-alt);
|
||||
padding: 32px;
|
||||
font-size: 18px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
width: 480px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Header = styled.h1`
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--text);
|
||||
`;
|
||||
|
||||
export const SubHeader = styled.h2`
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
export const FormContainer = styled.form`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const InputContainer = styled.h1<{ marginBottom: boolean }>`
|
||||
margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
export const LabelWrapper = styled.div<{ error?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")};
|
||||
`;
|
||||
|
||||
export const InputErrorText = styled.label`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
export const InputLabel = styled.label`
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
export const InputWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const Input = styled.input<{ error?: boolean }>`
|
||||
outline: none;
|
||||
background: var(--secondary);
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
border: none;
|
||||
aria-invalid: ${(props) => (props.error ? "true" : "false")};
|
||||
`;
|
||||
|
||||
export const PasswordResetLink = styled.button`
|
||||
margin-bottom: 20px;
|
||||
margin-top: 4px;
|
||||
padding: 2px 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
color: var(--text-link);
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoginButton = styled(Button)`
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
min-width: 130px;
|
||||
min-height: 44px;
|
||||
`;
|
||||
|
||||
export const RegisterContainer = styled.div`
|
||||
margin-top: 4px;
|
||||
text-align: initial;
|
||||
`;
|
||||
|
||||
export const RegisterLabel = styled.label`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const RegisterLink = styled.button`
|
||||
font-size: 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-link);
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Divider = styled.span`
|
||||
padding: 0 4px;
|
||||
`;
|
@ -1,192 +1,82 @@
|
||||
import HCaptchaLib from "@hcaptcha/react-hcaptcha";
|
||||
import { APIError, CaptchaError, MFAError } from "@puyodead1/fosscord-ts";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Button from "../components/Button";
|
||||
import Container from "../components/Container";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
|
||||
const Wrapper = styled(Container)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: var(--secondary);
|
||||
`;
|
||||
|
||||
const LoginBox = styled(Container)`
|
||||
background-color: var(--primary-alt);
|
||||
padding: 32px;
|
||||
font-size: 18px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
width: 480px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Header = styled.h1`
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--text);
|
||||
`;
|
||||
|
||||
const SubHeader = styled.h2`
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const FormContainer = styled.form`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const InputContainer = styled.h1<{ marginBottom: boolean }>`
|
||||
margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const LabelWrapper = styled.div<{ error?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")};
|
||||
`;
|
||||
|
||||
const InputErrorText = styled.label`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
const InputLabel = styled.label`
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Input = styled.input<{ error?: boolean }>`
|
||||
outline: none;
|
||||
background: var(--secondary);
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
border: none;
|
||||
aria-invalid: ${(props) => (props.error ? "true" : "false")};
|
||||
`;
|
||||
|
||||
const PasswordResetLink = styled.button`
|
||||
margin-bottom: 20px;
|
||||
margin-top: 4px;
|
||||
padding: 2px 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
color: var(--text-link);
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const LoginButton = styled(Button)`
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
min-width: 130px;
|
||||
min-height: 44px;
|
||||
`;
|
||||
|
||||
const RegisterContainer = styled.div`
|
||||
margin-top: 4px;
|
||||
text-align: initial;
|
||||
`;
|
||||
|
||||
const RegisterLabel = styled.label`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const RegisterLink = styled.button`
|
||||
font-size: 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-link);
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const Divider = styled.span`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
import {
|
||||
Divider,
|
||||
FormContainer,
|
||||
Header,
|
||||
HeaderContainer,
|
||||
Input,
|
||||
InputContainer,
|
||||
InputErrorText,
|
||||
InputLabel,
|
||||
InputWrapper,
|
||||
LabelWrapper,
|
||||
LoginBox,
|
||||
LoginButton,
|
||||
PasswordResetLink,
|
||||
RegisterContainer,
|
||||
RegisterLabel,
|
||||
RegisterLink,
|
||||
SubHeader,
|
||||
Wrapper,
|
||||
} from "./LoginPage.components";
|
||||
|
||||
type LoginFormValues = {
|
||||
login: string;
|
||||
password: string;
|
||||
captcha_key?: string;
|
||||
};
|
||||
|
||||
function LoginPage() {
|
||||
const app = useAppStore();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [captchaSiteKey, setCaptchaSiteKey] = React.useState<string>();
|
||||
const captchaRef = React.useRef<HCaptchaLib>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
setValue,
|
||||
} = useForm<LoginFormValues>();
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
app.api.login(data).catch((e) => {
|
||||
if (e instanceof MFAError) {
|
||||
console.log("MFA Required", e);
|
||||
} else if (e instanceof CaptchaError) {
|
||||
console.log("Captcha Required", e);
|
||||
} else if (e instanceof APIError) {
|
||||
console.log("APIError", e.message, e.code, e.fieldErrors);
|
||||
e.fieldErrors.forEach((fieldError) => {
|
||||
setError(fieldError.field as any, {
|
||||
type: "manual",
|
||||
message: fieldError.error,
|
||||
setLoading(true);
|
||||
app.api
|
||||
.login(data)
|
||||
.catch((e) => {
|
||||
if (e instanceof MFAError) {
|
||||
console.log("MFA Required", e);
|
||||
} else if (e instanceof CaptchaError) {
|
||||
// TODO: other captcha services
|
||||
setCaptchaSiteKey(e.captcha_sitekey);
|
||||
captchaRef.current?.execute();
|
||||
} else if (e instanceof APIError) {
|
||||
console.log("APIError", e.message, e.code, e.fieldErrors);
|
||||
e.fieldErrors.forEach((fieldError) => {
|
||||
setError(fieldError.field as any, {
|
||||
type: "manual",
|
||||
message: fieldError.error,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log("General Error", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("General Error", e);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
});
|
||||
|
||||
const onCaptchaVerify = (token: string) => {
|
||||
setValue("captcha_key", token);
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LoginBox>
|
||||
@ -214,6 +104,7 @@ function LoginPage() {
|
||||
autoFocus
|
||||
{...register("login", { required: true })}
|
||||
error={!!errors.login}
|
||||
disabled={loading}
|
||||
/>
|
||||
</InputWrapper>
|
||||
</InputContainer>
|
||||
@ -235,6 +126,7 @@ function LoginPage() {
|
||||
type="password"
|
||||
{...register("password", { required: true })}
|
||||
error={!!errors.password}
|
||||
disabled={loading}
|
||||
/>
|
||||
</InputWrapper>
|
||||
</InputContainer>
|
||||
@ -242,7 +134,26 @@ function LoginPage() {
|
||||
<PasswordResetLink onClick={() => {}} type="button">
|
||||
Forgot your password?
|
||||
</PasswordResetLink>
|
||||
<LoginButton variant="primary" type="submit">
|
||||
{captchaSiteKey && (
|
||||
<HCaptchaLib
|
||||
sitekey={captchaSiteKey}
|
||||
onVerify={onCaptchaVerify}
|
||||
ref={captchaRef}
|
||||
onChalExpired={() => {
|
||||
console.log("Captcha challenge Expired");
|
||||
// TODO: red outline?
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.log("Captcha Error", e);
|
||||
// TODO: red outline?
|
||||
}}
|
||||
onExpire={() => {
|
||||
console.log("Captcha Expired");
|
||||
// TODO: red outline?
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LoginButton variant="primary" type="submit" disabled={loading}>
|
||||
Log In
|
||||
</LoginButton>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user