mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 21:29:20 +00:00
Begin groups phase 2b
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
t.boolean("isEmailVerified");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
t.dropColumn("isEmailVerified");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,7 +21,8 @@ export const UsersSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isGhost: z.boolean().default(false),
|
isGhost: z.boolean().default(false),
|
||||||
username: z.string()
|
username: z.string(),
|
||||||
|
isEmailVerified: z.boolean().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export const getDefaultOnPremFeatures = () => {
|
|||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
scim: false,
|
scim: true,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
groups: false,
|
groups: false,
|
||||||
status: null,
|
status: null,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
scim: false,
|
scim: true,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
groups: false,
|
groups: false,
|
||||||
status: null,
|
status: null,
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export type TFeatureSet = {
|
|||||||
customAlerts: false;
|
customAlerts: false;
|
||||||
auditLogs: false;
|
auditLogs: false;
|
||||||
auditLogsRetentionDays: 0;
|
auditLogsRetentionDays: 0;
|
||||||
samlSSO: false;
|
samlSSO: true;
|
||||||
scim: false;
|
scim: true;
|
||||||
ldap: false;
|
ldap: false;
|
||||||
groups: false;
|
groups: false;
|
||||||
status: null;
|
status: null;
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ type TScimServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||||
|
|
||||||
|
// TODO: finish updating all userId refs to orgMembershipId
|
||||||
|
|
||||||
export const scimServiceFactory = ({
|
export const scimServiceFactory = ({
|
||||||
licenseService,
|
licenseService,
|
||||||
scimDAL,
|
scimDAL,
|
||||||
@@ -145,6 +147,7 @@ export const scimServiceFactory = ({
|
|||||||
|
|
||||||
// SCIM server endpoints
|
// SCIM server endpoints
|
||||||
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||||
|
console.log("listScimUsers"); // done
|
||||||
const org = await orgDAL.findById(orgId);
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
if (!org.scimEnabled)
|
if (!org.scimEnabled)
|
||||||
@@ -178,9 +181,11 @@ export const scimServiceFactory = ({
|
|||||||
findOpts
|
findOpts
|
||||||
);
|
);
|
||||||
|
|
||||||
const scimUsers = users.map(({ userId, username, firstName, lastName, email }) =>
|
console.log("orgDAL.findMembership users: ", users);
|
||||||
|
|
||||||
|
const scimUsers = users.map(({ id, username, firstName, lastName, email }) =>
|
||||||
buildScimUser({
|
buildScimUser({
|
||||||
userId: userId ?? "",
|
userId: id ?? "",
|
||||||
username,
|
username,
|
||||||
firstName: firstName ?? "",
|
firstName: firstName ?? "",
|
||||||
lastName: lastName ?? "",
|
lastName: lastName ?? "",
|
||||||
@@ -197,6 +202,7 @@ export const scimServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
|
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
|
||||||
|
console.log("getScimUser"); // done
|
||||||
const [membership] = await orgDAL
|
const [membership] = await orgDAL
|
||||||
.findMembership({
|
.findMembership({
|
||||||
userId,
|
userId,
|
||||||
@@ -221,8 +227,10 @@ export const scimServiceFactory = ({
|
|||||||
status: 403
|
status: 403
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("getScimUser membership: ", membership);
|
||||||
|
|
||||||
return buildScimUser({
|
return buildScimUser({
|
||||||
userId: membership.userId as string,
|
userId: membership.id,
|
||||||
username: membership.username,
|
username: membership.username,
|
||||||
email: membership.email ?? "",
|
email: membership.email ?? "",
|
||||||
firstName: membership.firstName as string,
|
firstName: membership.firstName as string,
|
||||||
@@ -232,6 +240,7 @@ export const scimServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
|
||||||
|
console.log("createScimUser"); // TODO: update implementation to always create a new user and be based on orgMembershipId
|
||||||
const org = await orgDAL.findById(orgId);
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
if (!org)
|
if (!org)
|
||||||
@@ -331,6 +340,7 @@ export const scimServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
|
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||||
|
console.log("updateScimUser"); // done
|
||||||
const [membership] = await orgDAL
|
const [membership] = await orgDAL
|
||||||
.findMembership({
|
.findMembership({
|
||||||
userId,
|
userId,
|
||||||
@@ -380,7 +390,7 @@ export const scimServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return buildScimUser({
|
return buildScimUser({
|
||||||
userId: membership.userId as string,
|
userId: membership.id,
|
||||||
username: membership.username,
|
username: membership.username,
|
||||||
email: membership.email,
|
email: membership.email,
|
||||||
firstName: membership.firstName as string,
|
firstName: membership.firstName as string,
|
||||||
@@ -390,6 +400,7 @@ export const scimServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
|
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
|
||||||
|
console.log("replaceScimUser"); // done
|
||||||
const [membership] = await orgDAL
|
const [membership] = await orgDAL
|
||||||
.findMembership({
|
.findMembership({
|
||||||
userId,
|
userId,
|
||||||
@@ -426,7 +437,7 @@ export const scimServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return buildScimUser({
|
return buildScimUser({
|
||||||
userId: membership.userId as string,
|
userId: membership.id,
|
||||||
username: membership.username,
|
username: membership.username,
|
||||||
email: membership.email,
|
email: membership.email,
|
||||||
firstName: membership.firstName as string,
|
firstName: membership.firstName as string,
|
||||||
@@ -436,6 +447,7 @@ export const scimServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
|
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
|
||||||
|
console.log("deleteScimUser"); // done
|
||||||
const [membership] = await orgDAL
|
const [membership] = await orgDAL
|
||||||
.findMembership({
|
.findMembership({
|
||||||
userId,
|
userId,
|
||||||
@@ -489,7 +501,7 @@ export const scimServiceFactory = ({
|
|||||||
buildScimGroup({
|
buildScimGroup({
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
members: []
|
members: [] // does this need to be populated?
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||||
const userService = userServiceFactory({ userDAL });
|
const userService = userServiceFactory({ userDAL, tokenService, smtpService });
|
||||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
|
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
|
|||||||
@@ -2,11 +2,72 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { 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 } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/emails/code",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preHandler: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.user.sendEmailVerificationCode(req.permission.id);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/me/emails/verify",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
code: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preHandler: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.user.verifyEmailVerificationCode(req.permission.id, req.body.code);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/me/users/same-email",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: UsersSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preHandler: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const users = await server.services.user.listUsersWithSameEmail(req.permission.id);
|
||||||
|
return {
|
||||||
|
users
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/me/mfa",
|
url: "/me/mfa",
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
return { token, expiresAt };
|
||||||
}
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_VERIFICATION: {
|
||||||
|
// generate random 6-digit code
|
||||||
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
case TokenType.TOKEN_EMAIL_MFA: {
|
case TokenType.TOKEN_EMAIL_MFA: {
|
||||||
// generate random 6-digit code
|
// generate random 6-digit code
|
||||||
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum TokenType {
|
export enum TokenType {
|
||||||
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||||
|
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||||
TOKEN_EMAIL_MFA = "emailMfa",
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const authSignupServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.EmailVerification,
|
template: SmtpTemplates.SignupEmailVerification,
|
||||||
subjectLine: "Infisical confirmation code",
|
subjectLine: "Infisical confirmation code",
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type TSmtpSendMail = {
|
|||||||
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
||||||
|
|
||||||
export enum SmtpTemplates {
|
export enum SmtpTemplates {
|
||||||
|
SignupEmailVerification = "signupEmailVerification.handlebars",
|
||||||
EmailVerification = "emailVerification.handlebars",
|
EmailVerification = "emailVerification.handlebars",
|
||||||
SecretReminder = "secretReminder.handlebars",
|
SecretReminder = "secretReminder.handlebars",
|
||||||
EmailMfa = "emailMfa.handlebars",
|
EmailMfa = "emailMfa.handlebars",
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
<title>Code</title>
|
<title>Code</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h2>Confirm your email address</h2>
|
<h2>Confirm your email address</h2>
|
||||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
|
||||||
<h1>{{code}}</h1>
|
<h1>{{code}}</h1>
|
||||||
<p>Questions about setting up Infisical? Email us at [email protected]</p>
|
</body>
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Code</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Confirm your email address</h2>
|
||||||
|
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||||
|
<h1>{{code}}</h1>
|
||||||
|
<p>Questions about setting up Infisical? Email us at [email protected]</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,15 +1,83 @@
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
|
|
||||||
type TUserServiceFactoryDep = {
|
type TUserServiceFactoryDep = {
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
|
tokenService: TAuthTokenServiceFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||||
|
|
||||||
export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
|
export const userServiceFactory = ({ userDAL, tokenService, smtpService }: TUserServiceFactoryDep) => {
|
||||||
|
const sendEmailVerificationCode = async (userId: string) => {
|
||||||
|
console.log("sendEmailVerificationCode userId: ", userId);
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
if (!user) throw new BadRequestError({ name: "Failed to find user" });
|
||||||
|
if (!user.email)
|
||||||
|
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
|
||||||
|
if (user.isEmailVerified)
|
||||||
|
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
|
||||||
|
|
||||||
|
console.log("sendEmailVerificationCode user: ", user);
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("sendEmailVerificationCode 2");
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.EmailVerification,
|
||||||
|
subjectLine: "Infisical confirmation code",
|
||||||
|
recipients: [user.email],
|
||||||
|
substitutions: {
|
||||||
|
code: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyEmailVerificationCode = async (userId: string, code: string) => {
|
||||||
|
console.log("verifyEmailVerificationCode args: ", {
|
||||||
|
userId,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
if (!user) throw new BadRequestError({ name: "Failed to find user" });
|
||||||
|
if (user.isEmailVerified)
|
||||||
|
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
||||||
|
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
|
userId: user.id,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
await userDAL.updateById(userId, { isEmailVerified: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// lists users with same verified email only
|
||||||
|
const listUsersWithSameEmail = async (userId: string) => {
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
if (!user) throw new BadRequestError({ name: "Failed to find user" });
|
||||||
|
if (!user.email)
|
||||||
|
throw new BadRequestError({ name: "Failed to list users with same email due to no email on user" });
|
||||||
|
if (!user.isEmailVerified)
|
||||||
|
throw new BadRequestError({ name: "Failed to list users with same email due to email not verified" });
|
||||||
|
|
||||||
|
const users = await userDAL.find({
|
||||||
|
email: user.email,
|
||||||
|
isEmailVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
|
|
||||||
@@ -72,6 +140,9 @@ export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
sendEmailVerificationCode,
|
||||||
|
verifyEmailVerificationCode,
|
||||||
|
listUsersWithSameEmail,
|
||||||
toggleUserMfa,
|
toggleUserMfa,
|
||||||
updateUserName,
|
updateUserName,
|
||||||
updateAuthMethods,
|
updateAuthMethods,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export {
|
|||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
useSendPasswordResetEmail,
|
useSendPasswordResetEmail,
|
||||||
useSendVerificationEmail,
|
useSendVerificationEmail,
|
||||||
useVerifyEmailVerificationCode,
|
|
||||||
useVerifyMfaToken,
|
useVerifyMfaToken,
|
||||||
useVerifyPasswordResetCode
|
useVerifyPasswordResetCode,
|
||||||
} from "./queries";
|
useVerifySignupEmailVerificationCode} from "./queries";
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export const useSendVerificationEmail = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useVerifyEmailVerificationCode = () => {
|
export const useVerifySignupEmailVerificationCode = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ email, code }: { email: string; code: string }) => {
|
mutationFn: async ({ email, code }: { email: string; code: string }) => {
|
||||||
const { data } = await apiRequest.post("/api/v3/signup/email/verify", {
|
const { data } = await apiRequest.post("/api/v3/signup/email/verify", {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export { useAddUserToWsE2EE, useAddUserToWsNonE2EE } from "./mutation";
|
export {
|
||||||
|
useAddUserToWsE2EE,
|
||||||
|
useAddUserToWsNonE2EE,
|
||||||
|
useSendEmailVerificationCode,
|
||||||
|
useVerifyEmailVerificationCode
|
||||||
|
} from "./mutation";
|
||||||
export {
|
export {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToOrg,
|
useAddUserToOrg,
|
||||||
|
|||||||
@@ -61,3 +61,23 @@ export const useAddUserToWsNonE2EE = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSendEmailVerificationCode = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await apiRequest.post("/api/v2/users/me/emails/code");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useVerifyEmailVerificationCode = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ code }: { code: string }) => {
|
||||||
|
await apiRequest.post("/api/v2/users/me/emails/verify", {
|
||||||
|
code
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
|||||||
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { useServerConfig } from "@app/context";
|
import { useServerConfig } from "@app/context";
|
||||||
import { useVerifyEmailVerificationCode } from "@app/hooks/api";
|
import { useVerifySignupEmailVerificationCode } from "@app/hooks/api";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export default function SignUp() {
|
|||||||
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
|
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
|
||||||
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutateAsync } = useVerifyEmailVerificationCode();
|
const { mutateAsync } = useVerifySignupEmailVerificationCode();
|
||||||
const { config } = useServerConfig();
|
const { config } = useServerConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
import { BackupPDFStep, UserInfoSSOStep } from "./components";
|
import { BackupPDFStep, EmailConfirmationStep,UserInfoSSOStep } from "./components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
providerAuthToken: string;
|
providerAuthToken: string;
|
||||||
@@ -28,6 +28,8 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
|
return <EmailConfirmationStep />;
|
||||||
|
case 2:
|
||||||
return (
|
return (
|
||||||
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
|
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// confirm email
|
||||||
|
// if same email exists, then trigger fn to merge automatically
|
||||||
|
import { useState } from "react";
|
||||||
|
import ReactCodeInput from "react-code-input";
|
||||||
|
|
||||||
|
// import Error from "@app/components/basic/Error";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
import { useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "@app/hooks/api";
|
||||||
|
|
||||||
|
// The style for the verification code input
|
||||||
|
const props = {
|
||||||
|
inputStyle: {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
margin: "4px",
|
||||||
|
MozAppearance: "textfield",
|
||||||
|
width: "55px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
fontSize: "24px",
|
||||||
|
height: "55px",
|
||||||
|
paddingLeft: "7",
|
||||||
|
backgroundColor: "#0d1117",
|
||||||
|
color: "white",
|
||||||
|
border: "1px solid #2d2f33",
|
||||||
|
textAlign: "center",
|
||||||
|
outlineColor: "#8ca542",
|
||||||
|
borderColor: "#2d2f33"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const propsPhone = {
|
||||||
|
inputStyle: {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
margin: "4px",
|
||||||
|
MozAppearance: "textfield",
|
||||||
|
width: "40px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
fontSize: "24px",
|
||||||
|
height: "40px",
|
||||||
|
paddingLeft: "7",
|
||||||
|
backgroundColor: "#0d1117",
|
||||||
|
color: "white",
|
||||||
|
border: "1px solid #2d2f33",
|
||||||
|
textAlign: "center",
|
||||||
|
outlineColor: "#8ca542",
|
||||||
|
borderColor: "#2d2f33"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EmailConfirmationStep = () => {
|
||||||
|
const { user } = useUser();
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
// const [codeError, setCodeError] = useState(false);
|
||||||
|
const [isResendingVerificationEmail] = useState(false);
|
||||||
|
const [isLoading] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: sendEmailVerificationCode } = useSendEmailVerificationCode();
|
||||||
|
const { mutateAsync: verifyEmailVerificationCode } = useVerifyEmailVerificationCode();
|
||||||
|
|
||||||
|
const checkCode = async () => {
|
||||||
|
try {
|
||||||
|
console.log("checkCode code: ", code);
|
||||||
|
await verifyEmailVerificationCode({ code });
|
||||||
|
console.log("checkCode 2");
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to verify code",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resendCode = async () => {
|
||||||
|
try {
|
||||||
|
console.log("resendCode");
|
||||||
|
await sendEmailVerificationCode();
|
||||||
|
console.log("resendCode");
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to resend code",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto h-full w-full pb-4 md:px-8">
|
||||||
|
<p className="text-md flex justify-center text-bunker-200">
|
||||||
|
We've sent a verification code to
|
||||||
|
</p>
|
||||||
|
<p className="text-md my-1 flex justify-center font-semibold text-bunker-200">
|
||||||
|
{user?.email}
|
||||||
|
</p>
|
||||||
|
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
|
||||||
|
<ReactCodeInput
|
||||||
|
name=""
|
||||||
|
inputMode="tel"
|
||||||
|
type="text"
|
||||||
|
fields={6}
|
||||||
|
onChange={setCode}
|
||||||
|
{...props}
|
||||||
|
className="mt-6 mb-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto mt-4 block w-max md:hidden">
|
||||||
|
<ReactCodeInput
|
||||||
|
name=""
|
||||||
|
inputMode="tel"
|
||||||
|
type="text"
|
||||||
|
fields={6}
|
||||||
|
onChange={setCode}
|
||||||
|
{...propsPhone}
|
||||||
|
className="mt-2 mb-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* {codeError && <Error text="Oops. Your code is wrong. Please try again." />} */}
|
||||||
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
|
<div className="text-l w-full py-1 text-lg">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={checkCode}
|
||||||
|
size="sm"
|
||||||
|
isFullWidth
|
||||||
|
className="h-14"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
|
||||||
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
|
<span className="text-bunker-400">Don't see the code?</span>
|
||||||
|
<div className="text-md mt-2 flex flex-row text-bunker-400">
|
||||||
|
<button disabled={isLoading} onClick={resendCode} type="button">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
{isResendingVerificationEmail ? "Resending..." : "Resend"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="pb-2 text-sm text-bunker-400">Make sure to check your spam inbox.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { EmailConfirmationStep } from "./EmailConfirmationStep";
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { BackupPDFStep } from "./BackupPDFStep";
|
export { BackupPDFStep } from "./BackupPDFStep";
|
||||||
|
export { EmailConfirmationStep } from "./EmailConfirmationStep";
|
||||||
export { UserInfoSSOStep } from "./UserInfoSSOStep";
|
export { UserInfoSSOStep } from "./UserInfoSSOStep";
|
||||||
|
|||||||
Reference in New Issue
Block a user