mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: initial implementation for totp authenticator
This commit is contained in:
Generated
+61
@@ -75,6 +75,7 @@
|
|||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@@ -6815,6 +6816,48 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@otplib/core": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/plugin-crypto": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/plugin-thirty-two": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"thirty-two": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/preset-default": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/plugin-crypto": "^12.0.1",
|
||||||
|
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@otplib/preset-v11": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/plugin-crypto": "^12.0.1",
|
||||||
|
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@peculiar/asn1-cms": {
|
"node_modules/@peculiar/asn1-cms": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
|
||||||
@@ -16453,6 +16496,16 @@
|
|||||||
"node": ">=14.6"
|
"node": ">=14.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/otplib": {
|
||||||
|
"version": "12.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
|
||||||
|
"integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@otplib/core": "^12.0.1",
|
||||||
|
"@otplib/preset-default": "^12.0.1",
|
||||||
|
"@otplib/preset-v11": "^12.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-finally": {
|
"node_modules/p-finally": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||||
@@ -19553,6 +19606,14 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thirty-two": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thread-stream": {
|
"node_modules/thread-stream": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz",
|
||||||
|
|||||||
@@ -181,6 +181,7 @@
|
|||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
|||||||
Vendored
+2
@@ -79,6 +79,7 @@ import { TServiceTokenServiceFactory } from "@app/services/service-token/service
|
|||||||
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
import { TUserServiceFactory } from "@app/services/user/user-service";
|
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||||
@@ -193,6 +194,7 @@ declare module "fastify" {
|
|||||||
migration: TExternalMigrationServiceFactory;
|
migration: TExternalMigrationServiceFactory;
|
||||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
projectTemplate: TProjectTemplateServiceFactory;
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
|
totp: TTotpServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
Vendored
+4
@@ -314,6 +314,9 @@ import {
|
|||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
|
TTotpConfigs,
|
||||||
|
TTotpConfigsInsert,
|
||||||
|
TTotpConfigsUpdate,
|
||||||
TTrustedIps,
|
TTrustedIps,
|
||||||
TTrustedIpsInsert,
|
TTrustedIpsInsert,
|
||||||
TTrustedIpsUpdate,
|
TTrustedIpsUpdate,
|
||||||
@@ -826,5 +829,6 @@ declare module "knex/types/tables" {
|
|||||||
TProjectTemplatesInsert,
|
TProjectTemplatesInsert,
|
||||||
TProjectTemplatesUpdate
|
TProjectTemplatesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.TotpConfig))) {
|
||||||
|
await knex.schema.createTable(TableName.TotpConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.boolean("isVerified").defaultTo(false).notNullable();
|
||||||
|
t.binary("encryptedRecoveryCodes").notNullable();
|
||||||
|
t.binary("encryptedSecret").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.unique("userId");
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.TotpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
if (!doesOrgMfaMethodColExist) {
|
||||||
|
t.string("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (!doesUserSelectedMfaMethodColExist) {
|
||||||
|
t.string("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.TotpConfig);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.TotpConfig);
|
||||||
|
|
||||||
|
const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
if (doesOrgMfaMethodColExist) {
|
||||||
|
t.dropColumn("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod");
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (doesUserSelectedMfaMethodColExist) {
|
||||||
|
t.dropColumn("selectedMfaMethod");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -106,6 +106,7 @@ export * from "./secrets-v2";
|
|||||||
export * from "./service-tokens";
|
export * from "./service-tokens";
|
||||||
export * from "./slack-integrations";
|
export * from "./slack-integrations";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
export * from "./user-actions";
|
export * from "./user-actions";
|
||||||
export * from "./user-aliases";
|
export * from "./user-aliases";
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export enum TableName {
|
|||||||
ExternalKms = "external_kms",
|
ExternalKms = "external_kms",
|
||||||
InternalKms = "internal_kms",
|
InternalKms = "internal_kms",
|
||||||
InternalKmsKeyVersion = "internal_kms_key_version",
|
InternalKmsKeyVersion = "internal_kms_key_version",
|
||||||
|
TotpConfig = "totp_configs",
|
||||||
// @depreciated
|
// @depreciated
|
||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||||
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
kmsEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false)
|
enforceMfa: z.boolean().default(false),
|
||||||
|
selectedMfaMethod: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const TotpConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
isVerified: z.boolean().default(false),
|
||||||
|
encryptedRecoveryCodes: zodBuffer,
|
||||||
|
encryptedSecret: zodBuffer,
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TTotpConfigs = z.infer<typeof TotpConfigsSchema>;
|
||||||
|
export type TTotpConfigsInsert = Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TTotpConfigsUpdate = Partial<Omit<z.input<typeof TotpConfigsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -26,7 +26,8 @@ export const UsersSchema = z.object({
|
|||||||
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()
|
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional(),
|
||||||
|
selectedMfaMethod: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admi
|
|||||||
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
||||||
import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue";
|
import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue";
|
||||||
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
|
||||||
|
import { totpServiceFactory } from "@app/services/totp/totp-service";
|
||||||
import { userDALFactory } from "@app/services/user/user-dal";
|
import { userDALFactory } from "@app/services/user/user-dal";
|
||||||
import { userServiceFactory } from "@app/services/user/user-service";
|
import { userServiceFactory } from "@app/services/user/user-service";
|
||||||
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
@@ -348,6 +350,7 @@ export const registerRoutes = async (
|
|||||||
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
||||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||||
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
||||||
|
const totpConfigDAL = totpConfigDALFactory(db);
|
||||||
|
|
||||||
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
||||||
|
|
||||||
@@ -511,7 +514,13 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL
|
projectMembershipDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL });
|
const totpService = totpServiceFactory({
|
||||||
|
totpConfigDAL,
|
||||||
|
userDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
@@ -1369,7 +1378,8 @@ export const registerRoutes = async (
|
|||||||
workflowIntegration: workflowIntegrationService,
|
workflowIntegration: workflowIntegrationService,
|
||||||
migration: migrationService,
|
migration: migrationService,
|
||||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||||
projectTemplate: projectTemplateService
|
projectTemplate: projectTemplateService,
|
||||||
|
totp: totpService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
|||||||
@@ -169,4 +169,89 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
return groupMemberships;
|
return groupMemberships;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/me/totp",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
isVerified: z.boolean(),
|
||||||
|
recoveryCodes: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.getUserTotpConfig({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/me/totp",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.deleteUserTotpConfig({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/totp/register",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
otpUrl: z.string(),
|
||||||
|
recoveryCodes: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], {
|
||||||
|
requireOrg: false
|
||||||
|
}),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.registerUserTotp({
|
||||||
|
userId: req.permission.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/totp/verify",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
totp: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], {
|
||||||
|
requireOrg: false
|
||||||
|
}),
|
||||||
|
handler: async (req) => {
|
||||||
|
return server.services.totp.verifyUserTotpConfig({
|
||||||
|
userId: req.permission.id,
|
||||||
|
totp: req.body.totp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
@@ -49,6 +50,38 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/mfa/check/totp",
|
||||||
|
config: {
|
||||||
|
rateLimit: mfaRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
isVerified: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
try {
|
||||||
|
const totpConfig = await server.services.totp.getUserTotpConfig({
|
||||||
|
userId: req.mfa.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVerified: Boolean(totpConfig)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
return { isVerified: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/mfa/verify",
|
url: "/mfa/verify",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -57,7 +90,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
mfaToken: z.string().trim()
|
mfaToken: z.string().trim(),
|
||||||
|
mfaMethod: z.nativeEnum(MfaMethod).optional().default(MfaMethod.EMAIL)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -86,7 +120,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userId: req.mfa.userId,
|
userId: req.mfa.userId,
|
||||||
orgId: req.mfa.orgId,
|
orgId: req.mfa.orgId,
|
||||||
mfaToken: req.body.mfaToken
|
mfaToken: req.body.mfaToken,
|
||||||
|
mfaMethod: req.body.mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
void res.setCookie("jid", token.refresh, {
|
void res.setCookie("jid", token.refresh, {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema,
|
|||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -56,7 +56,8 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
isMfaEnabled: z.boolean()
|
isMfaEnabled: z.boolean().optional(),
|
||||||
|
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -66,7 +67,12 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.user.toggleUserMfa(req.permission.id, req.body.isMfaEnabled);
|
const user = await server.services.user.updateUserMfa({
|
||||||
|
userId: req.permission.id,
|
||||||
|
isMfaEnabled: req.body.isMfaEnabled,
|
||||||
|
selectedMfaMethod: req.body.selectedMfaMethod
|
||||||
|
});
|
||||||
|
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
isMfaEnabled: z.boolean()
|
isMfaEnabled: z.boolean(),
|
||||||
|
mfaMethod: z.string().optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,7 +65,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (tokens.isMfaEnabled) {
|
if (tokens.isMfaEnabled) {
|
||||||
return {
|
return {
|
||||||
token: tokens.mfa as string,
|
token: tokens.mfa as string,
|
||||||
isMfaEnabled: true
|
isMfaEnabled: true,
|
||||||
|
mfaMethod: tokens.mfaMethod
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { TokenType } from "../auth-token/auth-token-types";
|
|||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { LoginMethod } from "../super-admin/super-admin-types";
|
import { LoginMethod } from "../super-admin/super-admin-types";
|
||||||
|
import { TTotpServiceFactory } from "../totp/totp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
||||||
import {
|
import {
|
||||||
@@ -26,13 +27,14 @@ import {
|
|||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
TVerifyMfaTokenDTO
|
TVerifyMfaTokenDTO
|
||||||
} from "./auth-login-type";
|
} from "./auth-login-type";
|
||||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type";
|
||||||
|
|
||||||
type TAuthLoginServiceFactoryDep = {
|
type TAuthLoginServiceFactoryDep = {
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
@@ -40,7 +42,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userDAL,
|
userDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
totpService
|
||||||
}: TAuthLoginServiceFactoryDep) => {
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Private
|
* Private
|
||||||
@@ -100,7 +103,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
organizationId,
|
organizationId,
|
||||||
authMethod,
|
authMethod,
|
||||||
isMfaVerified
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
}: {
|
}: {
|
||||||
user: TUsers;
|
user: TUsers;
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -108,6 +112,7 @@ export const authLoginServiceFactory = ({
|
|||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
}) => {
|
}) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
await updateUserDeviceSession(user, ip, userAgent);
|
await updateUserDeviceSession(user, ip, userAgent);
|
||||||
@@ -126,7 +131,8 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
accessVersion: tokenSession.accessVersion,
|
accessVersion: tokenSession.accessVersion,
|
||||||
organizationId,
|
organizationId,
|
||||||
isMfaVerified
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||||
@@ -140,7 +146,8 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenVersionId: tokenSession.id,
|
tokenVersionId: tokenSession.id,
|
||||||
refreshVersion: tokenSession.refreshVersion,
|
refreshVersion: tokenSession.refreshVersion,
|
||||||
organizationId,
|
organizationId,
|
||||||
isMfaVerified
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
||||||
@@ -353,8 +360,12 @@ export const authLoginServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// send multi factor auth token if they it enabled
|
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
if ((selectedOrg.enforceMfa || user.isMfaEnabled) && user.email && !decodedToken.isMfaVerified) {
|
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod : undefined;
|
||||||
|
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod : undefined;
|
||||||
|
const mfaMethod = orgMfaMethod ?? userMfaMethod ?? MfaMethod.EMAIL;
|
||||||
|
|
||||||
|
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
const mfaToken = jwt.sign(
|
const mfaToken = jwt.sign(
|
||||||
@@ -369,12 +380,14 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendUserMfaCode({
|
if (mfaMethod === MfaMethod.EMAIL && user.email) {
|
||||||
userId: user.id,
|
await sendUserMfaCode({
|
||||||
email: user.email
|
userId: user.id,
|
||||||
});
|
email: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { isMfaEnabled: true, mfa: mfaToken } as const;
|
return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await generateUserTokens({
|
const tokens = await generateUserTokens({
|
||||||
@@ -458,17 +471,39 @@ export const authLoginServiceFactory = ({
|
|||||||
* Multi factor authentication verification of code
|
* Multi factor authentication verification of code
|
||||||
* Third step of login in which user completes with mfa
|
* Third step of login in which user completes with mfa
|
||||||
* */
|
* */
|
||||||
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
const verifyMfaToken = async ({
|
||||||
|
userId,
|
||||||
|
mfaToken,
|
||||||
|
mfaMethod,
|
||||||
|
mfaJwtToken,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
orgId
|
||||||
|
}: TVerifyMfaTokenDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tokenService.validateTokenForUser({
|
if (mfaMethod === MfaMethod.EMAIL) {
|
||||||
type: TokenType.TOKEN_EMAIL_MFA,
|
await tokenService.validateTokenForUser({
|
||||||
userId,
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
code: mfaToken
|
userId,
|
||||||
});
|
code: mfaToken
|
||||||
|
});
|
||||||
|
} else if (mfaMethod === MfaMethod.TOTP) {
|
||||||
|
if (mfaToken.length === 6) {
|
||||||
|
await totpService.verifyUserTotp({
|
||||||
|
userId,
|
||||||
|
totp: mfaToken
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await totpService.verifyWithUserRecoveryCode({
|
||||||
|
userId,
|
||||||
|
recoveryCode: mfaToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const updatedUser = await processFailedMfaAttempt(userId);
|
const updatedUser = await processFailedMfaAttempt(userId);
|
||||||
if (updatedUser.isLocked) {
|
if (updatedUser.isLocked) {
|
||||||
@@ -513,7 +548,8 @@ export const authLoginServiceFactory = ({
|
|||||||
userAgent,
|
userAgent,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
authMethod: decodedToken.authMethod,
|
authMethod: decodedToken.authMethod,
|
||||||
isMfaVerified: true
|
isMfaVerified: true,
|
||||||
|
mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return { token, user: userEnc };
|
return { token, user: userEnc };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AuthMethod } from "./auth-type";
|
import { AuthMethod, MfaMethod } from "./auth-type";
|
||||||
|
|
||||||
export type TLoginGenServerPublicKeyDTO = {
|
export type TLoginGenServerPublicKeyDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -19,6 +19,7 @@ export type TLoginClientProofDTO = {
|
|||||||
export type TVerifyMfaTokenDTO = {
|
export type TVerifyMfaTokenDTO = {
|
||||||
userId: string;
|
userId: string;
|
||||||
mfaToken: string;
|
mfaToken: string;
|
||||||
|
mfaMethod: MfaMethod;
|
||||||
mfaJwtToken: string;
|
mfaJwtToken: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type AuthModeJwtTokenPayload = {
|
|||||||
accessVersion: number;
|
accessVersion: number;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthModeMfaJwtTokenPayload = {
|
export type AuthModeMfaJwtTokenPayload = {
|
||||||
@@ -85,3 +86,8 @@ export type AuthModeProviderSignUpTokenPayload = {
|
|||||||
authTokenType: AuthTokenType.SIGNUP_TOKEN;
|
authTokenType: AuthTokenType.SIGNUP_TOKEN;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum MfaMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
TOTP = "totp"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TTotpConfigDALFactory = ReturnType<typeof totpConfigDALFactory>;
|
||||||
|
|
||||||
|
export const totpConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const totpConfigDal = ormify(db, TableName.TotpConfig);
|
||||||
|
|
||||||
|
return totpConfigDal;
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { authenticator } from "otplib";
|
||||||
|
|
||||||
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TTotpConfigDALFactory } from "./totp-config-dal";
|
||||||
|
import {
|
||||||
|
TDeleteUserTotpConfigDTO,
|
||||||
|
TGetUserTotpConfigDTO,
|
||||||
|
TRegisterUserTotpDTO,
|
||||||
|
TVerifyUserTotpConfigDTO,
|
||||||
|
TVerifyUserTotpDTO,
|
||||||
|
TVerifyWithUserRecoveryCodeDTO
|
||||||
|
} from "./totp-types";
|
||||||
|
|
||||||
|
type TTotpServiceFactoryDep = {
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
|
totpConfigDAL: TTotpConfigDALFactory;
|
||||||
|
kmsService: TKmsServiceFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTotpServiceFactory = ReturnType<typeof totpServiceFactory>;
|
||||||
|
|
||||||
|
export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotpServiceFactoryDep) => {
|
||||||
|
const getUserTotpConfig = async ({ userId }: TGetUserTotpConfigDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVerified: totpConfig.isVerified,
|
||||||
|
recoveryCodes
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerUserTotp = async ({ userId }: TRegisterUserTotpDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.transaction(async (tx) => {
|
||||||
|
const verifiedTotpConfig = await totpConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verifiedTotpConfig) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TOTP configuration for user already exists"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unverifiedTotpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unverifiedTotpConfig) {
|
||||||
|
return unverifiedTotpConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
// create new TOTP configuration
|
||||||
|
const secret = authenticator.generateSecret();
|
||||||
|
const encryptedSecret = encryptWithRoot(Buffer.from(secret));
|
||||||
|
const recoveryCodes = Array.from({ length: 10 }).map(() => String(crypto.randomInt(10 ** 7, 10 ** 8 - 1)));
|
||||||
|
const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(recoveryCodes.join(",")));
|
||||||
|
const newTotpConfig = await totpConfigDAL.create({
|
||||||
|
userId,
|
||||||
|
encryptedRecoveryCodes,
|
||||||
|
encryptedSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
return newTotpConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
const otpUrl = authenticator.keyuri(user.username, "Infisical", secret);
|
||||||
|
|
||||||
|
return {
|
||||||
|
otpUrl,
|
||||||
|
recoveryCodes
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyUserTotpConfig = async ({ userId, totp }: TVerifyUserTotpConfigDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: totp,
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
await totpConfigDAL.updateById(totpConfig.id, {
|
||||||
|
isVerified: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid TOTP token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyUserTotp = async ({ userId, totp }: TVerifyUserTotpDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const secret = decryptWithRoot(totpConfig.encryptedSecret).toString();
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: totp,
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Invalid TOTP"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyWithUserRecoveryCode = async ({ userId, recoveryCode }: TVerifyWithUserRecoveryCodeDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(",");
|
||||||
|
const matchingCode = recoveryCodes.find((code) => recoveryCode === code);
|
||||||
|
if (!matchingCode) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Invalid TOTP recovery code"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRecoveryCodes = recoveryCodes.filter((code) => code !== matchingCode);
|
||||||
|
const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(updatedRecoveryCodes.join(",")));
|
||||||
|
await totpConfigDAL.updateById(totpConfig.id, {
|
||||||
|
encryptedRecoveryCodes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUserTotpConfig = async ({ userId }: TDeleteUserTotpConfigDTO) => {
|
||||||
|
const totpConfig = await totpConfigDAL.findOne({
|
||||||
|
userId,
|
||||||
|
isVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totpConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "TOTP configuration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await totpConfigDAL.deleteById(totpConfig.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
registerUserTotp,
|
||||||
|
verifyUserTotpConfig,
|
||||||
|
getUserTotpConfig,
|
||||||
|
verifyUserTotp,
|
||||||
|
verifyWithUserRecoveryCode,
|
||||||
|
deleteUserTotpConfig
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export type TRegisterUserTotpDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyUserTotpConfigDTO = {
|
||||||
|
userId: string;
|
||||||
|
totp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetUserTotpConfigDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyUserTotpDTO = {
|
||||||
|
userId: string;
|
||||||
|
totp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyWithUserRecoveryCodeDTO = {
|
||||||
|
userId: string;
|
||||||
|
recoveryCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteUserTotpConfigDTO = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ import { AuthMethod } from "../auth/auth-type";
|
|||||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
import { TListUserGroupsDTO } from "./user-types";
|
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
|
||||||
|
|
||||||
type TUserServiceFactoryDep = {
|
type TUserServiceFactoryDep = {
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
@@ -171,15 +171,24 @@ export const userServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
const updateUserMfa = async ({ userId, isMfaEnabled, selectedMfaMethod }: TUpdateUserMfaDTO) => {
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
|
|
||||||
if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" });
|
if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" });
|
||||||
|
|
||||||
|
let mfaMethods;
|
||||||
|
if (isMfaEnabled === undefined) {
|
||||||
|
mfaMethods = undefined;
|
||||||
|
} else {
|
||||||
|
mfaMethods = isMfaEnabled ? ["email"] : [];
|
||||||
|
}
|
||||||
|
|
||||||
const updatedUser = await userDAL.updateById(userId, {
|
const updatedUser = await userDAL.updateById(userId, {
|
||||||
isMfaEnabled,
|
isMfaEnabled,
|
||||||
mfaMethods: isMfaEnabled ? ["email"] : []
|
mfaMethods,
|
||||||
|
selectedMfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -327,7 +336,7 @@ export const userServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
toggleUserMfa,
|
updateUserMfa,
|
||||||
updateUserName,
|
updateUserName,
|
||||||
updateAuthMethods,
|
updateAuthMethods,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { MfaMethod } from "../auth/auth-type";
|
||||||
|
|
||||||
export type TListUserGroupsDTO = {
|
export type TListUserGroupsDTO = {
|
||||||
username: string;
|
username: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
@@ -8,3 +10,9 @@ export enum UserEncryption {
|
|||||||
V1 = 1,
|
V1 = 1,
|
||||||
V2 = 2
|
V2 = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TUpdateUserMfaDTO = {
|
||||||
|
userId: string;
|
||||||
|
isMfaEnabled?: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
|
};
|
||||||
|
|||||||
Generated
+189
-10
@@ -75,6 +75,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"picomatch": "^2.3.1",
|
"picomatch": "^2.3.1",
|
||||||
"posthog-js": "^1.105.6",
|
"posthog-js": "^1.105.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@@ -120,6 +121,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@types/picomatch": "^2.3.0",
|
"@types/picomatch": "^2.3.0",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
@@ -8857,6 +8859,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.11",
|
"version": "6.9.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
|
||||||
@@ -9785,7 +9796,6 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -9794,7 +9804,6 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -11076,7 +11085,6 @@
|
|||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -11376,6 +11384,29 @@
|
|||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clone": {
|
"node_modules/clone": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||||
@@ -12281,6 +12312,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decode-named-character-reference": {
|
"node_modules/decode-named-character-reference": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
||||||
@@ -12677,6 +12716,11 @@
|
|||||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||||
|
},
|
||||||
"node_modules/dir-glob": {
|
"node_modules/dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
@@ -14943,6 +14987,14 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||||
@@ -16212,7 +16264,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -19339,7 +19390,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -19445,7 +19495,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -19666,6 +19715,14 @@
|
|||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pnp-webpack-plugin": {
|
"node_modules/pnp-webpack-plugin": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz",
|
||||||
@@ -20550,6 +20607,22 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
@@ -21846,6 +21919,14 @@
|
|||||||
"throttleit": "^1.0.0"
|
"throttleit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-from-string": {
|
"node_modules/require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
@@ -21855,6 +21936,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||||
|
},
|
||||||
"node_modules/requireindex": {
|
"node_modules/requireindex": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
|
||||||
@@ -22314,6 +22400,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
|
||||||
@@ -22900,7 +22991,6 @@
|
|||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
@@ -22934,8 +23024,7 @@
|
|||||||
"node_modules/string-width/node_modules/emoji-regex": {
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
@@ -23006,7 +23095,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -24902,6 +24990,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||||
|
},
|
||||||
"node_modules/which-typed-array": {
|
"node_modules/which-typed-array": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
|
||||||
@@ -25066,6 +25159,11 @@
|
|||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
@@ -25079,6 +25177,87 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs/node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yauzl": {
|
"node_modules/yauzl": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"picomatch": "^2.3.1",
|
"picomatch": "^2.3.1",
|
||||||
"posthog-js": "^1.105.6",
|
"posthog-js": "^1.105.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@@ -133,6 +134,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@types/picomatch": "^2.3.0",
|
"@types/picomatch": "^2.3.0",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import { useGetUserTotpRegistration } from "@app/hooks/api";
|
||||||
|
import { useVerifyUserTotpRegistration } from "@app/hooks/api/users/mutation";
|
||||||
|
|
||||||
|
import { createNotification } from "../notifications";
|
||||||
|
import { Button, ContentLoader, Input } from "../v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onComplete?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TotpRegistration = ({ onComplete }: Props) => {
|
||||||
|
const { data: registration, isLoading } = useGetUserTotpRegistration();
|
||||||
|
const { mutateAsync: verifyUserTotp } = useVerifyUserTotpRegistration();
|
||||||
|
const [qrCodeUrl, setQrCodeUrl] = useState("");
|
||||||
|
const [totp, setTotp] = useState("");
|
||||||
|
|
||||||
|
const handleTotpVerify = async () => {
|
||||||
|
await verifyUserTotp({
|
||||||
|
totp
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully configured mobile authenticator",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generateQRCode = async () => {
|
||||||
|
if (registration?.otpUrl) {
|
||||||
|
const url = await QRCode.toDataURL(registration.otpUrl);
|
||||||
|
setQrCodeUrl(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
generateQRCode();
|
||||||
|
}, [registration]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <ContentLoader />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-sm flex-col text-bunker-200">
|
||||||
|
<div className="mb-4 text-center">
|
||||||
|
Download a two-step verification app (Duo, Google Authenticator, etc.) and scan the QR code.
|
||||||
|
</div>
|
||||||
|
<div className="mb-10 flex items-center justify-center">
|
||||||
|
<img src={qrCodeUrl} alt="registration-qr" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 text-center">Enter the resulting verification code</div>
|
||||||
|
<div className="mb-4 flex flex-row gap-2">
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setTotp(e.target.value)}
|
||||||
|
value={totp}
|
||||||
|
placeholder="Verification code"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleTotpVerify}>Enable MFA</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TotpRegistration;
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Login2Res,
|
Login2Res,
|
||||||
LoginLDAPDTO,
|
LoginLDAPDTO,
|
||||||
LoginLDAPRes,
|
LoginLDAPRes,
|
||||||
|
MfaMethod,
|
||||||
ResetPasswordDTO,
|
ResetPasswordDTO,
|
||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
SRP1DTO,
|
SRP1DTO,
|
||||||
@@ -65,10 +66,11 @@ export const selectOrganization = async (data: {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
userAgent?: UserAgentType;
|
userAgent?: UserAgentType;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: res } = await apiRequest.post<{ token: string; isMfaEnabled: boolean }>(
|
const { data: res } = await apiRequest.post<{
|
||||||
"/api/v3/auth/select-organization",
|
token: string;
|
||||||
data
|
isMfaEnabled: boolean;
|
||||||
);
|
mfaMethod?: MfaMethod;
|
||||||
|
}>("/api/v3/auth/select-organization", data);
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,10 +156,19 @@ export const useSendMfaToken = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => {
|
export const verifyMfaToken = async ({
|
||||||
|
email,
|
||||||
|
mfaCode,
|
||||||
|
mfaMethod
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
mfaCode: string;
|
||||||
|
mfaMethod: string;
|
||||||
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
||||||
email,
|
email,
|
||||||
mfaToken: mfaCode
|
mfaToken: mfaCode,
|
||||||
|
mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -165,10 +176,11 @@ export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCod
|
|||||||
|
|
||||||
export const useVerifyMfaToken = () => {
|
export const useVerifyMfaToken = () => {
|
||||||
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
|
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
|
||||||
mutationFn: async ({ email, mfaCode }) => {
|
mutationFn: async ({ email, mfaCode, mfaMethod }) => {
|
||||||
return verifyMfaToken({
|
return verifyMfaToken({
|
||||||
email,
|
email,
|
||||||
mfaCode
|
mfaCode,
|
||||||
|
mfaMethod
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -302,3 +314,9 @@ export const useGetAuthToken = () =>
|
|||||||
onSuccess: (data) => setAuthToken(data.token),
|
onSuccess: (data) => setAuthToken(data.token),
|
||||||
retry: 0
|
retry: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const checkUserTotpMfa = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ isVerified: boolean }>("/api/v2/auth/mfa/check/totp");
|
||||||
|
|
||||||
|
return data.isVerified;
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type SendMfaTokenDTO = {
|
|||||||
export type VerifyMfaTokenDTO = {
|
export type VerifyMfaTokenDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
mfaCode: string;
|
mfaCode: string;
|
||||||
|
mfaMethod: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VerifyMfaTokenRes = {
|
export type VerifyMfaTokenRes = {
|
||||||
@@ -149,3 +150,8 @@ export type GetBackupEncryptedPrivateKeyDTO = {
|
|||||||
export enum UserAgentType {
|
export enum UserAgentType {
|
||||||
CLI = "cli"
|
CLI = "cli"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MfaMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
TOTP = "totp"
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ export {
|
|||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
|
useGetUserTotpRegistration,
|
||||||
useListUserGroupMemberships,
|
useListUserGroupMemberships,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
useUpdateMfaEnabled,
|
|
||||||
useUpdateOrgMembership,
|
useUpdateOrgMembership,
|
||||||
useUpdateUserAuthMethods
|
useUpdateUserAuthMethods,
|
||||||
|
useUpdateUserMfa
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
export { userKeys } from "./query-keys";
|
export { userKeys } from "./query-keys";
|
||||||
|
|||||||
@@ -114,3 +114,29 @@ export const useUpdateUserProjectFavorites = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useVerifyUserTotpRegistration = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ totp }: { totp: string }) => {
|
||||||
|
await apiRequest.post("/api/v1/user/me/totp/verify", {
|
||||||
|
totp
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteUserTotpConfiguration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await apiRequest.delete("/api/v1/user/me/totp");
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { SessionStorageKeys } from "@app/const";
|
import { SessionStorageKeys } from "@app/const";
|
||||||
import { setAuthToken } from "@app/reactQuery";
|
import { setAuthToken } from "@app/reactQuery";
|
||||||
|
|
||||||
import { APIKeyDataV2 } from "../apiKeys/types";
|
import { APIKeyDataV2 } from "../apiKeys/types";
|
||||||
|
import { MfaMethod } from "../auth/types";
|
||||||
import { TGroupWithProjectMemberships } from "../groups/types";
|
import { TGroupWithProjectMemberships } from "../groups/types";
|
||||||
import { workspaceKeys } from "../workspace";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { userKeys } from "./query-keys";
|
import { userKeys } from "./query-keys";
|
||||||
@@ -390,14 +392,21 @@ export const useRevokeMySessions = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateMfaEnabled = () => {
|
export const useUpdateUserMfa = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ isMfaEnabled }: { isMfaEnabled: boolean }) => {
|
mutationFn: async ({
|
||||||
|
isMfaEnabled,
|
||||||
|
selectedMfaMethod
|
||||||
|
}: {
|
||||||
|
isMfaEnabled?: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
data: { user }
|
data: { user }
|
||||||
} = await apiRequest.patch("/api/v2/users/me/mfa", {
|
} = await apiRequest.patch("/api/v2/users/me/mfa", {
|
||||||
isMfaEnabled
|
isMfaEnabled,
|
||||||
|
selectedMfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@@ -446,3 +455,39 @@ export const useListUserGroupMemberships = (username: string) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetUserTotpRegistration = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.totpRegistration,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.post<{ otpUrl: string; recoveryCodes: string[] }>(
|
||||||
|
"/api/v1/user/me/totp/register"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetUserTotpConfiguration = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.totpConfiguration,
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiRequest.get<{ isVerified: boolean; recoveryCodes: string[] }>(
|
||||||
|
"/api/v1/user/me/totp"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && error.response?.data?.statusCode === 404) {
|
||||||
|
return {
|
||||||
|
isVerified: false,
|
||||||
|
recoveryCodes: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const userKeys = {
|
|||||||
myAPIKeysV2: ["api-keys-v2"] as const,
|
myAPIKeysV2: ["api-keys-v2"] as const,
|
||||||
mySessions: ["sessions"] as const,
|
mySessions: ["sessions"] as const,
|
||||||
listUsers: ["user-list"] as const,
|
listUsers: ["user-list"] as const,
|
||||||
|
totpRegistration: ["totp-registration"],
|
||||||
|
totpConfiguration: ["totp-configuration"],
|
||||||
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
|
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
|
||||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MfaMethod } from "../auth/types";
|
||||||
import { UserWsKeyPair } from "../keys/types";
|
import { UserWsKeyPair } from "../keys/types";
|
||||||
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export type User = {
|
|||||||
authProvider?: AuthMethod;
|
authProvider?: AuthMethod;
|
||||||
authMethods: AuthMethod[];
|
authMethods: AuthMethod[];
|
||||||
isMfaEnabled: boolean;
|
isMfaEnabled: boolean;
|
||||||
|
selectedMfaMethod?: MfaMethod;
|
||||||
seenIps: string[];
|
seenIps: string[];
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
import { MfaMethod, UserAgentType } from "@app/hooks/api/auth/types";
|
||||||
import { Organization } from "@app/hooks/api/types";
|
import { Organization } from "@app/hooks/api/types";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
||||||
@@ -46,6 +46,7 @@ export default function LoginPage() {
|
|||||||
const selectOrg = useSelectOrganization();
|
const selectOrg = useSelectOrganization();
|
||||||
const { data: user, isLoading: userLoading } = useGetUser();
|
const { data: user, isLoading: userLoading } = useGetUser();
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true);
|
const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true);
|
||||||
|
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
@@ -90,15 +91,17 @@ export default function LoginPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, isMfaEnabled } = await selectOrg.mutateAsync({
|
const { token, isMfaEnabled, mfaMethod } = await selectOrg.mutateAsync({
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
|
if (mfaMethod) {
|
||||||
|
setRequiredMfaMethod(mfaMethod);
|
||||||
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
|
|
||||||
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
|
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -213,7 +216,11 @@ export default function LoginPage() {
|
|||||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||||
</Head>
|
</Head>
|
||||||
{shouldShowMfa ? (
|
{shouldShowMfa ? (
|
||||||
<Mfa email={user.email as string} successCallback={mfaSuccessCallback} />
|
<Mfa
|
||||||
|
email={user.email as string}
|
||||||
|
successCallback={mfaSuccessCallback}
|
||||||
|
method={requiredMfaMethod}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-auto mt-20 w-fit rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
<div className="mx-auto mt-20 w-fit rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactCodeInput from "react-code-input";
|
import ReactCodeInput from "react-code-input";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -6,10 +6,12 @@ import { useRouter } from "next/router";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error";
|
import Error from "@app/components/basic/Error";
|
||||||
|
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useSendMfaToken } from "@app/hooks/api";
|
import { useSendMfaToken } from "@app/hooks/api";
|
||||||
import { verifyMfaToken } from "@app/hooks/api/auth/queries";
|
import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
// The style for the verification code input
|
// The style for the verification code input
|
||||||
const codeInputProps = {
|
const codeInputProps = {
|
||||||
@@ -36,23 +38,37 @@ type Props = {
|
|||||||
closeMfa?: () => void;
|
closeMfa?: () => void;
|
||||||
hideLogo?: boolean;
|
hideLogo?: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
|
method: MfaMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => {
|
||||||
const [mfaCode, setMfaCode] = useState("");
|
const [mfaCode, setMfaCode] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||||
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
|
||||||
|
const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false);
|
||||||
|
|
||||||
const sendMfaToken = useSendMfaToken();
|
const sendMfaToken = useSendMfaToken();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (method === MfaMethod.TOTP) {
|
||||||
|
checkUserTotpMfa().then((isVerified) => {
|
||||||
|
if (!isVerified) {
|
||||||
|
SecurityClient.setMfaToken("");
|
||||||
|
setShouldShowTotpRegistration(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const verifyMfa = async () => {
|
const verifyMfa = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const { token } = await verifyMfaToken({
|
const { token } = await verifyMfaToken({
|
||||||
email,
|
email,
|
||||||
mfaCode
|
mfaCode,
|
||||||
|
mfaMethod: method
|
||||||
});
|
});
|
||||||
|
|
||||||
SecurityClient.setMfaToken("");
|
SecurityClient.setMfaToken("");
|
||||||
@@ -92,6 +108,23 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (shouldShowTotpRegistration) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||||
|
<TotpRegistration
|
||||||
|
onComplete={async () => {
|
||||||
|
setShouldShowTotpRegistration(false);
|
||||||
|
|
||||||
|
await successCallback();
|
||||||
|
if (closeMfa) {
|
||||||
|
closeMfa();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
|
||||||
{!hideLogo && (
|
{!hideLogo && (
|
||||||
@@ -101,18 +134,40 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
{method === MfaMethod.EMAIL && (
|
||||||
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
<>
|
||||||
|
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
|
||||||
|
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{method === MfaMethod.TOTP && (
|
||||||
|
<>
|
||||||
|
<p className="text-l mb-4 flex max-w-xs justify-center text-center font-bold text-bunker-100">
|
||||||
|
Authenticator MFA Required
|
||||||
|
</p>
|
||||||
|
<p className="text-l flex max-w-xs justify-center text-center text-bunker-300">
|
||||||
|
Open the authenticator app on your mobile device to get your verification code or enter
|
||||||
|
a recovery code.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||||
<ReactCodeInput
|
{method === MfaMethod.EMAIL && (
|
||||||
name=""
|
<ReactCodeInput
|
||||||
inputMode="tel"
|
name=""
|
||||||
type="text"
|
inputMode="tel"
|
||||||
fields={6}
|
type="text"
|
||||||
onChange={setMfaCode}
|
fields={6}
|
||||||
className="mt-6 mb-2"
|
onChange={setMfaCode}
|
||||||
{...codeInputProps}
|
className="mt-6 mb-2"
|
||||||
/>
|
{...codeInputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{method === MfaMethod.TOTP && (
|
||||||
|
<div className="mt-6 mb-4">
|
||||||
|
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{typeof triesLeft === "number" && (
|
{typeof triesLeft === "number" && (
|
||||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||||
@@ -132,21 +187,23 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
{method === MfaMethod.EMAIL && (
|
||||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||||
{isLoadingResend
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
? t("signup.step2-resend-progress")
|
{isLoadingResend
|
||||||
: t("signup.step2-resend-submit")}
|
? t("signup.step2-resend-progress")
|
||||||
</span>
|
: t("signup.step2-resend-submit")}
|
||||||
</button>
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,84 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Checkbox, EmailServiceSetupModal } from "@app/components/v2";
|
import {
|
||||||
import { useGetUser, useUpdateMfaEnabled } from "@app/hooks/api";
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
ContentLoader,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmailServiceSetupModal,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
||||||
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
|
import { useDeleteUserTotpConfiguration } from "@app/hooks/api/users/mutation";
|
||||||
|
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
export const MFASection = () => {
|
export const MFASection = () => {
|
||||||
const { data: user } = useGetUser();
|
const { data: user } = useGetUser();
|
||||||
const { mutateAsync } = useUpdateMfaEnabled();
|
const { mutateAsync } = useUpdateUserMfa();
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
|
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"setUpEmail",
|
||||||
|
"deleteTotpConfig"
|
||||||
|
] as const);
|
||||||
|
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
|
||||||
|
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
|
||||||
|
useGetUserTotpConfiguration();
|
||||||
|
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
|
|
||||||
|
const handleTotpDeletion = async () => {
|
||||||
|
try {
|
||||||
|
await deleteTotpConfiguration();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted mobile authenticator",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteTotpConfig");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to delete mobile authenticator";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
||||||
|
try {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
selectedMfaMethod: mfaMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated preferred 2FA method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: "Something went wrong while updating preferred 2FA method.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMfa = async (state: boolean) => {
|
const toggleMfa = async (state: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -47,31 +113,86 @@ export const MFASection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form>
|
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<p className="mb-8 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||||
<p className="mb-8 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
{user && (
|
||||||
{user && (
|
<Checkbox
|
||||||
<Checkbox
|
className="data-[state=checked]:bg-primary"
|
||||||
className="data-[state=checked]:bg-primary"
|
id="isTwoFAEnabled"
|
||||||
id="isTwoFAEnabled"
|
isChecked={user?.isMfaEnabled}
|
||||||
isChecked={user?.isMfaEnabled}
|
onCheckedChange={(state) => {
|
||||||
onCheckedChange={(state) => {
|
if (serverDetails?.emailConfigured) {
|
||||||
if (serverDetails?.emailConfigured) {
|
toggleMfa(state as boolean);
|
||||||
toggleMfa(state as boolean);
|
} else {
|
||||||
} else {
|
handlePopUpOpen("setUpEmail");
|
||||||
handlePopUpOpen("setUpEmail");
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
|
Enable 2-factor authentication
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
{user?.isMfaEnabled && (
|
||||||
|
<FormControl label="Preferred 2FA method" className="mt-3">
|
||||||
|
<Select
|
||||||
|
className="min-w-[20rem] border border-mineshaft-500"
|
||||||
|
onValueChange={updateSelectedMfa}
|
||||||
|
defaultValue={user.selectedMfaMethod ?? MfaMethod.EMAIL}
|
||||||
>
|
>
|
||||||
Enable 2-factor authentication via your personal email.
|
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
||||||
</Checkbox>
|
Email
|
||||||
)}
|
</SelectItem>
|
||||||
</div>
|
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
||||||
</form>
|
Mobile Authenticator
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<div className="mt-12 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
||||||
|
{isTotpConfigurationLoading ? (
|
||||||
|
<ContentLoader />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{totpConfiguration?.isVerified ? (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Button colorSchema="secondary" onClick={setShouldShowRecoveryCodes.toggle}>
|
||||||
|
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
||||||
|
</Button>
|
||||||
|
<Button colorSchema="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{shouldShowRecoveryCodes && totpConfiguration.recoveryCodes.length && (
|
||||||
|
<div className="mt-4 bg-mineshaft-600 p-4">
|
||||||
|
{totpConfiguration.recoveryCodes.map((code) => (
|
||||||
|
<div key={code}>{code}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-6 flex min-w-full justify-center">
|
||||||
|
<TotpRegistration
|
||||||
|
onComplete={async () => {
|
||||||
|
await queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<EmailServiceSetupModal
|
<EmailServiceSetupModal
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
/>
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteTotpConfig.isOpen}
|
||||||
|
title="Are you sure want to delete the mobile authenticator? You’ll have to go through the setup process to enable it again."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleTotpDeletion}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user