mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 21:28:27 +00:00
feat: added base captcha implementation
This commit is contained in:
@@ -63,3 +63,8 @@ CLIENT_SECRET_GITHUB_LOGIN=
|
|||||||
|
|
||||||
CLIENT_ID_GITLAB_LOGIN=
|
CLIENT_ID_GITLAB_LOGIN=
|
||||||
CLIENT_SECRET_GITLAB_LOGIN=
|
CLIENT_SECRET_GITLAB_LOGIN=
|
||||||
|
|
||||||
|
CAPTCHA_SECRET=
|
||||||
|
CAPTCHA_ENABLED=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
|
||||||
|
TableName.Users,
|
||||||
|
"consecutiveFailedPasswordAttempts"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Users, (tb) => {
|
||||||
|
if (!hasConsecutiveFailedPasswordAttempts) {
|
||||||
|
tb.integer("consecutiveFailedPasswordAttempts").defaultTo(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
|
||||||
|
TableName.Users,
|
||||||
|
"consecutiveFailedPasswordAttempts"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Users, (tb) => {
|
||||||
|
if (hasConsecutiveFailedPasswordAttempts) {
|
||||||
|
tb.dropColumn("consecutiveFailedPasswordAttempts");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ export const ProjectsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
version: z.number().default(1),
|
version: z.number().default(1),
|
||||||
upgradeStatus: z.string().nullable().optional()
|
upgradeStatus: z.string().nullable().optional(),
|
||||||
|
pitVersionLimit: z.number().default(10)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export const UsersSchema = z.object({
|
|||||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||||
isLocked: z.boolean().default(false).nullable().optional(),
|
isLocked: z.boolean().default(false).nullable().optional(),
|
||||||
temporaryLockDateEnd: z.date().nullable().optional()
|
temporaryLockDateEnd: z.date().nullable().optional(),
|
||||||
|
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
|||||||
@@ -119,7 +119,11 @@ const envSchema = z
|
|||||||
.transform((val) => val === "true")
|
.transform((val) => val === "true")
|
||||||
.optional(),
|
.optional(),
|
||||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||||
MAINTENANCE_MODE: zodStrBool.default("false")
|
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||||
|
|
||||||
|
// CAPTCHA
|
||||||
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
|
CAPTCHA_ENABLED: zodStrBool.default("false")
|
||||||
})
|
})
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
email: z.string().trim(),
|
email: z.string().trim(),
|
||||||
providerAuthToken: z.string().trim().optional(),
|
providerAuthToken: z.string().trim().optional(),
|
||||||
clientProof: z.string().trim()
|
clientProof: z.string().trim(),
|
||||||
|
captchaToken: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.discriminatedUnion("mfaEnabled", [
|
200: z.discriminatedUnion("mfaEnabled", [
|
||||||
@@ -106,6 +107,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const data = await server.services.login.loginExchangeClientProof({
|
const data = await server.services.login.loginExchangeClientProof({
|
||||||
|
captchaToken: req.body.captchaToken,
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent,
|
userAgent,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -176,12 +177,16 @@ export const authLoginServiceFactory = ({
|
|||||||
clientProof,
|
clientProof,
|
||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
}: TLoginClientProofDTO) => {
|
}: TLoginClientProofDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||||
username: email
|
username: email
|
||||||
});
|
});
|
||||||
if (!userEnc) throw new Error("Failed to find user");
|
if (!userEnc) throw new Error("Failed to find user");
|
||||||
|
const user = await userDAL.findById(userEnc.userId);
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
|
|
||||||
let authMethod = AuthMethod.EMAIL;
|
let authMethod = AuthMethod.EMAIL;
|
||||||
@@ -196,6 +201,30 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.consecutiveFailedPasswordAttempts &&
|
||||||
|
user.consecutiveFailedPasswordAttempts > 10 &&
|
||||||
|
appCfg.CAPTCHA_ENABLED
|
||||||
|
) {
|
||||||
|
if (!captchaToken) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Captcha Required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate captcha token
|
||||||
|
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
|
||||||
|
response: captchaToken,
|
||||||
|
secret: appCfg.CAPTCHA_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Invalid Captcha"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||||
const isValidClientProof = await srpCheckClientProof(
|
const isValidClientProof = await srpCheckClientProof(
|
||||||
userEnc.salt,
|
userEnc.salt,
|
||||||
@@ -204,7 +233,19 @@ export const authLoginServiceFactory = ({
|
|||||||
userEnc.clientPublicKey,
|
userEnc.clientPublicKey,
|
||||||
clientProof
|
clientProof
|
||||||
);
|
);
|
||||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
|
||||||
|
if (!isValidClientProof) {
|
||||||
|
await userDAL.update(
|
||||||
|
{ id: userEnc.id },
|
||||||
|
{
|
||||||
|
$incr: {
|
||||||
|
consecutiveFailedPasswordAttempts: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error("Failed to authenticate. Try again?");
|
||||||
|
}
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
serverPrivateKey: null,
|
serverPrivateKey: null,
|
||||||
@@ -212,7 +253,6 @@ export const authLoginServiceFactory = ({
|
|||||||
});
|
});
|
||||||
// send multi factor auth token if they it enabled
|
// send multi factor auth token if they it enabled
|
||||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
const user = await userDAL.findById(userEnc.userId);
|
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
const mfaToken = jwt.sign(
|
const mfaToken = jwt.sign(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type TLoginClientProofDTO = {
|
|||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
captchaToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyMfaTokenDTO = {
|
export type TVerifyMfaTokenDTO = {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const ContentSecurityPolicy = `
|
const ContentSecurityPolicy = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com 'unsafe-inline' 'unsafe-eval';
|
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
||||||
style-src 'self' https://rsms.me 'unsafe-inline';
|
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
||||||
child-src https://api.stripe.com;
|
child-src https://api.stripe.com;
|
||||||
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
|
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;
|
||||||
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:*;
|
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com;
|
||||||
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
|
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
|
||||||
media-src https://js.intercomcdn.com;
|
media-src https://js.intercomcdn.com;
|
||||||
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
||||||
|
|||||||
Generated
+19
-1
@@ -4,7 +4,6 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@casl/react": "^3.1.0",
|
"@casl/react": "^3.1.0",
|
||||||
@@ -19,6 +18,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@octokit/rest": "^19.0.7",
|
"@octokit/rest": "^19.0.7",
|
||||||
@@ -3200,6 +3200,24 @@
|
|||||||
"react": ">=16.3"
|
"react": ">=16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hcaptcha/loader": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw=="
|
||||||
|
},
|
||||||
|
"node_modules/@hcaptcha/react-hcaptcha": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-P0en4gEZAecah7Pt3WIaJO2gFlaLZKkI0+Tfdg8fNqsDxqT9VytZWSkH4WAkiPRULK1QcGgUZK+J56MXYmPifw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.17.9",
|
||||||
|
"@hcaptcha/loader": "^1.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.3.0",
|
||||||
|
"react-dom": ">= 16.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@headlessui/react": {
|
"node_modules/@headlessui/react": {
|
||||||
"version": "1.7.18",
|
"version": "1.7.18",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@octokit/rest": "^19.0.7",
|
"@octokit/rest": "^19.0.7",
|
||||||
|
|||||||
@@ -30,11 +30,13 @@ export interface IsCliLoginSuccessful {
|
|||||||
const attemptLogin = async ({
|
const attemptLogin = async ({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
captchaToken?: string;
|
||||||
}): Promise<IsCliLoginSuccessful> => {
|
}): Promise<IsCliLoginSuccessful> => {
|
||||||
const telemetry = new Telemetry().getInstance();
|
const telemetry = new Telemetry().getInstance();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -70,7 +72,8 @@ const attemptLogin = async ({
|
|||||||
} = await login2({
|
} = await login2({
|
||||||
email,
|
email,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
if (mfaEnabled) {
|
if (mfaEnabled) {
|
||||||
// case: MFA is enabled
|
// case: MFA is enabled
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ interface IsLoginSuccessful {
|
|||||||
const attemptLogin = async ({
|
const attemptLogin = async ({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
captchaToken?: string;
|
||||||
}): Promise<IsLoginSuccessful> => {
|
}): Promise<IsLoginSuccessful> => {
|
||||||
const telemetry = new Telemetry().getInstance();
|
const telemetry = new Telemetry().getInstance();
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
@@ -58,6 +60,7 @@ const attemptLogin = async ({
|
|||||||
iv,
|
iv,
|
||||||
tag
|
tag
|
||||||
} = await login2({
|
} = await login2({
|
||||||
|
captchaToken,
|
||||||
email,
|
email,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken
|
providerAuthToken
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
|
|||||||
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
|
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
|
||||||
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||||
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
|
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
|
||||||
|
const CAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!;
|
||||||
|
|
||||||
export { ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
|
export { CAPTCHA_SITE_KEY, ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type Login1DTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Login2DTO = {
|
export type Login2DTO = {
|
||||||
|
captchaToken?: string;
|
||||||
email: string;
|
email: string;
|
||||||
clientProof: string;
|
clientProof: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { FormEvent, useEffect, useState } from "react";
|
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error";
|
import Error from "@app/components/basic/Error";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||||
import { Button, Input } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useServerConfig } from "@app/context";
|
import { useServerConfig } from "@app/context";
|
||||||
|
|
||||||
@@ -31,6 +33,9 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
const [loginError, setLoginError] = useState(false);
|
const [loginError, setLoginError] = useState(false);
|
||||||
const { config } = useServerConfig();
|
const { config } = useServerConfig();
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
|
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||||
|
const captchaRef = useRef<HCaptcha>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -61,7 +66,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
// attemptCliLogin
|
// attemptCliLogin
|
||||||
const isCliLoginSuccessful = await attemptCliLogin({
|
const isCliLoginSuccessful = await attemptCliLogin({
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
password
|
password,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||||
@@ -83,7 +89,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
} else {
|
} else {
|
||||||
const isLoginSuccessful = await attemptLogin({
|
const isLoginSuccessful = await attemptLogin({
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
password
|
password,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||||
@@ -117,6 +124,12 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.response.data.error === "Captcha Required") {
|
||||||
|
setShouldShowCaptcha(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoginError(true);
|
setLoginError(true);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||||
@@ -124,6 +137,10 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (captchaRef.current) {
|
||||||
|
captchaRef.current.resetCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,6 +262,16 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
className="select:-webkit-autofill:focus h-10"
|
className="select:-webkit-autofill:focus h-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{shouldShowCaptcha && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<HCaptcha
|
||||||
|
theme="dark"
|
||||||
|
sitekey={CAPTCHA_SITE_KEY}
|
||||||
|
onVerify={(token) => setCaptchaToken(token)}
|
||||||
|
ref={captchaRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||||
import { Button, Input } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||||
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
@@ -41,6 +43,10 @@ export const PasswordStep = ({
|
|||||||
providerAuthToken
|
providerAuthToken
|
||||||
) as any;
|
) as any;
|
||||||
|
|
||||||
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
|
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||||
|
const captchaRef = useRef<HCaptcha>(null);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@@ -51,7 +57,8 @@ export const PasswordStep = ({
|
|||||||
const isCliLoginSuccessful = await attemptCliLogin({
|
const isCliLoginSuccessful = await attemptCliLogin({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||||
@@ -99,7 +106,8 @@ export const PasswordStep = ({
|
|||||||
const loginAttempt = await attemptLogin({
|
const loginAttempt = await attemptLogin({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loginAttempt && loginAttempt.success) {
|
if (loginAttempt && loginAttempt.success) {
|
||||||
@@ -158,11 +166,20 @@ export const PasswordStep = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.response.data.error === "Captcha Required") {
|
||||||
|
setShouldShowCaptcha(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your master password and try again.",
|
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (captchaRef.current) {
|
||||||
|
captchaRef.current.resetCaptcha();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -194,6 +211,16 @@ export const PasswordStep = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{shouldShowCaptcha && (
|
||||||
|
<div className="mx-auto mt-4 flex w-full min-w-[22rem] items-center justify-center lg:w-1/6">
|
||||||
|
<HCaptcha
|
||||||
|
theme="dark"
|
||||||
|
sitekey={CAPTCHA_SITE_KEY}
|
||||||
|
onVerify={(token) => setCaptchaToken(token)}
|
||||||
|
ref={captchaRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
|
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
Reference in New Issue
Block a user