mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 07:26:11 +00:00
feat(infisical-pg): completed signup, login, password and backup key migration
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.*
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY package-lock.json package-lock.json
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev:docker"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": ".ts,.js",
|
||||||
|
"ignore": [],
|
||||||
|
"exec": "tsx ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine"
|
||||||
|
}
|
||||||
Generated
+1309
File diff suppressed because it is too large
Load Diff
+23
-1
@@ -6,15 +6,28 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
"dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
||||||
|
"dev:docker": "nodemon",
|
||||||
"type:check": "tsc --noEmit",
|
"type:check": "tsc --noEmit",
|
||||||
"lint:fix": "eslint --fix 'src/**/*.ts'",
|
"lint:fix": "eslint --fix 'src/**/*.ts'",
|
||||||
"lint": "eslint 'src/**/*.ts'"
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
|
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||||
|
"migration:new": "tsx ./scripts/create-migration.ts",
|
||||||
|
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||||
|
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||||
|
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||||
|
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
|
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.9.5",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
"eslint": "^8.54.0",
|
"eslint": "^8.54.0",
|
||||||
@@ -24,7 +37,9 @@
|
|||||||
"eslint-plugin-import": "^2.29.0",
|
"eslint-plugin-import": "^2.29.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
"pino-pretty": "^10.2.3",
|
"pino-pretty": "^10.2.3",
|
||||||
|
"prompt-sync": "^4.2.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsx": "^4.4.0",
|
"tsx": "^4.4.0",
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
@@ -36,11 +51,18 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^1.10.1",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||||
"fastify": "^4.24.3",
|
"fastify": "^4.24.3",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jsrp": "^0.2.4",
|
||||||
|
"knex": "^3.0.1",
|
||||||
|
"nodemailer": "^6.9.7",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
|
"pg": "^8.11.3",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-to-json-schema": "^3.22.0"
|
"zod-to-json-schema": "^3.22.0"
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import promptSync from "prompt-sync";
|
||||||
|
|
||||||
|
const prompt = promptSync({
|
||||||
|
sigint: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
Component List
|
||||||
|
--------------
|
||||||
|
1. Service component
|
||||||
|
2. Schema file
|
||||||
|
`);
|
||||||
|
const componentType = parseInt(prompt("Select a component: "), 10);
|
||||||
|
|
||||||
|
if (componentType === 1) {
|
||||||
|
const componentName = prompt("Enter service name: ");
|
||||||
|
const dir = path.join(__dirname, `../src/services/${componentName}`);
|
||||||
|
const capitalizedComponentName = componentName.at(0)?.toUpperCase() + componentName.slice(1);
|
||||||
|
const dalTypeName = `T${capitalizedComponentName}DalFactory`;
|
||||||
|
const dalName = `${componentName}DalFactory`;
|
||||||
|
const serviceTypeName = `T${capitalizedComponentName}ServiceFactory`;
|
||||||
|
const serviceName = `${componentName}ServiceFactory`;
|
||||||
|
|
||||||
|
mkdirSync(dir);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, `${componentName}-dal.ts`),
|
||||||
|
`import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export type ${dalTypeName} = {};
|
||||||
|
|
||||||
|
export const ${dalName} = (db: TDbClient): ${dalTypeName} => {
|
||||||
|
|
||||||
|
return { };
|
||||||
|
};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, `${componentName}-service.ts`),
|
||||||
|
`import { ${dalTypeName} } from "./${componentName}-dal";
|
||||||
|
|
||||||
|
type ${serviceTypeName}Dep = {
|
||||||
|
${componentName}Dal: ${dalTypeName};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>;
|
||||||
|
|
||||||
|
export const ${serviceName} = ({ ${componentName}Dal }: ${serviceTypeName}Dep) => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
writeFileSync(path.join(dir, `${componentName}-types.ts`), "");
|
||||||
|
} else if (componentType === 2) {
|
||||||
|
const componentName = prompt("Type component name in lowercase with space seperated: ");
|
||||||
|
const dashcase = componentName.split(" ").join("-");
|
||||||
|
const pascalCase = componentName
|
||||||
|
.split(" ")
|
||||||
|
.reduce(
|
||||||
|
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||||
|
`
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ${pascalCase}Schema = z.object({});
|
||||||
|
|
||||||
|
export type T${pascalCase} = z.infer<typeof ${pascalCase}Schema>;
|
||||||
|
export type T${pascalCase}Insert = Omit<T${pascalCase}, TImmutableDBKeys>;
|
||||||
|
export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
import promptSync from "prompt-sync";
|
||||||
|
|
||||||
|
const prompt = promptSync();
|
||||||
|
|
||||||
|
const migrationName = prompt("Enter name for migration: ");
|
||||||
|
|
||||||
|
execSync(
|
||||||
|
`npx knex migrate:make --knexfile ${path.join(
|
||||||
|
__dirname,
|
||||||
|
"../src/db/knexfile.ts"
|
||||||
|
)} -x ts ${migrationName}`,
|
||||||
|
{ stdio: "inherit" }
|
||||||
|
);
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
FastifyInstance,
|
||||||
|
RawReplyDefaultExpression,
|
||||||
|
RawRequestDefaultExpression,
|
||||||
|
RawServerDefault
|
||||||
|
} from "fastify";
|
||||||
|
import { Logger } from "pino";
|
||||||
|
|
||||||
|
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
type FastifyZodProvider = FastifyInstance<
|
||||||
|
RawServerDefault,
|
||||||
|
RawRequestDefaultExpression<RawServerDefault>,
|
||||||
|
RawReplyDefaultExpression<RawServerDefault>,
|
||||||
|
Readonly<Logger>,
|
||||||
|
ZodTypeProvider
|
||||||
|
>;
|
||||||
|
}
|
||||||
Vendored
+24
-11
@@ -1,19 +1,32 @@
|
|||||||
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
import { TUser } from "@app/db/schemas";
|
||||||
|
import { TAuthDalFactory } from "@app/services/auth/auth-dal";
|
||||||
|
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||||
|
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||||
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
|
|
||||||
import "fastify";
|
import "fastify";
|
||||||
|
|
||||||
declare module "fastify" {
|
declare module "fastify" {
|
||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
realIp: string;
|
realIp: string;
|
||||||
|
// used for mfa session authentication
|
||||||
|
mfa: {
|
||||||
|
userId: string;
|
||||||
|
user: TUser;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FastifyInstance {
|
||||||
|
services: {
|
||||||
|
login: TAuthLoginFactory;
|
||||||
|
password: TAuthPasswordFactory;
|
||||||
|
signup: TAuthSignupFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
|
// everywhere else access using service layer
|
||||||
|
store: {
|
||||||
|
user: Pick<TAuthDalFactory, "getUserById">;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
|
||||||
type FastifyZodProvider = FastifyInstance<
|
|
||||||
RawServerDefault,
|
|
||||||
RawRequestDefaultExpression<RawServerDefault>,
|
|
||||||
RawReplyDefaultExpression<RawServerDefault>,
|
|
||||||
FastifyBaseLogger,
|
|
||||||
ZodTypeProvider
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TableName,
|
||||||
|
TBackupPrivateKey,
|
||||||
|
TBackupPrivateKeyInsert,
|
||||||
|
TToken,
|
||||||
|
TTokenInsert,
|
||||||
|
TTokenUpdate,
|
||||||
|
TUser,
|
||||||
|
TUserEncryptionKey,
|
||||||
|
TUserEncryptionKeyInsert,
|
||||||
|
TUserEncryptionKeyUpdate,
|
||||||
|
TUserInsert,
|
||||||
|
TUserUpdate
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
TTokenSession,
|
||||||
|
TTokenSessionInsert,
|
||||||
|
TTokenSessionUpdate
|
||||||
|
} from "@app/db/schemas/token-session";
|
||||||
|
|
||||||
|
declare module "knex/types/tables" {
|
||||||
|
interface Tables extends { [key in TableName]: Knex.CompositeTableType<any> } {
|
||||||
|
[TableName.Users]: Knex.CompositeTableType<TUser, TUserInsert, TUserUpdate>;
|
||||||
|
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
|
||||||
|
TUserEncryptionKey,
|
||||||
|
TUserEncryptionKeyInsert,
|
||||||
|
TUserEncryptionKeyUpdate
|
||||||
|
>;
|
||||||
|
[TableName.AuthTokens]: Knex.CompositeTableType<TToken, TTokenInsert, TTokenUpdate>;
|
||||||
|
[TableName.AuthTokenSession]: Knex.CompositeTableType<
|
||||||
|
TTokenSession,
|
||||||
|
TTokenSessionInsert,
|
||||||
|
TTokenSessionUpdate
|
||||||
|
>;
|
||||||
|
[TableName.BackupPrivateKey]: Knex.CompositeTableType<
|
||||||
|
TBackupPrivateKey,
|
||||||
|
TBackupPrivateKeyInsert,
|
||||||
|
TTokenSessionUpdate
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type { TDbClient } from "./instance";
|
||||||
|
export { initDbConnection } from "./instance";
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import knex from "knex";
|
||||||
|
|
||||||
|
export type TDbClient = ReturnType<typeof initDbConnection>;
|
||||||
|
export const initDbConnection = (dbConnectionUri: string) => {
|
||||||
|
const db = knex({
|
||||||
|
client: "pg",
|
||||||
|
connection: dbConnectionUri
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import "ts-node/register";
|
||||||
|
|
||||||
|
// Update with your config settings.
|
||||||
|
dotenv.config({
|
||||||
|
path: path.join(__dirname, "../../.env"),
|
||||||
|
debug: true
|
||||||
|
});
|
||||||
|
export default {
|
||||||
|
useNullAsDefault: true,
|
||||||
|
client: "postgres",
|
||||||
|
connection: process.env.DB_CONNECTION_URI,
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: "infisical_migrations"
|
||||||
|
}
|
||||||
|
} as Knex.Config;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import {
|
||||||
|
createOnUpdateTrigger,
|
||||||
|
createUpdateAtTriggerFunction,
|
||||||
|
dropOnUpdateTrigger,
|
||||||
|
dropUpdatedAtTriggerFunction
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const isTablePresent = await knex.schema.hasTable(TableName.Users);
|
||||||
|
if (!isTablePresent) {
|
||||||
|
await knex.schema.createTable(TableName.Users, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.string("email").notNullable();
|
||||||
|
t.specificType("authMethods", "text[]");
|
||||||
|
t.boolean("superAdmin").defaultTo(false);
|
||||||
|
t.string("firstName");
|
||||||
|
t.string("lastName");
|
||||||
|
t.boolean("isAccepted").defaultTo(false);
|
||||||
|
t.boolean("isMfaEnabled").defaultTo(false);
|
||||||
|
t.specificType("mfaMethods", "text[]");
|
||||||
|
t.jsonb("devices");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// this is a one time function
|
||||||
|
await createUpdateAtTriggerFunction(knex);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Users);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Users);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Users);
|
||||||
|
await dropUpdatedAtTriggerFunction(knex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const isTablePresent = await knex.schema.hasTable(TableName.UserEncryptionKey);
|
||||||
|
if (!isTablePresent) {
|
||||||
|
await knex.schema.createTable(TableName.UserEncryptionKey, (t) => {
|
||||||
|
t.increments().primary();
|
||||||
|
t.text("clientPublicKey");
|
||||||
|
t.text("serverPrivateKey");
|
||||||
|
t.text("encryptionVersion").defaultTo(1);
|
||||||
|
t.text("protectedKey").notNullable();
|
||||||
|
t.text("protectedKeyIV").notNullable();
|
||||||
|
t.text("protectedKeyTag").notNullable();
|
||||||
|
t.text("publicKey").notNullable();
|
||||||
|
t.text("encryptedPrivateKey").notNullable();
|
||||||
|
t.text("iv").notNullable();
|
||||||
|
t.text("tag").notNullable();
|
||||||
|
t.text("salt").notNullable();
|
||||||
|
t.text("verifier").notNullable();
|
||||||
|
// one to one relationship
|
||||||
|
t.uuid("userId").notNullable().unique();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.UserEncryptionKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const isTablePresent = await knex.schema.hasTable(TableName.AuthTokens);
|
||||||
|
if (!isTablePresent) {
|
||||||
|
await knex.schema.createTable(TableName.AuthTokens, (t) => {
|
||||||
|
t.string("type").notNullable();
|
||||||
|
t.string("phoneNumber");
|
||||||
|
t.string("tokenHash").notNullable();
|
||||||
|
t.integer("triesLeft");
|
||||||
|
t.datetime("expiresAt").notNullable();
|
||||||
|
// does not need update trigger we will do it manually
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("userId");
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.AuthTokens);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const isTablePresent = await knex.schema.hasTable(TableName.AuthTokenSession);
|
||||||
|
if (!isTablePresent) {
|
||||||
|
await knex.schema.createTable(TableName.AuthTokenSession, (t) => {
|
||||||
|
t.increments();
|
||||||
|
t.string("ip").notNullable();
|
||||||
|
t.string("userAgent");
|
||||||
|
t.integer("refreshVersion").notNullable().defaultTo(1);
|
||||||
|
t.integer("accessVersion").notNullable().defaultTo(1);
|
||||||
|
t.datetime("lastUsed").notNullable();
|
||||||
|
// does not need update trigger we will do it manually
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// this is a one time function
|
||||||
|
await createOnUpdateTrigger(knex, TableName.AuthTokenSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.AuthTokenSession);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.AuthTokenSession);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesTableExist = await knex.schema.hasTable(TableName.BackupPrivateKey);
|
||||||
|
if (!doesTableExist) {
|
||||||
|
await knex.schema.createTable(TableName.BackupPrivateKey, (t) => {
|
||||||
|
t.increments();
|
||||||
|
t.string("encryptedPrivateKey").notNullable();
|
||||||
|
t.string("iv").notNullable();
|
||||||
|
t.string("tag").notNullable();
|
||||||
|
t.string("algorithm").notNullable();
|
||||||
|
t.string("keyEncoding").notNullable();
|
||||||
|
t.string("salt").notNullable();
|
||||||
|
t.string("verifier").notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("userId").notNullable().unique();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.BackupPrivateKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretEncryptionAlgo, SecretKeyEncoding, TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const BackupPrivateKeySchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
encryptedPrivateKey: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
algorithm: z.nativeEnum(SecretEncryptionAlgo),
|
||||||
|
keyEncoding: z.nativeEnum(SecretKeyEncoding),
|
||||||
|
salt: z.string(),
|
||||||
|
verifier: z.string(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
updatedAt: z.string().datetime()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TBackupPrivateKey = z.infer<typeof BackupPrivateKeySchema>;
|
||||||
|
export type TBackupPrivateKeyInsert = Omit<TBackupPrivateKey, TImmutableDBKeys>;
|
||||||
|
export type TBackupPrivateKeyUpdate = Partial<Omit<TBackupPrivateKey, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export {
|
||||||
|
BackupPrivateKeySchema,
|
||||||
|
TBackupPrivateKey,
|
||||||
|
TBackupPrivateKeyInsert,
|
||||||
|
TBackupPrivateKeyUpdate
|
||||||
|
} from "./backup-private-key";
|
||||||
|
export { TableName } from "./models";
|
||||||
|
export { TokenSchema, TToken, TTokenInsert, TTokenUpdate } from "./token";
|
||||||
|
export { AuthMethod, TUser, TUserInsert, TUserUpdate, UserSchema } from "./user";
|
||||||
|
export {
|
||||||
|
TUserEncryptionKey,
|
||||||
|
TUserEncryptionKeyInsert,
|
||||||
|
TUserEncryptionKeyUpdate,
|
||||||
|
UserEncryptionKey
|
||||||
|
} from "./user-encryption-key";
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export enum TableName {
|
||||||
|
Users = "users",
|
||||||
|
UserEncryptionKey = "user_encryption_keys",
|
||||||
|
AuthTokens = "auth_tokens",
|
||||||
|
AuthTokenSession = "auth_token_sessions",
|
||||||
|
BackupPrivateKey = "backup_private_key"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
|
||||||
|
export enum SecretEncryptionAlgo {
|
||||||
|
AES_256_GCM = "aes-256-gcm"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SecretKeyEncoding {
|
||||||
|
UTF8 = "utf8",
|
||||||
|
BASE64 = "base64",
|
||||||
|
HEX = "hex"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const TokenSessionSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
ip: z.string(),
|
||||||
|
userAgent: z.string(),
|
||||||
|
refreshVersion: z.number().default(1),
|
||||||
|
accessVersion: z.number().default(1),
|
||||||
|
lastUsed: z.string().datetime()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TTokenSession = z.infer<typeof TokenSessionSchema>;
|
||||||
|
export type TTokenSessionInsert = Omit<TTokenSession, TImmutableDBKeys>;
|
||||||
|
export type TTokenSessionUpdate = Partial<Omit<TTokenSession, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const TokenSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
type: z.enum(["emailConfirmation", "emailMfa", "organizationInvitation", "passwordReset"]),
|
||||||
|
phoneNumber: z.string().optional(),
|
||||||
|
tokenHash: z.string(),
|
||||||
|
triesLeft: z.number().optional(),
|
||||||
|
expiresAt: z.string().datetime().optional(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
updatedAt: z.string().datetime(),
|
||||||
|
userId: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TToken = z.infer<typeof TokenSchema>;
|
||||||
|
export type TTokenInsert = Omit<TToken, TImmutableDBKeys>;
|
||||||
|
export type TTokenUpdate = Partial<Omit<TToken, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { PickRequired } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const UserEncryptionKey = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
userId: z.string(),
|
||||||
|
serverPrivateKey: z.string().optional().nullable(),
|
||||||
|
clientPublicKey: z.string().optional().nullable(),
|
||||||
|
encryptionVersion: z.number().default(1).optional(),
|
||||||
|
protectedKey: z.string(),
|
||||||
|
protectedKeyIV: z.string(),
|
||||||
|
protectedKeyTag: z.string(),
|
||||||
|
publicKey: z.string(),
|
||||||
|
encryptedPrivateKey: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
salt: z.string(),
|
||||||
|
verifier: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUserEncryptionKey = z.infer<typeof UserEncryptionKey>;
|
||||||
|
export type TUserEncryptionKeyInsert = Omit<PickRequired<TUserEncryptionKey>, TImmutableDBKeys>;
|
||||||
|
export type TUserEncryptionKeyUpdate = Partial<Omit<TUserEncryptionKey, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export enum AuthMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
GOOGLE = "google",
|
||||||
|
GITHUB = "github",
|
||||||
|
GITLAB = "gitlab",
|
||||||
|
OKTA_SAML = "okta-saml",
|
||||||
|
AZURE_SAML = "azure-saml",
|
||||||
|
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
authMethods: z.nativeEnum(AuthMethod).array().default([AuthMethod.EMAIL]).optional().nullable(),
|
||||||
|
email: z.string(),
|
||||||
|
isSuperAdmin: z.boolean().default(false).optional(),
|
||||||
|
firstName: z.string().optional().nullable(),
|
||||||
|
lastName: z.string().optional().nullable(),
|
||||||
|
isMfaEnabled: z.boolean().default(false).optional(),
|
||||||
|
mfaMethods: z.string().array().default([]).optional().nullable(),
|
||||||
|
isAccepted: z.boolean().default(false).optional(),
|
||||||
|
devices: z.string().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UserDeviceSchema = z
|
||||||
|
.object({
|
||||||
|
ip: z.string(),
|
||||||
|
userAgent: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.default([]);
|
||||||
|
|
||||||
|
export type TUser = z.infer<typeof UserSchema>;
|
||||||
|
export type TUserInsert = Omit<TUser, TImmutableDBKeys>;
|
||||||
|
export type TUserUpdate = Partial<Omit<TUser, "id" | "email">>;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
export const createJunctionTable = (
|
||||||
|
knex: Knex,
|
||||||
|
tableName: string,
|
||||||
|
table1Name: string,
|
||||||
|
table2Name: string
|
||||||
|
) =>
|
||||||
|
knex.schema.createTable(tableName, (table) => {
|
||||||
|
table.increments(); // Primary key
|
||||||
|
table.integer(`${table1Name}Id`).unsigned().notNullable(); // Foreign key for table1
|
||||||
|
table.integer(`${table2Name}Id`).unsigned().notNullable(); // Foreign key for table2
|
||||||
|
table.foreign(`${table1Name}Id`).references("id").inTable(table2Name);
|
||||||
|
table.foreign(`${table2Name}Id`).references("id").inTable(table1Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// one time logic
|
||||||
|
// this is a postgres function log to set updateAt to present time whenever row gets updated
|
||||||
|
export const createUpdateAtTriggerFunction = (knex: Knex) =>
|
||||||
|
knex.raw(`
|
||||||
|
CREATE OR REPLACE FUNCTION on_update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW."updatedAt" = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const dropUpdatedAtTriggerFunction = (knex: Knex) =>
|
||||||
|
knex.raw(`
|
||||||
|
DROP FUNCTION IF EXISTS on_update_timestamp() CASCADE;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// we would be using this to apply updatedAt where ever we wanta
|
||||||
|
// remember to set `timestamps(true,true,true)` before this on schema
|
||||||
|
export const createOnUpdateTrigger = (knex: Knex, tableName: string) =>
|
||||||
|
knex.raw(`
|
||||||
|
CREATE TRIGGER ${tableName}_updatedAt
|
||||||
|
BEFORE UPDATE ON ${tableName}
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE on_update_timestamp();
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const dropOnUpdateTrigger = (knex: Knex, tableName: string) =>
|
||||||
|
knex.raw(`DROP TRIGGER IF EXISTS ${tableName}_updatedAt ON ${tableName}`);
|
||||||
@@ -1,16 +1,39 @@
|
|||||||
import { Logger } from "pino";
|
import { Logger } from "pino";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zpStr } from "../zod";
|
||||||
|
|
||||||
|
const zodStrBool = z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val === "true");
|
||||||
|
|
||||||
const envSchema = z.object({
|
const envSchema = z.object({
|
||||||
|
PORT: z.coerce.number().default(4000),
|
||||||
|
HOST: zpStr(z.string().default("localhost")),
|
||||||
|
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||||
SALT_ROUNDS: z.coerce.number().default(10),
|
SALT_ROUNDS: z.coerce.number().default(10),
|
||||||
// TODO(akhilmhdh): will be changed to one
|
// TODO(akhilmhdh): will be changed to one
|
||||||
ENCRYPTION_KEY: z.string().optional(),
|
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||||
ROOT_ENCRYPTION_KEY: z.string().optional(),
|
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||||
HTTPS_ENABLED: z
|
HTTPS_ENABLED: zodStrBool,
|
||||||
.enum(["true", "false"])
|
// smtp options
|
||||||
.optional()
|
SMTP_HOST: zpStr(z.string().optional()),
|
||||||
.transform((val) => val === "true")
|
SMTP_SECURE: zodStrBool,
|
||||||
|
SMTP_PORT: z.coerce.number().default(587),
|
||||||
|
SMTP_USERNAME: zpStr(z.string().optional()),
|
||||||
|
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||||
|
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
|
||||||
|
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
|
||||||
|
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
|
||||||
|
SITE_URL: zpStr(z.string().optional()),
|
||||||
|
// jwt options
|
||||||
|
JWT_AUTH_SECRET: zpStr(z.string()),
|
||||||
|
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||||
|
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||||
|
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||||
|
JWT_MFA_LIFETIME: zpStr(z.string().default("5m"))
|
||||||
});
|
});
|
||||||
|
|
||||||
let envCfg: Readonly<z.infer<typeof envSchema>>;
|
let envCfg: Readonly<z.infer<typeof envSchema>>;
|
||||||
@@ -21,8 +44,20 @@ export const initEnvConfig = (logger: Logger) => {
|
|||||||
const parsedEnv = envSchema.safeParse(process.env);
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
if (!parsedEnv.success) {
|
if (!parsedEnv.success) {
|
||||||
logger.error("Invalid environment variables. Check the error below");
|
logger.error("Invalid environment variables. Check the error below");
|
||||||
logger.error(parsedEnv.error);
|
logger.error(parsedEnv.error.issues);
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
envCfg = Object.freeze(parsedEnv.data);
|
envCfg = Object.freeze(parsedEnv.data);
|
||||||
|
return envCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatSmtpConfig = () => ({
|
||||||
|
host: envCfg.SMTP_HOST,
|
||||||
|
port: envCfg.SMTP_PORT,
|
||||||
|
auth:
|
||||||
|
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
||||||
|
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
||||||
|
: undefined,
|
||||||
|
secure: envCfg.SMTP_SECURE,
|
||||||
|
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
|
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const server = new jsrp.server();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
server.init({ salt, verifier }, () => resolve(null));
|
||||||
|
});
|
||||||
|
return { pubKey: server.getPublicKey(), privateKey: server.getPrivateKey() };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const srpCheckClientProof = async (
|
||||||
|
salt: string,
|
||||||
|
verifier: string,
|
||||||
|
serverPrivateKey: string,
|
||||||
|
clientPublicKey: string,
|
||||||
|
clientProof: string
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const server = new jsrp.server();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
server.init({ salt, verifier, b: serverPrivateKey }, () => resolve(null));
|
||||||
|
});
|
||||||
|
server.setClientPublicKey(clientPublicKey);
|
||||||
|
return server.checkClientProof(clientProof);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
export class DatabaseError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name: string; error: unknown }) {
|
||||||
|
super(message || "Failed to execute db ops");
|
||||||
|
this.name = name;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name: string; error: unknown }) {
|
||||||
|
super(message ?? "You are not allowed to access this resourve");
|
||||||
|
this.name = name;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BadRequestError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name: string; error: unknown }) {
|
||||||
|
super(message ?? "The request is invalid");
|
||||||
|
this.name = name;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type RequiredKeys<T> = {
|
||||||
|
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
export { isDisposableEmail } from "./validate-email";
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export const isDisposableEmail = async (email: string) => {
|
||||||
|
const emailDomain = email.split("@")[1];
|
||||||
|
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
|
||||||
|
|
||||||
|
if (disposableEmails.split("\n").includes(emailDomain)) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// this is a patched zod string to remove empty string to undefined
|
||||||
|
export const zpStr = <T extends z.ZodString>(
|
||||||
|
schema: T,
|
||||||
|
opt: { stripNull: boolean } = { stripNull: true }
|
||||||
|
) =>
|
||||||
|
z.preprocess((val) => {
|
||||||
|
if (opt.stripNull && val === null) return undefined;
|
||||||
|
if (typeof val !== "string") return val;
|
||||||
|
return val.trim() || undefined;
|
||||||
|
}, schema);
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import fasitfy from "fastify";
|
import fasitfy from "fastify";
|
||||||
import { z } from "zod";
|
|
||||||
import type { FastifyCookieOptions } from "@fastify/cookie";
|
import type { FastifyCookieOptions } from "@fastify/cookie";
|
||||||
import cookie from "@fastify/cookie";
|
import cookie from "@fastify/cookie";
|
||||||
import type { FastifyCorsOptions } from "@fastify/cors";
|
import type { FastifyCorsOptions } from "@fastify/cors";
|
||||||
@@ -9,20 +8,24 @@ import helmet from "@fastify/helmet";
|
|||||||
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
||||||
import ratelimiter from "@fastify/rate-limit";
|
import ratelimiter from "@fastify/rate-limit";
|
||||||
|
|
||||||
import { initEnvConfig } from "@lib/config/env";
|
import { initDbConnection } from "@app/db";
|
||||||
|
import { smtpServiceFactory } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
|
import { formatSmtpConfig, initEnvConfig } from "@lib/config/env";
|
||||||
import { initLogger } from "@lib/logger";
|
import { initLogger } from "@lib/logger";
|
||||||
|
|
||||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||||
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
||||||
import { fastifyIp } from "./plugins/ip";
|
import { fastifyIp } from "./plugins/ip";
|
||||||
import { fastifySwagger } from "./plugins/swagger";
|
import { fastifySwagger } from "./plugins/swagger";
|
||||||
|
import { registerRoutes } from "./routes";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Run the server!
|
// Run the server!
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const logger = await initLogger();
|
const logger = await initLogger();
|
||||||
initEnvConfig(logger);
|
const envCfg = initEnvConfig(logger);
|
||||||
|
|
||||||
const server = fasitfy({
|
const server = fasitfy({
|
||||||
logger,
|
logger,
|
||||||
@@ -32,42 +35,31 @@ const main = async () => {
|
|||||||
server.setValidatorCompiler(validatorCompiler);
|
server.setValidatorCompiler(validatorCompiler);
|
||||||
server.setSerializerCompiler(serializerCompiler);
|
server.setSerializerCompiler(serializerCompiler);
|
||||||
|
|
||||||
|
const db = initDbConnection(envCfg.DB_CONNECTION_URI);
|
||||||
|
const smtp = smtpServiceFactory(formatSmtpConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO(akhilmhdh:pg): change this to environment variable with default
|
|
||||||
await server.register<FastifyCookieOptions>(cookie, {
|
await server.register<FastifyCookieOptions>(cookie, {
|
||||||
secret: "infisical-cookie-secret"
|
secret: envCfg.COOKIE_SECRET_SIGN_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.register<FastifyCorsOptions>(cors, {
|
await server.register<FastifyCorsOptions>(cors, {
|
||||||
credentials: true,
|
credentials: true,
|
||||||
origin: "http://localhost:3000"
|
origin: true
|
||||||
});
|
});
|
||||||
// pull ip based on various proxy headers
|
// pull ip based on various proxy headers
|
||||||
await server.register(fastifyIp);
|
await server.register(fastifyIp);
|
||||||
|
|
||||||
// Rate limiters and security headers
|
|
||||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
|
|
||||||
await server.register(helmet);
|
|
||||||
|
|
||||||
await server.register(fastifySwagger);
|
await server.register(fastifySwagger);
|
||||||
|
|
||||||
// Declare a route
|
// Rate limiters and security headers
|
||||||
server.route({
|
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
|
||||||
method: "GET",
|
await server.register(helmet, { contentSecurityPolicy: false });
|
||||||
url: "/",
|
|
||||||
schema: {
|
|
||||||
response: {
|
|
||||||
200: z.object({ hello: z.string() })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: () => ({
|
|
||||||
hello: "world"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
await server.register(registerRoutes, { prefix: "/api", smtp, db });
|
||||||
await server.ready();
|
await server.ready();
|
||||||
server.swagger();
|
server.swagger();
|
||||||
await server.listen({ port: 8000 });
|
await server.listen({ port: envCfg.PORT, host: envCfg.HOST });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
server.log.error(err);
|
server.log.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import swaggerUI from "@fastify/swagger-ui";
|
|||||||
|
|
||||||
import { jsonSchemaTransform } from "./fastify-zod";
|
import { jsonSchemaTransform } from "./fastify-zod";
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): change the localhost port later
|
||||||
export const fastifySwagger = fp(async (fastify) => {
|
export const fastifySwagger = fp(async (fastify) => {
|
||||||
await fastify.register(swagger, {
|
await fastify.register(swagger, {
|
||||||
transform: jsonSchemaTransform,
|
transform: jsonSchemaTransform,
|
||||||
@@ -15,12 +16,12 @@ export const fastifySwagger = fp(async (fastify) => {
|
|||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.infisical.com",
|
url: "http://localhost:4000",
|
||||||
description: "Production server"
|
description: "Local server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "http://localhost:8000",
|
url: "https://app.infisical.com",
|
||||||
description: "Local server"
|
description: "Production server"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { authDalFactory } from "@app/services/auth/auth-dal";
|
||||||
|
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||||
|
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||||
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { tokenDalFactory } from "@app/services/token/token-dal";
|
||||||
|
import { tokenServiceFactory } from "@app/services/token/token-service";
|
||||||
|
|
||||||
|
import { registerV1Routes } from "./v1";
|
||||||
|
import { registerV2Routes } from "./v2";
|
||||||
|
import { registerV3Routes } from "./v3";
|
||||||
|
|
||||||
|
export const registerRoutes = async (
|
||||||
|
server: FastifyZodProvider,
|
||||||
|
{ db, smtp }: { db: Knex; smtp: TSmtpService }
|
||||||
|
) => {
|
||||||
|
// db layers
|
||||||
|
const authDal = authDalFactory(db);
|
||||||
|
const authTokenDal = tokenDalFactory(db);
|
||||||
|
|
||||||
|
// service layers
|
||||||
|
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal });
|
||||||
|
const loginService = authLoginServiceFactory({ authDal, smtpService: smtp, tokenService });
|
||||||
|
const passwordService = authPaswordServiceFactory({ tokenService, smtpService: smtp, authDal });
|
||||||
|
const signupService = authSignupServiceFactory({ tokenService, smtpService: smtp, authDal });
|
||||||
|
|
||||||
|
// inject all services
|
||||||
|
server.decorate("services", {
|
||||||
|
login: loginService,
|
||||||
|
password: passwordService,
|
||||||
|
signup: signupService
|
||||||
|
} as FastifyZodProvider["services"]);
|
||||||
|
|
||||||
|
server.decorate("store", {
|
||||||
|
user: authDal
|
||||||
|
} as FastifyZodProvider["store"]);
|
||||||
|
|
||||||
|
// register routes for v1
|
||||||
|
await server.register(registerV1Routes, { prefix: "/v1" });
|
||||||
|
await server.register(registerV2Routes, { prefix: "/v2" });
|
||||||
|
await server.register(registerV3Routes, { prefix: "/v3" });
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { registerPasswordRouter } from "./password-router";
|
||||||
|
|
||||||
|
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||||
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BackupPrivateKeySchema } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/srp1",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
clientPublicKey: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serverPublicKey: z.string(),
|
||||||
|
salt: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
|
||||||
|
req.auth.userId,
|
||||||
|
req.body.clientPublicKey
|
||||||
|
);
|
||||||
|
return { salt, serverPublicKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/change-password",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
clientProof: z.string().trim(),
|
||||||
|
protectedKey: z.string().trim(),
|
||||||
|
protectedKeyIV: z.string().trim(),
|
||||||
|
protectedKeyTag: z.string().trim(),
|
||||||
|
encryptedPrivateKey: z.string().trim(),
|
||||||
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
|
salt: z.string().trim(),
|
||||||
|
verifier: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await server.services.password.changePassword({ ...req.body, userId: req.auth.userId });
|
||||||
|
|
||||||
|
res.cookie("jid", appCfg.COOKIE_SECRET_SIGN_KEY, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
return { message: "Successfully changed password" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/backup-private-key",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
clientProof: z.string().trim(),
|
||||||
|
encryptedPrivateKey: z.string().trim(),
|
||||||
|
iv: z.string().trim(),
|
||||||
|
tag: z.string().trim(),
|
||||||
|
salt: z.string().trim(),
|
||||||
|
verifier: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
backupPrivateKey: BackupPrivateKeySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
|
||||||
|
...req.body,
|
||||||
|
userId: req.auth.userId
|
||||||
|
});
|
||||||
|
if (!backupPrivateKey) throw new Error("Failed to create backup key");
|
||||||
|
|
||||||
|
return { message: "Successfully updated backup private key", backupPrivateKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/backup-private-key",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
backupPrivateKey: BackupPrivateKeySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const backupPrivateKey = await server.services.password.getBackupPrivateKeyOfUser(
|
||||||
|
req.auth.userId
|
||||||
|
);
|
||||||
|
if (!backupPrivateKey) throw new Error("Failed to find backup key");
|
||||||
|
|
||||||
|
return { message: "Successfully updated backup private key", backupPrivateKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { registerMfaRouter } from "./mfa-router";
|
||||||
|
|
||||||
|
export const registerV2Routes = async (server: FastifyZodProvider) => {
|
||||||
|
await server.register(registerMfaRouter, { prefix: "/auth" });
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { AuthTokenType } from "@app/services/auth/auth-signup-type";
|
||||||
|
|
||||||
|
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
server.addHook("preParsing", async (req, res) => {
|
||||||
|
const authorizationHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
|
||||||
|
res.status(401).send({ error: "Missing bearer token" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = authorizationHeader.split(" ")[1];
|
||||||
|
if (!token) res.status(401).send({ error: "Missing bearer token" });
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(token, cfg.JWT_AUTH_SECRET) as JwtPayload;
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN)
|
||||||
|
throw new Error("Unauthorized access");
|
||||||
|
|
||||||
|
const user = await server.store.user.getUserById(decodedToken.userId);
|
||||||
|
if (!user) throw new Error("User not found");
|
||||||
|
req.mfa.userId = user.id;
|
||||||
|
req.mfa.user = user;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/mfa/send",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.login.resendMfaToken(req.mfa.userId);
|
||||||
|
return { message: "Successfully send new mfa code" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/mfa/verify",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
mfaToken: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
encryptionVersion: z.number().default(1).optional(),
|
||||||
|
protectedKey: z.string(),
|
||||||
|
protectedKeyIV: z.string(),
|
||||||
|
protectedKeyTag: z.string(),
|
||||||
|
publicKey: z.string(),
|
||||||
|
encryptedPrivateKey: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
token: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const { user, token } = await server.services.login.verifyMfaToken({
|
||||||
|
userAgent,
|
||||||
|
ip: req.realIp,
|
||||||
|
userId: req.mfa.userId,
|
||||||
|
mfaToken: req.body.mfaToken
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setCookie("jid", token.refresh, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return { token: token.access, ...user };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { registerLoginRouter } from "./login-router";
|
||||||
|
import { registerSignupRouter } from "./signup-router";
|
||||||
|
|
||||||
|
export const registerV3Routes = async (server: FastifyZodProvider) => {
|
||||||
|
await server.register(registerSignupRouter, { prefix: "/signup" });
|
||||||
|
await server.register(registerLoginRouter, { prefix: "/auth" });
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/login1",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email().trim(),
|
||||||
|
providerAuthToken: z.string().trim().optional(),
|
||||||
|
clientPublicKey: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serverPublicKey: z.string(),
|
||||||
|
salt: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serverPublicKey, salt } = await server.services.login.loginGenServerPublicKey({
|
||||||
|
email: req.body.email,
|
||||||
|
clientPublicKey: req.body.clientPublicKey,
|
||||||
|
providerAuthToken: req.body.providerAuthToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return { serverPublicKey, salt };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/login2",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email().trim(),
|
||||||
|
providerAuthToken: z.string().trim().optional(),
|
||||||
|
clientProof: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.discriminatedUnion("mfaEnabled", [
|
||||||
|
z.object({ mfaEnabled: z.literal(true), token: z.string() }),
|
||||||
|
z.object({
|
||||||
|
mfaEnabled: z.literal(false),
|
||||||
|
encryptionVersion: z.number().default(1).optional(),
|
||||||
|
protectedKey: z.string(),
|
||||||
|
protectedKeyIV: z.string(),
|
||||||
|
protectedKeyTag: z.string(),
|
||||||
|
publicKey: z.string(),
|
||||||
|
encryptedPrivateKey: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
token: z.string()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const data = await server.services.login.loginExchangeClientProof({
|
||||||
|
email: req.body.email,
|
||||||
|
ip: req.realIp,
|
||||||
|
userAgent,
|
||||||
|
clientProof: req.body.clientProof
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.isMfaEnabled) {
|
||||||
|
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setCookie("jid", data.token.refresh, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: data.user.encryptionVersion,
|
||||||
|
token: data.token.access,
|
||||||
|
publicKey: data.user.publicKey,
|
||||||
|
encryptedPrivateKey: data.user.encryptedPrivateKey,
|
||||||
|
iv: data.user.iv,
|
||||||
|
tag: data.user.tag,
|
||||||
|
protectedKey: data.user.protectedKey,
|
||||||
|
protectedKeyIV: data.user.protectedKeyIV,
|
||||||
|
protectedKeyTag: data.user.protectedKeyTag
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { UserSchema } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/email/signup",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.signup.beginEmailSignupProcess(req.body.email);
|
||||||
|
return { message: `Sent an email verification code to ${req.body.email}` };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/email/verify",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email().trim(),
|
||||||
|
code: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
user: UserSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { token, user } = await server.services.signup.verifyEmailSignup(
|
||||||
|
req.body.email,
|
||||||
|
req.body.code
|
||||||
|
);
|
||||||
|
return { message: "Successfuly verified email", token, user };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/complete-account/signup",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
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(),
|
||||||
|
organizationName: z.string().trim(),
|
||||||
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
|
attributionSource: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
user: UserSchema,
|
||||||
|
token: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const { user, accessToken, refreshToken } =
|
||||||
|
await server.services.signup.completeEmailAccountSignup({
|
||||||
|
...req.body,
|
||||||
|
ip: req.realIp,
|
||||||
|
userAgent
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setCookie("jid", refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
// TODO(akhilmhdh-pg): add telemetry service
|
||||||
|
|
||||||
|
return { message: "Successfully set up account", user, token: accessToken };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TBackupPrivateKey, TUser, TUserEncryptionKey } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export type TAuthDalFactory = ReturnType<typeof authDalFactory>;
|
||||||
|
|
||||||
|
export const authDalFactory = (db: TDbClient) => {
|
||||||
|
// getters
|
||||||
|
const getUserByEmail = async (email: string): Promise<TUser | undefined> =>
|
||||||
|
db(TableName.Users).where({ email }).select("*").first();
|
||||||
|
|
||||||
|
const getUserById = async (userId: string): Promise<TUser | undefined> =>
|
||||||
|
db(TableName.Users).where({ id: userId }).select("*").first();
|
||||||
|
|
||||||
|
const getUserEncKeyByEmail = async (email: string) =>
|
||||||
|
db(TableName.Users)
|
||||||
|
.where({ email })
|
||||||
|
.join(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.Users}.id`,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const getUserEncKeyByUserId = async (userId: string) =>
|
||||||
|
db(TableName.Users)
|
||||||
|
.where({ id: userId })
|
||||||
|
.join(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.Users}.id`,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const getBackupPrivateKeyByUserId = async (userId: string) =>
|
||||||
|
db(TableName.BackupPrivateKey).where({ userId }).first("*");
|
||||||
|
|
||||||
|
// all inserts and updates
|
||||||
|
const createUser = async (
|
||||||
|
email: string,
|
||||||
|
data: Partial<TUser> = {}
|
||||||
|
): Promise<TUser | undefined> => {
|
||||||
|
const [user] = await db(TableName.Users)
|
||||||
|
.insert({ email, ...data })
|
||||||
|
.returning("*");
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = async (
|
||||||
|
email: string,
|
||||||
|
data: Partial<TUser> = {}
|
||||||
|
): Promise<TUser | undefined> => {
|
||||||
|
const [user] = await db(TableName.Users)
|
||||||
|
.where({ email })
|
||||||
|
.update({ ...data })
|
||||||
|
.returning("*");
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUserById = async (
|
||||||
|
id: string,
|
||||||
|
data: Partial<TUser> = {},
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TUser | undefined> => {
|
||||||
|
const [user] = await (tx ? tx(TableName.Users) : db(TableName.Users))
|
||||||
|
.where({ id })
|
||||||
|
.update({ ...data })
|
||||||
|
.returning("*");
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUserEncryptionByUserId = async (
|
||||||
|
userId: string,
|
||||||
|
data: Partial<TUserEncryptionKey> = {},
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TUserEncryptionKey | undefined> => {
|
||||||
|
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
|
||||||
|
.where({ userId })
|
||||||
|
.update({ ...data })
|
||||||
|
.returning("*");
|
||||||
|
return userEnc;
|
||||||
|
};
|
||||||
|
|
||||||
|
// all upserts
|
||||||
|
const upsertUserEncryptionKey = async (
|
||||||
|
userId: string,
|
||||||
|
data: Partial<TUserEncryptionKey>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
|
||||||
|
// if user insert make sure to pass all required data
|
||||||
|
.insert({ userId, ...data } as TUserEncryptionKey)
|
||||||
|
.onConflict("userId")
|
||||||
|
.merge()
|
||||||
|
.returning("*");
|
||||||
|
return userEnc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertBackupKey = async (
|
||||||
|
userId: string,
|
||||||
|
data: Partial<TBackupPrivateKey>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TBackupPrivateKey | undefined> => {
|
||||||
|
const [backupKey] = await (tx ? tx(TableName.BackupPrivateKey) : db(TableName.BackupPrivateKey))
|
||||||
|
.insert({ userId, ...data, updatedAt: new Date().toUTCString() } as TBackupPrivateKey)
|
||||||
|
.onConflict("userId")
|
||||||
|
.merge()
|
||||||
|
.returning("*");
|
||||||
|
return backupKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
transaction: async <T>(cb: (tx: Knex) => T) =>
|
||||||
|
db.transaction(async (trx) => {
|
||||||
|
const res = await cb(trx);
|
||||||
|
return res;
|
||||||
|
}),
|
||||||
|
getUserByEmail,
|
||||||
|
getUserById,
|
||||||
|
getUserEncKeyByEmail,
|
||||||
|
getUserEncKeyByUserId,
|
||||||
|
getBackupPrivateKeyByUserId,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
updateUserById,
|
||||||
|
updateUserEncryptionByUserId,
|
||||||
|
upsertUserEncryptionKey,
|
||||||
|
upsertBackupKey
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { AuthMethod, TUser } from "@app/db/schemas";
|
||||||
|
import { UserDeviceSchema } from "@app/db/schemas/user";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TTokenServiceFactory } from "../token/token-service";
|
||||||
|
import { TokenType } from "../token/token-types";
|
||||||
|
import { TAuthDalFactory } from "./auth-dal";
|
||||||
|
import {
|
||||||
|
TLoginClientProofDTO,
|
||||||
|
TLoginGenServerPublicKeyDTO,
|
||||||
|
TVerifyMfaTokenDTO
|
||||||
|
} from "./auth-login-type";
|
||||||
|
import { AuthTokenType } from "./auth-signup-type";
|
||||||
|
|
||||||
|
const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuthToken?: string) => {
|
||||||
|
if (!providerAuthToken) return false;
|
||||||
|
const decodedToken = jwt.verify(providerAuthToken, jwtSecret) as jwt.JwtPayload;
|
||||||
|
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) return false;
|
||||||
|
if (decodedToken.email !== email) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TAuthLoginServiceFactoryDep = {
|
||||||
|
authDal: TAuthDalFactory;
|
||||||
|
tokenService: TTokenServiceFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
|
export const authLoginServiceFactory = ({
|
||||||
|
authDal,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
|
/*
|
||||||
|
* Private
|
||||||
|
* Not exported. This is to update user device list
|
||||||
|
* If new device is found. Will be saved and a mail will be send
|
||||||
|
*/
|
||||||
|
const updateUserDeviceSession = async (user: TUser, ip: string, userAgent: string) => {
|
||||||
|
const devices = await UserDeviceSchema.parseAsync(JSON.parse(user.devices || "[]"));
|
||||||
|
const isDeviceSeen = devices.some(
|
||||||
|
(device) => device.ip === ip && device.userAgent === userAgent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDeviceSeen) {
|
||||||
|
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||||
|
await authDal.updateUserById(user.id, { devices: JSON.stringify(newDeviceList) });
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.NewDeviceJoin,
|
||||||
|
subjectLine: "Successful login from new device",
|
||||||
|
recipients: [user.email],
|
||||||
|
substitutions: {
|
||||||
|
email: user.email,
|
||||||
|
timestamp: new Date().toString(),
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Private
|
||||||
|
* Send mfa code via email
|
||||||
|
* */
|
||||||
|
const sendUserMfaCode = async (user: TUser) => {
|
||||||
|
const code = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.EmailMfa,
|
||||||
|
subjectLine: "Infisical MFA code",
|
||||||
|
recipients: [user.email],
|
||||||
|
substitutions: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Private
|
||||||
|
* Check user device and send mail if new device
|
||||||
|
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
||||||
|
*/
|
||||||
|
const generateUserTokens = async (user: TUser, ip: string, userAgent: string) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
await updateUserDeviceSession(user, ip, userAgent);
|
||||||
|
const tokenSession = await tokenService.getUserTokenSession({
|
||||||
|
userAgent,
|
||||||
|
ip,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||||
|
userId: user.id,
|
||||||
|
tokenVersionId: tokenSession.id,
|
||||||
|
accessVersion: tokenSession.accessVersion
|
||||||
|
},
|
||||||
|
cfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||||
|
userId: user.id,
|
||||||
|
tokenVersionId: tokenSession.id,
|
||||||
|
refreshVersion: tokenSession.refreshVersion
|
||||||
|
},
|
||||||
|
cfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { access: accessToken, refresh: refreshToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 1 of login. To get server public key in exchange of client public key
|
||||||
|
*/
|
||||||
|
const loginGenServerPublicKey = async ({
|
||||||
|
email,
|
||||||
|
providerAuthToken,
|
||||||
|
clientPublicKey
|
||||||
|
}: TLoginGenServerPublicKeyDTO) => {
|
||||||
|
const user = await authDal.getUserEncKeyByEmail(email);
|
||||||
|
if (!user || (user && !user.isAccepted)) {
|
||||||
|
throw new Error("Failed to find user");
|
||||||
|
}
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!user.authMethods?.includes(AuthMethod.EMAIL) &&
|
||||||
|
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid authorization request");
|
||||||
|
}
|
||||||
|
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier);
|
||||||
|
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, {
|
||||||
|
clientPublicKey,
|
||||||
|
serverPrivateKey: serverSrpKey.privateKey
|
||||||
|
});
|
||||||
|
if (!userEncKeys) throw new Error("Failed to update encryption key");
|
||||||
|
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 2 of login. Pass the client proof and with multi factor setup handle the required steps
|
||||||
|
*/
|
||||||
|
const loginExchangeClientProof = async ({
|
||||||
|
email,
|
||||||
|
clientProof,
|
||||||
|
providerAuthToken,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: TLoginClientProofDTO) => {
|
||||||
|
const user = await authDal.getUserEncKeyByEmail(email);
|
||||||
|
if (!user) throw new Error("Failed to find user");
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!user.authMethods?.includes(AuthMethod.EMAIL) &&
|
||||||
|
!isValidProviderAuthToken(email, cfg.JWT_AUTH_SECRET, providerAuthToken)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid authorization request");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.serverPrivateKey || !user.clientPublicKey)
|
||||||
|
throw new Error("Failed to authenticate. Try again?");
|
||||||
|
const isValidClientProof = await srpCheckClientProof(
|
||||||
|
user.salt,
|
||||||
|
user.verifier,
|
||||||
|
user.serverPrivateKey,
|
||||||
|
user.clientPublicKey,
|
||||||
|
clientProof
|
||||||
|
);
|
||||||
|
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||||
|
|
||||||
|
await authDal.updateUserEncryptionByUserId(user.id, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
});
|
||||||
|
// send multi factor auth token if they it enabled
|
||||||
|
if (user.isMfaEnabled) {
|
||||||
|
const mfaToken = jwt.sign(
|
||||||
|
{ authTokenType: AuthTokenType.MFA_TOKEN, userId: user.id },
|
||||||
|
cfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: cfg.JWT_MFA_LIFETIME }
|
||||||
|
);
|
||||||
|
await sendUserMfaCode(user);
|
||||||
|
|
||||||
|
return { isMfaEnabled: true, token: mfaToken } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateUserTokens(user, ip, userAgent);
|
||||||
|
return { token, isMfaEnabled: false, user } as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Multi factor authentication re-send code, Get user id from token
|
||||||
|
* saved in frontend
|
||||||
|
*/
|
||||||
|
const resendMfaToken = async (userId: string) => {
|
||||||
|
const user = await authDal.getUserById(userId);
|
||||||
|
if (!user) return;
|
||||||
|
await sendUserMfaCode(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Multi factor authentication verification of code
|
||||||
|
* Third step of login in which user completes with mfa
|
||||||
|
* */
|
||||||
|
const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent }: TVerifyMfaTokenDTO) => {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
userId,
|
||||||
|
code: mfaToken
|
||||||
|
});
|
||||||
|
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||||
|
if (!user) throw new Error("Failed to authenticate user");
|
||||||
|
|
||||||
|
const token = await generateUserTokens(user, ip, userAgent);
|
||||||
|
return { token, user };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
||||||
|
* as there number is behind
|
||||||
|
* */
|
||||||
|
const logout = async (userId: string, sessionId: string) => {
|
||||||
|
await tokenService.clearTokenSessionById(userId, sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginGenServerPublicKey,
|
||||||
|
loginExchangeClientProof,
|
||||||
|
logout,
|
||||||
|
resendMfaToken,
|
||||||
|
verifyMfaToken
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export type TLoginGenServerPublicKeyDTO = {
|
||||||
|
email: string;
|
||||||
|
clientPublicKey: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLoginClientProofDTO = {
|
||||||
|
email: string;
|
||||||
|
clientProof: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyMfaTokenDTO = {
|
||||||
|
userId: string;
|
||||||
|
mfaToken: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TTokenServiceFactory } from "../token/token-service";
|
||||||
|
import { TokenType } from "../token/token-types";
|
||||||
|
import { TAuthDalFactory } from "./auth-dal";
|
||||||
|
import {
|
||||||
|
TChangePasswordDTO,
|
||||||
|
TCreateBackupPrivateKeyDTO,
|
||||||
|
TResetPasswordViaBackupKeyDTO
|
||||||
|
} from "./auth-password-type";
|
||||||
|
import { AuthTokenType } from "./auth-signup-type";
|
||||||
|
|
||||||
|
type TAuthPasswordServiceFactoryDep = {
|
||||||
|
authDal: TAuthDalFactory;
|
||||||
|
tokenService: TTokenServiceFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
||||||
|
export const authPaswordServiceFactory = ({
|
||||||
|
authDal,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
}: TAuthPasswordServiceFactoryDep) => {
|
||||||
|
/*
|
||||||
|
* Pre setup for pass change with srp protocol
|
||||||
|
* Gets srp server user salt and server public key
|
||||||
|
*/
|
||||||
|
const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
|
||||||
|
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||||
|
if (!user) throw new Error("Failed to find user");
|
||||||
|
|
||||||
|
const serverSrpKey = await generateSrpServerKey(user.salt, user.verifier);
|
||||||
|
const userEncKeys = await authDal.updateUserEncryptionByUserId(user.id, {
|
||||||
|
clientPublicKey,
|
||||||
|
serverPrivateKey: serverSrpKey.privateKey
|
||||||
|
});
|
||||||
|
if (!userEncKeys) throw new Error("Failed to update encryption key");
|
||||||
|
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change password to new pass
|
||||||
|
* */
|
||||||
|
const changePassword = async ({
|
||||||
|
userId,
|
||||||
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
tokenVersionId
|
||||||
|
}: TChangePasswordDTO) => {
|
||||||
|
const userEnc = await authDal.getUserEncKeyByUserId(userId);
|
||||||
|
if (!userEnc) throw new Error("Failed to find user");
|
||||||
|
|
||||||
|
await authDal.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
});
|
||||||
|
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey)
|
||||||
|
throw new Error("Failed to authenticate. Try again?");
|
||||||
|
const isValidClientProof = await srpCheckClientProof(
|
||||||
|
userEnc.salt,
|
||||||
|
userEnc.verifier,
|
||||||
|
userEnc.serverPrivateKey,
|
||||||
|
userEnc.clientPublicKey,
|
||||||
|
clientProof
|
||||||
|
);
|
||||||
|
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||||
|
|
||||||
|
await authDal.updateUserEncryptionByUserId(userId, {
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tokenVersionId) {
|
||||||
|
await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Email password reset flow via email. Step 1 send email
|
||||||
|
*/
|
||||||
|
const sendPasswordResetEmail = async (email: string) => {
|
||||||
|
const user = await authDal.getUserByEmail(email);
|
||||||
|
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
||||||
|
if (!user || (user && !user.isAccepted)) return;
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.ResetPassword,
|
||||||
|
recipients: [email],
|
||||||
|
subjectLine: "Infisical password reset",
|
||||||
|
substitutions: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 2 of reset password. Verify the token and inject a temp token to reset password
|
||||||
|
* */
|
||||||
|
const verifyPasswordResetEmail = async (email: string, code: string) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
const user = await authDal.getUserByEmail(email);
|
||||||
|
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
||||||
|
if (!user || (user && !user.isAccepted)) {
|
||||||
|
throw new Error("Failed email verification for pass reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
|
userId: user.id,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
cfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { token, user };
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Reset password of a user via backup key
|
||||||
|
* */
|
||||||
|
const resetPasswordByBackupKey = async (
|
||||||
|
userId: string,
|
||||||
|
{
|
||||||
|
encryptedPrivateKey,
|
||||||
|
protectedKeyTag,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag
|
||||||
|
}: TResetPasswordViaBackupKeyDTO
|
||||||
|
) => {
|
||||||
|
await authDal.updateUserEncryptionByUserId(userId, {
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* backup key creation to give user's their access back when lost their password
|
||||||
|
* this also needs to do the generateServerPubKey function to be executed first
|
||||||
|
* then only client proof can be verified
|
||||||
|
* */
|
||||||
|
const createBackupPrivateKey = async ({
|
||||||
|
clientProof,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
userId
|
||||||
|
}: TCreateBackupPrivateKeyDTO) => {
|
||||||
|
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||||
|
if (!user || (user && !user.isAccepted)) {
|
||||||
|
throw new Error("Failed to find user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.clientPublicKey || !user.serverPrivateKey)
|
||||||
|
throw new Error("failed to create backup key");
|
||||||
|
const isValidClientProff = await srpCheckClientProof(
|
||||||
|
user.salt,
|
||||||
|
user.verifier,
|
||||||
|
user.serverPrivateKey,
|
||||||
|
user.clientPublicKey,
|
||||||
|
clientProof
|
||||||
|
);
|
||||||
|
if (!isValidClientProff) throw new Error("failed to create backup key");
|
||||||
|
const backup = await authDal.transaction(async (tx) => {
|
||||||
|
const backupKey = await authDal.upsertBackupKey(user.id, {
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
await authDal.updateUserEncryptionByUserId(
|
||||||
|
user.id,
|
||||||
|
{
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return backupKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
return backup;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return user back up
|
||||||
|
* */
|
||||||
|
const getBackupPrivateKeyOfUser = async (userId: string) => {
|
||||||
|
const user = await authDal.getUserEncKeyByUserId(userId);
|
||||||
|
if (!user || (user && !user.isAccepted)) {
|
||||||
|
throw new Error("Failed to find user");
|
||||||
|
}
|
||||||
|
const backupKey = await authDal.getBackupPrivateKeyByUserId(userId);
|
||||||
|
if (!backupKey) throw new Error("Failed to find user backup key");
|
||||||
|
|
||||||
|
return backupKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateServerPubKey,
|
||||||
|
changePassword,
|
||||||
|
resetPasswordByBackupKey,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
verifyPasswordResetEmail,
|
||||||
|
createBackupPrivateKey,
|
||||||
|
getBackupPrivateKeyOfUser
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export type TChangePasswordDTO = {
|
||||||
|
userId: string;
|
||||||
|
clientProof: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
tokenVersionId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TResetPasswordViaBackupKeyDTO = {
|
||||||
|
userId: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateBackupPrivateKeyDTO = {
|
||||||
|
userId: string;
|
||||||
|
clientProof: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
salt: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
verifier: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { AuthMethod } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
|
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TTokenServiceFactory } from "../token/token-service";
|
||||||
|
import { TokenType } from "../token/token-types";
|
||||||
|
import { TAuthDalFactory } from "./auth-dal";
|
||||||
|
import { AuthTokenType, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||||
|
|
||||||
|
type TAuthSignupDep = {
|
||||||
|
authDal: TAuthDalFactory;
|
||||||
|
tokenService: TTokenServiceFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
||||||
|
export const authSignupServiceFactory = ({
|
||||||
|
authDal,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
}: TAuthSignupDep) => {
|
||||||
|
// first step of signup. create user and send email
|
||||||
|
const beginEmailSignupProcess = async (email: string) => {
|
||||||
|
const isEmailInvalid = await isDisposableEmail(email);
|
||||||
|
if (isEmailInvalid) {
|
||||||
|
throw new Error("Provided a disposable email");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await authDal.getUserByEmail(email);
|
||||||
|
if (user && user.isAccepted) {
|
||||||
|
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
|
||||||
|
throw new Error("Failed to send verification code for complete account");
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
user = await authDal.createUser(email, { authMethods: [AuthMethod.EMAIL] });
|
||||||
|
}
|
||||||
|
if (!user) throw new Error("Failed to create user");
|
||||||
|
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_CONFIRMATION,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.EmailVerification,
|
||||||
|
subjectLine: "Infisical confirmation code",
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
code: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyEmailSignup = async (email: string, code: string) => {
|
||||||
|
const user = await authDal.getUserByEmail(email);
|
||||||
|
if (!user || (user && user.isAccepted)) {
|
||||||
|
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
|
||||||
|
throw new Error("Failed to send verification code for complete account");
|
||||||
|
}
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_CONFIRMATION,
|
||||||
|
userId: user.id,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate jwt token this is a temporary token
|
||||||
|
const jwtToken = tokenService.createJwtToken(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||||
|
userId: user.id.toString()
|
||||||
|
},
|
||||||
|
appCfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { user, token: jwtToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeEmailAccountSignup = async ({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
// providerAuthToken,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
publicKey,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
// organizationName,
|
||||||
|
// attributionSource,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: TCompleteAccountSignupDTO) => {
|
||||||
|
const user = await authDal.getUserByEmail(email);
|
||||||
|
if (!user || (user && user.isAccepted)) {
|
||||||
|
throw new Error("Failed to complete account for complete user");
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateduser = await authDal.transaction(async (tx) => {
|
||||||
|
const us = await authDal.updateUserById(
|
||||||
|
user.id,
|
||||||
|
{ firstName, lastName, isAccepted: true },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (!us) throw new Error("User not found");
|
||||||
|
const userEncKey = await authDal.upsertUserEncryptionKey(
|
||||||
|
us.id,
|
||||||
|
{
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
publicKey,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return { info: us, key: userEncKey };
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(akhilmhdh-pg): add default org memberships
|
||||||
|
const tokenSession = await tokenService.getUserTokenSession({
|
||||||
|
userAgent,
|
||||||
|
ip,
|
||||||
|
userId: updateduser.info.id
|
||||||
|
});
|
||||||
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const accessToken = tokenService.createJwtToken(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||||
|
userId: updateduser.info.id,
|
||||||
|
tokenVersionId: tokenSession.id,
|
||||||
|
accessVersion: tokenSession.accessVersion
|
||||||
|
},
|
||||||
|
appCfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshToken = tokenService.createJwtToken(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||||
|
userId: updateduser.info.id,
|
||||||
|
tokenVersionId: tokenSession.id,
|
||||||
|
refreshVersion: tokenSession.refreshVersion
|
||||||
|
},
|
||||||
|
appCfg.JWT_AUTH_SECRET,
|
||||||
|
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { user: updateduser.info, accessToken, refreshToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
beginEmailSignupProcess,
|
||||||
|
verifyEmailSignup,
|
||||||
|
completeEmailAccountSignup
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export enum AuthTokenType {
|
||||||
|
ACCESS_TOKEN = "accessToken",
|
||||||
|
REFRESH_TOKEN = "refreshToken",
|
||||||
|
SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim
|
||||||
|
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
|
||||||
|
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
|
||||||
|
API_KEY = "apiKey",
|
||||||
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
|
SERVICE_REFRESH_TOKEN = "serviceRefreshToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthMode {
|
||||||
|
JWT = "jwt",
|
||||||
|
SERVICE_TOKEN = "serviceToken",
|
||||||
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
|
API_KEY = "apiKey",
|
||||||
|
API_KEY_V2 = "apiKeyV2"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCompleteAccountSignupDTO = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
organizationName: string;
|
||||||
|
providerAuthToken?: string | null;
|
||||||
|
attributionSource?: string | undefined;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
import { createTransport } from "nodemailer";
|
||||||
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
export type TSmtpConfig = SMTPTransport.Options;
|
||||||
|
export type TSmtpSendMail = {
|
||||||
|
template: SmtpTemplates;
|
||||||
|
subjectLine: string;
|
||||||
|
recipients: string[];
|
||||||
|
substitutions: unknown;
|
||||||
|
};
|
||||||
|
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
|
||||||
|
|
||||||
|
export enum SmtpTemplates {
|
||||||
|
EmailVerification = "emailVerification.handlebars",
|
||||||
|
EmailMfa = "emailMfa.handlebars",
|
||||||
|
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||||
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
|
ResetPassword = "passwordReset.handlebars",
|
||||||
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
|
WorkspaceInvite = "workspaceInvitation.handlebars"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SmtpHost {
|
||||||
|
Sendgrid = "smtp.sendgrid.net",
|
||||||
|
Mailgun = "smtp.mailgun.org",
|
||||||
|
SocketLabs = "smtp.sockerlabs.com",
|
||||||
|
Zohomail = "smtp.zoho.com",
|
||||||
|
Gmail = "smtp.gmail.com",
|
||||||
|
Office365 = "smtp.office365.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => {
|
||||||
|
if (!secure) return { secure: false };
|
||||||
|
if (!host) return { secure: true };
|
||||||
|
|
||||||
|
if (host === SmtpHost.Sendgrid) {
|
||||||
|
return { requireTLS: true };
|
||||||
|
}
|
||||||
|
if (host.includes("amazonaws.com")) {
|
||||||
|
return { tls: { ciphers: "TLSv1.2" } };
|
||||||
|
}
|
||||||
|
return { requireTLS: true, tls: { ciphers: "TLSv1.2" } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||||
|
const smtp = createTransport({ ...cfg, ...getTlsOption(cfg.host, cfg.secure) });
|
||||||
|
const isSmtpOn = Boolean(cfg.host);
|
||||||
|
|
||||||
|
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||||
|
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
|
||||||
|
const temp = handlebars.compile(html);
|
||||||
|
const htmlToSend = temp(substitutions);
|
||||||
|
if (isSmtpOn) {
|
||||||
|
await smtp.sendMail({
|
||||||
|
from: cfg.from,
|
||||||
|
to: recipients.join(", "),
|
||||||
|
subject: subjectLine,
|
||||||
|
html: htmlToSend
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.info("SMTP is not configured. Outputting it in terminal");
|
||||||
|
logger.info({
|
||||||
|
from: cfg.from,
|
||||||
|
to: recipients.join(", "),
|
||||||
|
subject: subjectLine,
|
||||||
|
html: htmlToSend
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { sendMail };
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>MFA Code</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>Sign in attempt requires further verification</h2>
|
||||||
|
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
|
||||||
|
<h2>{{code}}</h2>
|
||||||
|
<p>The MFA code will be valid for 2 minutes.</p>
|
||||||
|
<p>Not you? Contact Infisical or your administrator immediately.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Incident alert: secrets potentially leaked</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
|
||||||
|
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||||
|
|
||||||
|
<p>If these are production secrets, please rotate them immediately.</p>
|
||||||
|
|
||||||
|
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||||
|
href="https://app.infisical.com/">Infisical
|
||||||
|
dashboard</a>.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Successful login for {{email}} from new device</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<p>We're verifying a recent login for {{email}}:</p>
|
||||||
|
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
||||||
|
<p><strong>IP address</strong>: {{ip}}</p>
|
||||||
|
<p><strong>User agent</strong>: {{userAgent}}</p>
|
||||||
|
<p>If you believe that this login is suspicious, please contact Infisical or reset your password immediately.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Organization Invitation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Join your organization on Infisical</h2>
|
||||||
|
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
|
||||||
|
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
|
||||||
|
<h3>What is Infisical?</h3>
|
||||||
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Account Recovery</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Reset your password</h2>
|
||||||
|
<p>Someone requested a password reset.</p>
|
||||||
|
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||||
|
<p>If you didn't initiate this request, please contact us immediately at [email protected]</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Incident alert: secret leaked</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||||
|
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||||
|
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
||||||
|
by {{pusher_name}} ({{pusher_email}}). If
|
||||||
|
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
|
||||||
|
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
|
||||||
|
|
||||||
|
<p>If these are production secrets, please rotate them immediately.</p>
|
||||||
|
|
||||||
|
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||||
|
href="https://app.infisical.com/">Infisical
|
||||||
|
dashboard</a>.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Project Invitation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Join your team on Infisical</h2>
|
||||||
|
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
|
||||||
|
<a href="{{callback_url}}">Join now</a>
|
||||||
|
<h3>What is Infisical?</h3>
|
||||||
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TToken } from "@app/db/schemas";
|
||||||
|
import { TTokenSession } from "@app/db/schemas/token-session";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TDeleteTokenForUserDalDTO,
|
||||||
|
TGetTokenForUserDalDTO,
|
||||||
|
TUpsertTokenForUserDalDTO
|
||||||
|
} from "./token-types";
|
||||||
|
|
||||||
|
export type TTokenDalConfig = {};
|
||||||
|
|
||||||
|
export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>;
|
||||||
|
|
||||||
|
export const tokenDalFactory = (db: TDbClient) => {
|
||||||
|
const upsertTokenForUser = async ({
|
||||||
|
tokenHash,
|
||||||
|
expiresAt,
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
triesLeft
|
||||||
|
}: TUpsertTokenForUserDalDTO): Promise<TToken | undefined> => {
|
||||||
|
const token = await db.transaction(async (tx) => {
|
||||||
|
await tx(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
|
||||||
|
const [newToken] = await tx(TableName.AuthTokens)
|
||||||
|
.insert({ tokenHash, expiresAt: expiresAt.toUTCString(), type, userId, triesLeft })
|
||||||
|
.returning("*");
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTokenForUser = async ({
|
||||||
|
userId,
|
||||||
|
type
|
||||||
|
}: TGetTokenForUserDalDTO): Promise<TToken | undefined> =>
|
||||||
|
db(TableName.AuthTokens).where({ userId, type }).first();
|
||||||
|
|
||||||
|
const deleteTokenForUser = async ({
|
||||||
|
userId,
|
||||||
|
type
|
||||||
|
}: TDeleteTokenForUserDalDTO): Promise<TToken[] | undefined> =>
|
||||||
|
db(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
|
||||||
|
|
||||||
|
const decrementTriesField = async ({
|
||||||
|
userId,
|
||||||
|
type
|
||||||
|
}: TDeleteTokenForUserDalDTO): Promise<void> => {
|
||||||
|
await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTokenSession = async (
|
||||||
|
userId: string,
|
||||||
|
ip: string,
|
||||||
|
userAgent: string
|
||||||
|
): Promise<TTokenSession | undefined> =>
|
||||||
|
db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first();
|
||||||
|
|
||||||
|
const insertTokenSession = async (
|
||||||
|
userId: string,
|
||||||
|
ip: string,
|
||||||
|
userAgent: string
|
||||||
|
): Promise<TTokenSession | undefined> => {
|
||||||
|
const [session] = await db(TableName.AuthTokenSession)
|
||||||
|
.insert({
|
||||||
|
userId,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
accessVersion: 1,
|
||||||
|
refreshVersion: 1,
|
||||||
|
lastUsed: new Date().toUTCString()
|
||||||
|
})
|
||||||
|
.returning("*");
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
const incrementVersion = async (
|
||||||
|
userId: string,
|
||||||
|
sessionId: string
|
||||||
|
): Promise<TTokenSession | undefined> => {
|
||||||
|
const [session] = await db(TableName.AuthTokenSession)
|
||||||
|
.where({ userId, id: sessionId })
|
||||||
|
.increment("accessVersion", 1)
|
||||||
|
.increment("refreshVersion", 1)
|
||||||
|
.returning("*");
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
upsertTokenForUser,
|
||||||
|
getTokenForUser,
|
||||||
|
deleteTokenForUser,
|
||||||
|
decrementTriesField,
|
||||||
|
getTokenSession,
|
||||||
|
insertTokenSession,
|
||||||
|
incrementVersion
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt, { SignOptions } from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { TToken } from "@app/db/schemas";
|
||||||
|
import { TTokenSession } from "@app/db/schemas/token-session";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
import { TTokenDalFactory } from "./token-dal";
|
||||||
|
import {
|
||||||
|
TCreateTokenForUserDTO,
|
||||||
|
TIssueAuthTokenDTO,
|
||||||
|
TokenType,
|
||||||
|
TValidateTokenForUserDTO
|
||||||
|
} from "./token-types";
|
||||||
|
|
||||||
|
type TTokenServiceFactoryDep = {
|
||||||
|
tokenDal: TTokenDalFactory;
|
||||||
|
// adjust the expiry from env through here
|
||||||
|
};
|
||||||
|
export type TTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||||
|
|
||||||
|
export const getTokenConfig = (tokenType: TokenType) => {
|
||||||
|
// generate random token based on specified token use-case
|
||||||
|
// type [type]
|
||||||
|
switch (tokenType) {
|
||||||
|
case TokenType.TOKEN_EMAIL_CONFIRMATION: {
|
||||||
|
// 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: {
|
||||||
|
// generate random 6-digit code
|
||||||
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
|
const triesLeft = 5;
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 300000);
|
||||||
|
return { token, triesLeft, expiresAt };
|
||||||
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_ORG_INVITATION: {
|
||||||
|
// generate random hex
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_PASSWORD_RESET: {
|
||||||
|
// generate random hex
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date();
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tokenServiceFactory = ({ tokenDal }: TTokenServiceFactoryDep) => {
|
||||||
|
const createTokenForUser = async ({ type, userId }: TCreateTokenForUserDTO) => {
|
||||||
|
const { token, ...tkCfg } = getTokenConfig(type);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS);
|
||||||
|
await tokenDal.upsertTokenForUser({
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
expiresAt: tkCfg.expiresAt,
|
||||||
|
tokenHash,
|
||||||
|
triesLeft: tkCfg?.triesLeft
|
||||||
|
});
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateTokenForUser = async ({
|
||||||
|
type,
|
||||||
|
userId,
|
||||||
|
code
|
||||||
|
}: TValidateTokenForUserDTO): Promise<TToken | undefined> => {
|
||||||
|
const token = await tokenDal.getTokenForUser({ type, userId });
|
||||||
|
// validate token
|
||||||
|
if (!token) throw new Error("Failed to find token");
|
||||||
|
if (token?.expiresAt && new Date(token.expiresAt) < new Date()) {
|
||||||
|
await tokenDal.deleteTokenForUser({ type, userId });
|
||||||
|
throw new Error("Token expired. Please try again");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidToken = await bcrypt.compare(code, token.tokenHash);
|
||||||
|
if (!isValidToken) {
|
||||||
|
if (token?.triesLeft) {
|
||||||
|
if (token.triesLeft === 1) {
|
||||||
|
await tokenDal.deleteTokenForUser({ type, userId });
|
||||||
|
} else {
|
||||||
|
await tokenDal.decrementTriesField({ type, userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedToken = await tokenDal.deleteTokenForUser({ type, userId });
|
||||||
|
return deletedToken?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserTokenSession = async ({
|
||||||
|
userId,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: TIssueAuthTokenDTO): Promise<TTokenSession | undefined> => {
|
||||||
|
let session = await tokenDal.getTokenSession(userId, ip, userAgent);
|
||||||
|
if (!session) {
|
||||||
|
session = await tokenDal.insertTokenSession(userId, ip, userAgent);
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearTokenSessionById = async (
|
||||||
|
userId: string,
|
||||||
|
sessionId: string
|
||||||
|
): Promise<TTokenSession | undefined> => tokenDal.incrementVersion(userId, sessionId);
|
||||||
|
|
||||||
|
const createJwtToken = (
|
||||||
|
payload: string | Buffer | object,
|
||||||
|
secret: string,
|
||||||
|
options?: SignOptions
|
||||||
|
) => jwt.sign(payload, secret, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
createTokenForUser,
|
||||||
|
validateTokenForUser,
|
||||||
|
createJwtToken,
|
||||||
|
getUserTokenSession,
|
||||||
|
clearTokenSessionById
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export enum TokenType {
|
||||||
|
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||||
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateTokenForUserDTO = {
|
||||||
|
type: TokenType;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateTokenForUserDTO = {
|
||||||
|
type: TokenType;
|
||||||
|
code: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpsertTokenForUserDalDTO = {
|
||||||
|
type: TokenType;
|
||||||
|
expiresAt: Date;
|
||||||
|
userId: string;
|
||||||
|
tokenHash: string;
|
||||||
|
triesLeft?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetTokenForUserDalDTO = {
|
||||||
|
userId: string;
|
||||||
|
type: TokenType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteTokenForUserDalDTO = {
|
||||||
|
userId: string;
|
||||||
|
type: TokenType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueAuthTokenDTO = {
|
||||||
|
userId: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
container_name: infisical-dev-nginx
|
||||||
|
image: nginx
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
volumes:
|
||||||
|
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: infisical
|
||||||
|
POSTGRES_USER: infisical
|
||||||
|
POSTGRES_DB: infisical
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend-pg
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
|
||||||
|
volumes:
|
||||||
|
- ./backend-pg/src:/app/src
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: infisical-dev-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
volumes:
|
||||||
|
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
||||||
|
- ./frontend/public:/app/public
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_ENV=development
|
||||||
|
- INFISICAL_TELEMETRY_ENABLED=false
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
driver: local
|
||||||
@@ -72,9 +72,9 @@ export const ServerConfigProvider = ({ children }: Props): JSX.Element => {
|
|||||||
|
|
||||||
export const useServerConfig = () => {
|
export const useServerConfig = () => {
|
||||||
const ctx = useContext(ServerConfigContext);
|
const ctx = useContext(ServerConfigContext);
|
||||||
if (!ctx) {
|
// if (!ctx) {
|
||||||
throw new Error("useServerConfig has to be used within <UserContext.Provider>");
|
// throw new Error("useServerConfig has to be used within <UserContext.Provider>");
|
||||||
}
|
// }
|
||||||
|
|
||||||
return ctx;
|
return ctx || { config: { allowSignUp: true } };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const verifySignupInvite = async (details: VerifySignupInviteDTO) => {
|
|||||||
export const useSendVerificationEmail = () => {
|
export const useSendVerificationEmail = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ email }: { email: string }) => {
|
mutationFn: async ({ email }: { email: string }) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/signup/email/signup", {
|
const { data } = await apiRequest.post("/api/v3/signup/email/signup", {
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export const useSendVerificationEmail = () => {
|
|||||||
export const useVerifyEmailVerificationCode = () => {
|
export const useVerifyEmailVerificationCode = () => {
|
||||||
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/v1/signup/email/verify", {
|
const { data } = await apiRequest.post("/api/v3/signup/email/verify", {
|
||||||
email,
|
email,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,14 +82,13 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) ||
|
publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) ||
|
||||||
!Component.requireAuth
|
!Component.requireAuth
|
||||||
) {
|
) {
|
||||||
|
// TODO(akhilmhdh): bring back server config later
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ServerConfigProvider>
|
<AuthProvider>
|
||||||
<AuthProvider>
|
<Component {...pageProps} />
|
||||||
<Component {...pageProps} />
|
</AuthProvider>
|
||||||
</AuthProvider>
|
|
||||||
</ServerConfigProvider>
|
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user