1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-22 02:12:38 +01:00

implement hcaptcha

This commit is contained in:
Puyodead1 2023-04-10 17:47:13 -04:00
parent 092de4eac5
commit 32671b9ebc
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
6 changed files with 4034 additions and 8406 deletions

View File

@ -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",

File diff suppressed because it is too large Load Diff

View 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;

View File

@ -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>
);

View 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;
`;

View File

@ -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>