mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-16 23:00:51 +00:00
feat: enhance captcha integration by improving type definitions and refactoring callback functions
This commit is contained in:
parent
a03a2bf9b0
commit
deb902463c
6 changed files with 104 additions and 67 deletions
|
|
@ -7,7 +7,12 @@ import {
|
|||
import Route from "Common/Types/API/Route";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||
import ModelForm, { FormType, ModelField } from "Common/UI/Components/Forms/ModelForm";
|
||||
import ModelForm, {
|
||||
FormType,
|
||||
ModelField,
|
||||
} from "Common/UI/Components/Forms/ModelForm";
|
||||
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
|
||||
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import Link from "Common/UI/Components/Link/Link";
|
||||
import Captcha from "Common/UI/Components/Captcha/Captcha";
|
||||
|
|
@ -85,8 +90,10 @@ const LoginPage: () => JSX.Element = () => {
|
|||
React.useState<boolean>(false);
|
||||
const [captchaResetSignal, setCaptchaResetSignal] = React.useState<number>(0);
|
||||
|
||||
const handleCaptchaReset = React.useCallback(() => {
|
||||
setCaptchaResetSignal((current: number) => current + 1);
|
||||
const handleCaptchaReset: () => void = React.useCallback(() => {
|
||||
setCaptchaResetSignal((current: number) => {
|
||||
return current + 1;
|
||||
});
|
||||
}, []);
|
||||
let loginFields: Array<ModelField<User>> = [
|
||||
{
|
||||
|
|
@ -134,17 +141,22 @@ const LoginPage: () => JSX.Element = () => {
|
|||
"Complete the captcha challenge so we know you're not a bot.",
|
||||
required: true,
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
getCustomElement: (_values, customProps) => (
|
||||
<Captcha
|
||||
siteKey={CAPTCHA_SITE_KEY}
|
||||
resetSignal={captchaResetSignal}
|
||||
error={customProps.error}
|
||||
onTokenChange={(token: string) => {
|
||||
customProps.onChange?.(token);
|
||||
}}
|
||||
onBlur={customProps.onBlur}
|
||||
/>
|
||||
),
|
||||
getCustomElement: (
|
||||
_values: FormValues<User>,
|
||||
customProps: CustomElementProps,
|
||||
) => {
|
||||
return (
|
||||
<Captcha
|
||||
siteKey={CAPTCHA_SITE_KEY}
|
||||
resetSignal={captchaResetSignal}
|
||||
error={customProps.error}
|
||||
onTokenChange={(token: string) => {
|
||||
customProps.onChange?.(token);
|
||||
}}
|
||||
onBlur={customProps.onBlur}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,13 @@ import URL from "Common/Types/API/URL";
|
|||
import Dictionary from "Common/Types/Dictionary";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import ModelForm, { FormType, ModelField } from "Common/UI/Components/Forms/ModelForm";
|
||||
import ModelForm, {
|
||||
FormType,
|
||||
ModelField,
|
||||
} from "Common/UI/Components/Forms/ModelForm";
|
||||
import { CustomElementProps } from "Common/UI/Components/Forms/Types/Field";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
|
||||
import Link from "Common/UI/Components/Link/Link";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import Captcha from "Common/UI/Components/Captcha/Captcha";
|
||||
|
|
@ -48,8 +53,10 @@ const RegisterPage: () => JSX.Element = () => {
|
|||
React.useState<boolean>(false);
|
||||
const [captchaResetSignal, setCaptchaResetSignal] = React.useState<number>(0);
|
||||
|
||||
const handleCaptchaReset = React.useCallback(() => {
|
||||
setCaptchaResetSignal((current: number) => current + 1);
|
||||
const handleCaptchaReset: () => void = React.useCallback(() => {
|
||||
setCaptchaResetSignal((current: number) => {
|
||||
return current + 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (UserUtil.isLoggedIn()) {
|
||||
|
|
@ -212,17 +219,22 @@ const RegisterPage: () => JSX.Element = () => {
|
|||
"Complete the captcha challenge so we know you're not a bot.",
|
||||
required: true,
|
||||
showEvenIfPermissionDoesNotExist: true,
|
||||
getCustomElement: (_values, customProps) => (
|
||||
<Captcha
|
||||
siteKey={CAPTCHA_SITE_KEY}
|
||||
resetSignal={captchaResetSignal}
|
||||
error={customProps.error}
|
||||
onTokenChange={(token: string) => {
|
||||
customProps.onChange?.(token);
|
||||
}}
|
||||
onBlur={customProps.onBlur}
|
||||
/>
|
||||
),
|
||||
getCustomElement: (
|
||||
_values: FormValues<User>,
|
||||
customProps: CustomElementProps,
|
||||
) => {
|
||||
return (
|
||||
<Captcha
|
||||
siteKey={CAPTCHA_SITE_KEY}
|
||||
resetSignal={captchaResetSignal}
|
||||
error={customProps.error}
|
||||
onTokenChange={(token: string) => {
|
||||
customProps.onChange?.(token);
|
||||
}}
|
||||
onBlur={customProps.onBlur}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
@ -266,7 +278,10 @@ const RegisterPage: () => JSX.Element = () => {
|
|||
maxPrimaryButtonWidth={true}
|
||||
fields={formFields}
|
||||
createOrUpdateApiUrl={apiUrl}
|
||||
onBeforeCreate={(item: User, miscDataProps: JSONObject): Promise<User> => {
|
||||
onBeforeCreate={(
|
||||
item: User,
|
||||
miscDataProps: JSONObject,
|
||||
): Promise<User> => {
|
||||
if (isCaptchaEnabled) {
|
||||
const captchaToken: string | undefined = (
|
||||
miscDataProps["captchaToken"] as string | undefined
|
||||
|
|
|
|||
|
|
@ -329,11 +329,9 @@ export const ProvisionSsl: boolean = process.env["PROVISION_SSL"] === "true";
|
|||
export const CaptchaEnabled: boolean =
|
||||
process.env["CAPTCHA_ENABLED"] === "true";
|
||||
|
||||
export const CaptchaSecretKey: string =
|
||||
process.env["CAPTCHA_SECRET_KEY"] || "";
|
||||
export const CaptchaSecretKey: string = process.env["CAPTCHA_SECRET_KEY"] || "";
|
||||
|
||||
export const CaptchaSiteKey: string =
|
||||
process.env["CAPTCHA_SITE_KEY"] || "";
|
||||
export const CaptchaSiteKey: string = process.env["CAPTCHA_SITE_KEY"] || "";
|
||||
|
||||
export const WorkflowScriptTimeoutInMS: number = process.env[
|
||||
"WORKFLOW_SCRIPT_TIMEOUT_IN_MS"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import axios, { AxiosError } from "axios";
|
||||
import axios, { AxiosError, AxiosResponse } from "axios";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import logger from "./Logger";
|
||||
import { CaptchaEnabled, CaptchaSecretKey } from "../EnvironmentConfig";
|
||||
|
|
@ -12,6 +12,11 @@ const REQUEST_TIMEOUT_MS: number = 5000;
|
|||
const GENERIC_ERROR_MESSAGE: string =
|
||||
"Captcha verification failed. Please try again.";
|
||||
|
||||
type HCaptchaResponse = {
|
||||
success?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
class CaptchaUtil {
|
||||
public static isCaptchaEnabled(): boolean {
|
||||
return CaptchaEnabled && Boolean(CaptchaSecretKey);
|
||||
|
|
@ -69,16 +74,17 @@ class CaptchaUtil {
|
|||
params.append("remoteip", remoteIp);
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
"https://hcaptcha.com/siteverify",
|
||||
params.toString(),
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
const response: AxiosResponse<HCaptchaResponse> =
|
||||
await axios.post<HCaptchaResponse>(
|
||||
"https://hcaptcha.com/siteverify",
|
||||
params.toString(),
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
},
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
if (!response.data?.success) {
|
||||
logger.warn(
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ export default class GreenlockUtil {
|
|||
expiresAt: QueryHelper.lessThanEqualTo(
|
||||
OneUptimeDate.addRemoveDays(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
40 // 40 days before expiry
|
||||
)
|
||||
40, // 40 days before expiry
|
||||
),
|
||||
),
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
|
|
@ -55,7 +55,7 @@ export default class GreenlockUtil {
|
|||
});
|
||||
|
||||
logger.debug(
|
||||
`Found ${certificates.length} certificates which are expiring soon`
|
||||
`Found ${certificates.length} certificates which are expiring soon`,
|
||||
);
|
||||
|
||||
// order certificate for each domain
|
||||
|
|
@ -70,12 +70,12 @@ export default class GreenlockUtil {
|
|||
try {
|
||||
//validate cname
|
||||
const isValidCname: boolean = await data.validateCname(
|
||||
certificate.domain
|
||||
certificate.domain,
|
||||
);
|
||||
|
||||
if (!isValidCname) {
|
||||
logger.debug(
|
||||
`CNAME is not valid for domain: ${certificate.domain}`
|
||||
`CNAME is not valid for domain: ${certificate.domain}`,
|
||||
);
|
||||
|
||||
// if cname is not valid then remove the domain
|
||||
|
|
@ -83,7 +83,7 @@ export default class GreenlockUtil {
|
|||
await data.notifyDomainRemoved(certificate.domain);
|
||||
|
||||
logger.error(
|
||||
`Cname is not valid for domain: ${certificate.domain}`
|
||||
`Cname is not valid for domain: ${certificate.domain}`,
|
||||
);
|
||||
} else {
|
||||
logger.debug(`CNAME is valid for domain: ${certificate.domain}`);
|
||||
|
|
@ -94,12 +94,12 @@ export default class GreenlockUtil {
|
|||
});
|
||||
|
||||
logger.debug(
|
||||
`Certificate renewed for domain: ${certificate.domain}`
|
||||
`Certificate renewed for domain: ${certificate.domain}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Error renewing certificate for domain: ${certificate.domain}`
|
||||
`Error renewing certificate for domain: ${certificate.domain}`,
|
||||
);
|
||||
logger.error(e);
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ export default class GreenlockUtil {
|
|||
}): Promise<void> {
|
||||
try {
|
||||
logger.debug(
|
||||
`GreenlockUtil - Ordering certificate for domain: ${data.domain}`
|
||||
`GreenlockUtil - Ordering certificate for domain: ${data.domain}`,
|
||||
);
|
||||
|
||||
let { domain } = data;
|
||||
|
|
@ -150,13 +150,13 @@ export default class GreenlockUtil {
|
|||
|
||||
if (!acmeAccountKeyInBase64) {
|
||||
throw new ServerException(
|
||||
"No lets encrypt account key found in environment variables. Please add one."
|
||||
"No lets encrypt account key found in environment variables. Please add one.",
|
||||
);
|
||||
}
|
||||
|
||||
let acmeAccountKey: string = Buffer.from(
|
||||
acmeAccountKeyInBase64,
|
||||
"base64"
|
||||
"base64",
|
||||
).toString();
|
||||
|
||||
acmeAccountKey = Text.replaceAll(acmeAccountKey, "\\n", "\n");
|
||||
|
|
@ -197,13 +197,13 @@ export default class GreenlockUtil {
|
|||
challengeCreateFn: async (
|
||||
authz: acme.Authorization,
|
||||
challenge: Challenge,
|
||||
keyAuthorization: string
|
||||
keyAuthorization: string,
|
||||
) => {
|
||||
// Satisfy challenge here
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
logger.debug(
|
||||
`Creating challenge for domain: ${authz.identifier.value}`
|
||||
`Creating challenge for domain: ${authz.identifier.value}`,
|
||||
);
|
||||
|
||||
const acmeChallenge: AcmeChallenge = new AcmeChallenge();
|
||||
|
|
@ -219,18 +219,18 @@ export default class GreenlockUtil {
|
|||
});
|
||||
|
||||
logger.debug(
|
||||
`Challenge created for domain: ${authz.identifier.value}`
|
||||
`Challenge created for domain: ${authz.identifier.value}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
challengeRemoveFn: async (
|
||||
authz: acme.Authorization,
|
||||
challenge: Challenge
|
||||
challenge: Challenge,
|
||||
) => {
|
||||
// Clean up challenge here
|
||||
|
||||
logger.debug(
|
||||
`Removing challenge for domain: ${authz.identifier.value}`
|
||||
`Removing challenge for domain: ${authz.identifier.value}`,
|
||||
);
|
||||
|
||||
if (challenge.type === "http-01") {
|
||||
|
|
@ -247,7 +247,7 @@ export default class GreenlockUtil {
|
|||
}
|
||||
|
||||
logger.debug(
|
||||
`Challenge removed for domain: ${authz.identifier.value}`
|
||||
`Challenge removed for domain: ${authz.identifier.value}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -328,11 +328,11 @@ export default class GreenlockUtil {
|
|||
|
||||
if (IsBillingEnabled) {
|
||||
throw new ServerException(
|
||||
`Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`
|
||||
`Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
|
||||
);
|
||||
} else {
|
||||
throw new ServerException(
|
||||
`Unable to order certificate for ${data.domain}. Please make sure that your server can be accessed publicly over port 80 (HTTP) and port 443 (HTTPS). If the problem persists, please refer to server logs for more information. Please also set up LOG_LEVEL=DEBUG to get more detailed server logs.`
|
||||
`Unable to order certificate for ${data.domain}. Please make sure that your server can be accessed publicly over port 80 (HTTP) and port 443 (HTTPS). If the problem persists, please refer to server logs for more information. Please also set up LOG_LEVEL=DEBUG to get more detailed server logs.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,16 +18,22 @@ const Captcha: React.FC<CaptchaProps> = ({
|
|||
onBlur,
|
||||
className,
|
||||
}: CaptchaProps): JSX.Element => {
|
||||
const captchaRef = React.useRef<HCaptcha | null>(null);
|
||||
const onTokenChangeRef = React.useRef<typeof onTokenChange>(onTokenChange);
|
||||
const captchaRef: React.MutableRefObject<HCaptcha | null> =
|
||||
React.useRef<HCaptcha | null>(null);
|
||||
const onTokenChangeRef: React.MutableRefObject<
|
||||
CaptchaProps["onTokenChange"]
|
||||
> = React.useRef<CaptchaProps["onTokenChange"]>(onTokenChange);
|
||||
|
||||
React.useEffect(() => {
|
||||
onTokenChangeRef.current = onTokenChange;
|
||||
}, [onTokenChange]);
|
||||
|
||||
const handleTokenChange = React.useCallback((token: string | null) => {
|
||||
onTokenChangeRef.current?.(token || "");
|
||||
}, []);
|
||||
const handleTokenChange: (token: string | null) => void = React.useCallback(
|
||||
(token: string | null) => {
|
||||
onTokenChangeRef.current?.(token || "");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
captchaRef.current?.resetCaptcha();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue