feat(onboarding): backend api for onboarding users

This commit is contained in:
Akhil Mohan
2023-11-10 13:12:58 +05:30
parent 1de5fd28a9
commit df7d8e7be9
20 changed files with 734 additions and 220 deletions
+43
View File
@@ -0,0 +1,43 @@
import ora from "ora";
import nodemailer from "nodemailer";
import { getSmtpHost, getSmtpPort } from "./config";
import { logger } from "./utils/logging";
import mongoose from "mongoose";
import { redisClient } from "./services/RedisService";
type BootstrapOpt = {
transporter: nodemailer.Transporter;
};
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
const spinner = ora().start();
spinner.info("Checking configurations...");
spinner.info("Testing smtp connection");
await transporter
.verify()
.then(async () => {
spinner.succeed("SMTP successfully connected");
})
.catch(async (err) => {
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
logger.error(err);
});
spinner.info("Testing mongodb connection");
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
spinner.fail("Mongo DB - Failed to connect");
} else {
spinner.succeed("Mongodb successfully connected");
}
spinner.info("Testing redis connection");
const redisPing = await redisClient?.ping();
if (!redisPing) {
spinner.fail("Redis - Failed to connect");
} else {
spinner.succeed("Redis successfully connected");
}
spinner.stop();
};
+24
View File
@@ -0,0 +1,24 @@
import { IServerConfig, ServerConfig } from "../models/serverConfig";
let serverConfig: IServerConfig;
export const serverConfigInit = async () => {
const cfg = await ServerConfig.findOne({});
if (!cfg) {
const cfg = new ServerConfig();
await cfg.save();
serverConfig = cfg;
} else {
serverConfig = cfg;
}
return serverConfig;
};
export const getServerConfig = () => serverConfig;
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
if (!cfg) throw new Error("Failed to update server config");
serverConfig = cfg;
return serverConfig;
};
@@ -0,0 +1,87 @@
import { Request, Response } from "express";
import { getHttpsEnabled } from "../../config";
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import { User } from "../../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/admin";
export const getServerConfigInfo = (_req: Request, res: Response) => {
const config = getServerConfig();
return res.send({ config });
};
export const updateServerConfig = async (req: Request, res: Response) => {
const {
body: { allowSignUp }
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
const config = await setServerConfig({ allowSignUp });
return res.send({ config });
};
export const adminSignUp = async (req: Request, res: Response) => {
const cfg = getServerConfig();
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.SignupV1, req);
let user = await User.findOne({ email });
if (user) throw BadRequestError({ message: "User already exist" });
user = new User({
email,
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
superAdmin: true
});
await user.save();
await initializeDefaultOrg({ organizationName: "Admin Org", user });
await setServerConfig({ initialized: true });
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: "Successfully set up admin account",
user,
token
});
};
+4 -1
View File
@@ -16,6 +16,8 @@ import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as adminController from "./adminController";
export {
authController,
botController,
@@ -34,5 +36,6 @@ export {
workspaceController,
secretScanningController,
webhookController,
secretImpsController
secretImpsController,
adminController
};
+20 -1
View File
@@ -35,6 +35,7 @@ import {
import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import {
admin as v1AdminRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
integrationAuth as v1IntegrationAuthRouter,
@@ -94,6 +95,7 @@ import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecre
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require("smee-client"); // eslint-disable-line
import path from "path";
import { serverConfigInit } from "./config/serverConfig";
let handler: null | any = null;
@@ -101,6 +103,7 @@ const main = async () => {
const port = await getPort();
await setup();
const serverCfg = await serverConfigInit();
await EELicenseService.initGlobalFeatureSet();
@@ -203,6 +206,7 @@ const main = async () => {
// v1 routes
app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/admin", v1AdminRouter);
app.use("/api/v1/bot", v1BotRouter);
app.use("/api/v1/user", v1UserRouter);
app.use("/api/v1/user-action", v1UserActionRouter);
@@ -271,7 +275,22 @@ const main = async () => {
app.use(requestErrorHandler);
const server = app.listen(port, async () => {
logger.info(`Server started listening at port ${port}`);
if (!serverCfg.initialized) {
logger.info(`Welcome to Infisical
Create your Infisical administrator account at:
http://localhost:${port}/admin/signup
`);
} else {
logger.info(`Welcome back!
To access Infisical Administrator Panel open
http://localhost:${port}/admin
To access Infisical server
http://localhost:${port}
`);
}
});
// await createTestUserForDevelopment();
+14 -10
View File
@@ -7,17 +7,21 @@ import requireSecretAuth from "./requireSecretAuth";
import requireSecretsAuth from "./requireSecretsAuth";
import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled";
import requireE2EEOff from "./requireE2EEOff";
import { requireSuperAdminAccess } from "./requireSuperAdminAccess";
import validateRequest from "./validateRequest";
import { disableSignUpByServerCfg } from "./serverAdmin";
export {
requireAuth,
requireMfaAuth,
requireSignupAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
requireSecretAuth,
requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
validateRequest,
requireAuth,
requireMfaAuth,
requireSignupAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
requireSecretAuth,
requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
validateRequest,
requireSuperAdminAccess,
disableSignUpByServerCfg
};
@@ -0,0 +1,8 @@
import { NextFunction, Request, Response } from "express";
import { UnauthorizedRequestError } from "../utils/errors";
export const requireSuperAdminAccess = (req: Request, _res: Response, next: NextFunction) => {
const isSuperAdmin = req.user.superAdmin;
if (!isSuperAdmin) throw UnauthorizedRequestError({ message: "Requires superadmin access" });
return next();
};
+9
View File
@@ -0,0 +1,9 @@
import { NextFunction, Request, Response } from "express";
import { getServerConfig } from "../config/serverConfig";
import { BadRequestError } from "../utils/errors";
export const disableSignUpByServerCfg = (_req: Request, _res: Response, next: NextFunction) => {
const cfg = getServerConfig();
if (!cfg.allowSignUp) throw BadRequestError({ message: "Signup are disabled" });
return next();
};
+25
View File
@@ -0,0 +1,25 @@
import { Schema, Types, model } from "mongoose";
export interface IServerConfig {
_id: Types.ObjectId;
initialized: boolean;
allowSignUp: boolean;
}
const serverConfigSchema = new Schema<IServerConfig>(
{
initialized: {
type: Boolean,
default: false
},
allowSignUp: {
type: Boolean,
default: true
}
},
{
timestamps: true
}
);
export const ServerConfig = model<IServerConfig>("ServerConfig", serverConfigSchema);
+130 -114
View File
@@ -1,125 +1,141 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export interface IUser extends Document {
_id: Types.ObjectId;
authProvider?: AuthMethod;
authMethods: AuthMethod[];
email: string;
firstName?: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
salt?: string;
verifier?: string;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
ip: string;
userAgent: string;
}[];
_id: Types.ObjectId;
authProvider?: AuthMethod;
authMethods: AuthMethod[];
email: string;
superAdmin?: boolean;
firstName?: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
salt?: string;
verifier?: string;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
ip: string;
userAgent: string;
}[];
}
const userSchema = new Schema<IUser>(
{
authProvider: { // TODO field: deprecate
type: String,
enum: AuthMethod,
},
authMethods: {
type: [{
type: String,
enum: AuthMethod,
}],
default: [AuthMethod.EMAIL],
required: true
},
email: {
type: String,
required: true,
unique: true,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
encryptionVersion: {
type: Number,
select: false,
default: 1, // to resolve backward-compatibility issues
},
protectedKey: { // introduced as part of encryption version 2
type: String,
select: false,
},
protectedKeyIV: { // introduced as part of encryption version 2
type: String,
select: false,
},
protectedKeyTag: { // introduced as part of encryption version 2
type: String,
select: false,
},
publicKey: {
type: String,
select: false,
},
encryptedPrivateKey: {
type: String,
select: false,
},
iv: { // iv of [encryptedPrivateKey]
type: String,
select: false,
},
tag: { // tag of [encryptedPrivateKey]
type: String,
select: false,
},
salt: {
type: String,
select: false,
},
verifier: {
type: String,
select: false,
},
isMfaEnabled: {
type: Boolean,
default: false,
},
mfaMethods: [{
type: String,
}],
devices: {
type: [{
ip: String,
userAgent: String,
}],
default: [],
select: false,
},
},
{
timestamps: true,
}
{
authProvider: {
// TODO field: deprecate
type: String,
enum: AuthMethod
},
authMethods: {
type: [
{
type: String,
enum: AuthMethod
}
],
default: [AuthMethod.EMAIL],
required: true
},
email: {
type: String,
required: true,
unique: true
},
firstName: {
type: String
},
lastName: {
type: String
},
encryptionVersion: {
type: Number,
select: false,
default: 1 // to resolve backward-compatibility issues
},
protectedKey: {
// introduced as part of encryption version 2
type: String,
select: false
},
protectedKeyIV: {
// introduced as part of encryption version 2
type: String,
select: false
},
protectedKeyTag: {
// introduced as part of encryption version 2
type: String,
select: false
},
publicKey: {
type: String,
select: false
},
encryptedPrivateKey: {
type: String,
select: false
},
superAdmin: {
type: Boolean
},
iv: {
// iv of [encryptedPrivateKey]
type: String,
select: false
},
tag: {
// tag of [encryptedPrivateKey]
type: String,
select: false
},
salt: {
type: String,
select: false
},
verifier: {
type: String,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
},
mfaMethods: [
{
type: String
}
],
devices: {
type: [
{
ip: String,
userAgent: String
}
],
default: [],
select: false
}
},
{
timestamps: true
}
);
export const User = model<IUser>("User", userSchema);
export const User = model<IUser>("User", userSchema);
+20
View File
@@ -0,0 +1,20 @@
import express from "express";
import { adminController } from "../../controllers/v1";
const router = express.Router();
import { requireAuth, requireSuperAdminAccess } from "../../middleware";
import { AuthMode } from "../../variables";
router.get("/config", adminController.getServerConfigInfo);
router.post("/signup", adminController.adminSignUp);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireSuperAdminAccess,
adminController.updateServerConfig
);
export default router;
+3 -1
View File
@@ -18,6 +18,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import webhooks from "./webhook";
import secretImps from "./secretImps";
import admin from "./admin";
export {
signup,
@@ -39,5 +40,6 @@ export {
secretsFolder,
webhooks,
secretImps,
sso
sso,
admin
};
+3
View File
@@ -2,18 +2,21 @@ import express from "express";
const router = express.Router();
import { signupController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter";
import { disableSignUpByServerCfg } from "../../middleware";
// TODO: consider moving to users/v3/signup
router.post(
// TODO endpoint: consider moving to v3/users/signup/mail
"/email/signup",
disableSignUpByServerCfg,
authLimiter,
signupController.beginEmailSignup
);
router.post(
"/email/verify", // TODO endpoint: consider moving to v3/users/signup/verify
disableSignUpByServerCfg,
authLimiter,
signupController.verifyEmailSignup
);
+39 -37
View File
@@ -1,49 +1,51 @@
import express from "express";
const router = express.Router();
import { body } from "express-validator";
import { requireSignupAuth, validateRequest } from "../../middleware";
import { disableSignUpByServerCfg, requireSignupAuth, validateRequest } from "../../middleware";
import { signupController } from "../../controllers/v2";
import { authLimiter } from "../../helpers/rateLimiter";
router.post(
"/complete-account/signup", // TODO endpoint: deprecate (moved to v3/signup/complete/account-signup)
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountSignup
"/complete-account/signup", // TODO endpoint: deprecate (moved to v3/signup/complete/account-signup),
disableSignUpByServerCfg,
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountSignup
);
router.post(
"/complete-account/invite", // TODO: consider moving to v3/users/new/complete-account/invite
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountInvite
"/complete-account/invite", // TODO: consider moving to v3/users/new/complete-account/invite
disableSignUpByServerCfg,
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountInvite
);
export default router;
export default router;
+2 -1
View File
@@ -2,10 +2,11 @@ import express from "express";
const router = express.Router();
import { signupController } from "../../controllers/v3";
import { authLimiter } from "../../helpers/rateLimiter";
import { validateRequest } from "../../middleware";
import { disableSignUpByServerCfg, validateRequest } from "../../middleware";
router.post(
"/complete-account/signup", // TODO: consider moving endpoint to v3/users/new/complete-account/signup
disableSignUpByServerCfg,
authLimiter,
validateRequest,
signupController.completeAccountSignup
+15 -31
View File
@@ -8,30 +8,28 @@ import {
SMTP_HOST_ZOHOMAIL
} from "../variables";
import SMTPConnection from "nodemailer/lib/smtp-connection";
import * as Sentry from "@sentry/node";
import {
getSmtpHost,
getSmtpPassword,
getSmtpPort,
getSmtpSecure,
getSmtpUsername,
getSmtpUsername
} from "../config";
import { logger } from "../utils/logging";
export const initSmtp = async () => {
const mailOpts: SMTPConnection.Options = {
host: await getSmtpHost(),
port: await getSmtpPort(),
port: await getSmtpPort()
};
if ((await getSmtpUsername()) && (await getSmtpPassword())) {
mailOpts.auth = {
user: await getSmtpUsername(),
pass: await getSmtpPassword(),
pass: await getSmtpPassword()
};
}
if ((await getSmtpSecure()) ? (await getSmtpSecure()) : false) {
if ((await getSmtpSecure()) ? await getSmtpSecure() : false) {
switch (await getSmtpHost()) {
case SMTP_HOST_SENDGRID:
mailOpts.requireTLS = true;
@@ -39,38 +37,38 @@ export const initSmtp = async () => {
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_GMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_OFFICE365:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2"
}
};
break;
default:
if ((await getSmtpHost()).includes("amazonaws.com")) {
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
} else {
mailOpts.secure = true;
}
@@ -79,20 +77,6 @@ export const initSmtp = async () => {
}
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
.then(async () => {
Sentry.setUser(null);
Sentry.captureMessage("SMTP - Successfully connected");
logger.info("SMTP - Successfully connected");
})
.catch(async (err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()} \n\t${err}`
);
logger.error(err, `SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
});
return transporter;
};
+6 -3
View File
@@ -33,6 +33,7 @@ import {
initializeSamlStrategy
} from "../authn/passport";
import { logger } from "../logging";
import { bootstrap } from "../../bootstrap";
/**
* Prepare Infisical upon startup. This includes tasks like:
@@ -55,14 +56,15 @@ export const setup = async () => {
await TelemetryService.logTelemetryMessage();
// initializing SMTP configuration
setTransporter(await initSmtp());
const transporter = await initSmtp();
setTransporter(transporter);
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing auth strategies
await initializeGoogleStrategy();
await initializeGitHubStrategy()
await initializeGitHubStrategy();
await initializeGitLabStrategy();
await initializeSamlStrategy();
@@ -73,6 +75,7 @@ export const setup = async () => {
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
await bootstrap({ transporter });
/**
* NOTE: the order in this setup function is critical.
@@ -92,7 +95,7 @@ export const setup = async () => {
await backfillTrustedIps();
await backfillUserAuthMethods();
// await backfillPermission();
await migrateRoleFromOwnerToAdmin()
await migrateRoleFromOwnerToAdmin();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
+24
View File
@@ -0,0 +1,24 @@
import { z } from "zod";
export const UpdateServerConfigV1 = z.object({
body: z.object({
allowSignUp: z.boolean().optional()
})
});
export const SignupV1 = z.object({
body: z.object({
email: z.string().email().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
})
});