mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): completed signup, login, password and backup key migration
This commit is contained in:
11
backend-pg/.dockerignore
Normal file
11
backend-pg/.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
14
backend-pg/Dockerfile.dev
Normal file
14
backend-pg/Dockerfile.dev
Normal file
@@ -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"]
|
||||
6
backend-pg/nodemon.json
Normal file
6
backend-pg/nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": ".ts,.js",
|
||||
"ignore": [],
|
||||
"exec": "tsx ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine"
|
||||
}
|
||||
1309
backend-pg/package-lock.json
generated
1309
backend-pg/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,15 +6,28 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "tsx watch --clear-screen=false ./src/server/app.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
||||
"dev:docker": "nodemon",
|
||||
"type:check": "tsc --noEmit",
|
||||
"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": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@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/parser": "^6.12.0",
|
||||
"eslint": "^8.54.0",
|
||||
@@ -24,7 +37,9 @@
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"prompt-sync": "^4.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsx": "^4.4.0",
|
||||
"typescript": "^5.3.2"
|
||||
@@ -36,11 +51,18 @@
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"fastify": "^4.24.3",
|
||||
"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",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^8.16.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
|
||||
81
backend-pg/scripts/create-backend-file.ts
Normal file
81
backend-pg/scripts/create-backend-file.ts
Normal file
@@ -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>>;
|
||||
`
|
||||
);
|
||||
}
|
||||
15
backend-pg/scripts/create-migration.ts
Normal file
15
backend-pg/scripts/create-migration.ts
Normal file
@@ -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
backend-pg/src/@types/fastify-zod.d.ts
vendored
Normal file
19
backend-pg/src/@types/fastify-zod.d.ts
vendored
Normal file
@@ -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
|
||||
>;
|
||||
}
|
||||
35
backend-pg/src/@types/fastify.d.ts
vendored
35
backend-pg/src/@types/fastify.d.ts
vendored
@@ -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";
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
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
|
||||
>;
|
||||
}
|
||||
|
||||
43
backend-pg/src/@types/knex.d.ts
vendored
Normal file
43
backend-pg/src/@types/knex.d.ts
vendored
Normal file
@@ -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
|
||||
>;
|
||||
}
|
||||
}
|
||||
2
backend-pg/src/db/index.ts
Normal file
2
backend-pg/src/db/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { TDbClient } from "./instance";
|
||||
export { initDbConnection } from "./instance";
|
||||
11
backend-pg/src/db/instance.ts
Normal file
11
backend-pg/src/db/instance.ts
Normal file
@@ -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;
|
||||
};
|
||||
24
backend-pg/src/db/knexfile.ts
Normal file
24
backend-pg/src/db/knexfile.ts
Normal file
@@ -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;
|
||||
37
backend-pg/src/db/migrations/20231128072457_user.ts
Normal file
37
backend-pg/src/db/migrations/20231128072457_user.ts
Normal file
@@ -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);
|
||||
}
|
||||
24
backend-pg/src/db/migrations/20231129072939_auth-token.ts
Normal file
24
backend-pg/src/db/migrations/20231129072939_auth-token.ts
Normal file
@@ -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);
|
||||
}
|
||||
26
backend-pg/src/db/migrations/20231201151432_backup-key.ts
Normal file
26
backend-pg/src/db/migrations/20231201151432_backup-key.ts
Normal file
@@ -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);
|
||||
}
|
||||
21
backend-pg/src/db/schemas/backup-private-key.ts
Normal file
21
backend-pg/src/db/schemas/backup-private-key.ts
Normal file
@@ -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>>;
|
||||
15
backend-pg/src/db/schemas/index.ts
Normal file
15
backend-pg/src/db/schemas/index.ts
Normal file
@@ -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";
|
||||
19
backend-pg/src/db/schemas/models.ts
Normal file
19
backend-pg/src/db/schemas/models.ts
Normal file
@@ -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"
|
||||
}
|
||||
17
backend-pg/src/db/schemas/token-session.ts
Normal file
17
backend-pg/src/db/schemas/token-session.ts
Normal file
@@ -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>>;
|
||||
19
backend-pg/src/db/schemas/token.ts
Normal file
19
backend-pg/src/db/schemas/token.ts
Normal file
@@ -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>>;
|
||||
26
backend-pg/src/db/schemas/user-encryption-key.ts
Normal file
26
backend-pg/src/db/schemas/user-encryption-key.ts
Normal file
@@ -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>>;
|
||||
38
backend-pg/src/db/schemas/user.ts
Normal file
38
backend-pg/src/db/schemas/user.ts
Normal file
@@ -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">>;
|
||||
43
backend-pg/src/db/utils.ts
Normal file
43
backend-pg/src/db/utils.ts
Normal file
@@ -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 { z } from "zod";
|
||||
|
||||
import { zpStr } from "../zod";
|
||||
|
||||
const zodStrBool = z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true");
|
||||
|
||||
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"),
|
||||
SALT_ROUNDS: z.coerce.number().default(10),
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: z.string().optional(),
|
||||
ROOT_ENCRYPTION_KEY: z.string().optional(),
|
||||
HTTPS_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true")
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
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>>;
|
||||
@@ -21,8 +44,20 @@ export const initEnvConfig = (logger: Logger) => {
|
||||
const parsedEnv = envSchema.safeParse(process.env);
|
||||
if (!parsedEnv.success) {
|
||||
logger.error("Invalid environment variables. Check the error below");
|
||||
logger.error(parsedEnv.error);
|
||||
logger.error(parsedEnv.error.issues);
|
||||
process.exit(-1);
|
||||
}
|
||||
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}>`
|
||||
});
|
||||
|
||||
1
backend-pg/src/lib/crypto/index.ts
Normal file
1
backend-pg/src/lib/crypto/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||
26
backend-pg/src/lib/crypto/srp.ts
Normal file
26
backend-pg/src/lib/crypto/srp.ts
Normal file
@@ -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);
|
||||
};
|
||||
36
backend-pg/src/lib/errors/index.ts
Normal file
36
backend-pg/src/lib/errors/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
5
backend-pg/src/lib/types/index.ts
Normal file
5
backend-pg/src/lib/types/index.ts
Normal file
@@ -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>>;
|
||||
3519
backend-pg/src/lib/validator/disposable_emails.txt
Normal file
3519
backend-pg/src/lib/validator/disposable_emails.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
backend-pg/src/lib/validator/index.ts
Normal file
1
backend-pg/src/lib/validator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { isDisposableEmail } from "./validate-email";
|
||||
10
backend-pg/src/lib/validator/validate-email.ts
Normal file
10
backend-pg/src/lib/validator/validate-email.ts
Normal file
@@ -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;
|
||||
};
|
||||
12
backend-pg/src/lib/zod/index.ts
Normal file
12
backend-pg/src/lib/zod/index.ts
Normal file
@@ -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 fasitfy from "fastify";
|
||||
import { z } from "zod";
|
||||
import type { FastifyCookieOptions } from "@fastify/cookie";
|
||||
import cookie from "@fastify/cookie";
|
||||
import type { FastifyCorsOptions } from "@fastify/cors";
|
||||
@@ -9,20 +8,24 @@ import helmet from "@fastify/helmet";
|
||||
import type { FastifyRateLimitOptions } 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 { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
||||
import { fastifyIp } from "./plugins/ip";
|
||||
import { fastifySwagger } from "./plugins/swagger";
|
||||
import { registerRoutes } from "./routes";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Run the server!
|
||||
const main = async () => {
|
||||
const logger = await initLogger();
|
||||
initEnvConfig(logger);
|
||||
const envCfg = initEnvConfig(logger);
|
||||
|
||||
const server = fasitfy({
|
||||
logger,
|
||||
@@ -32,42 +35,31 @@ const main = async () => {
|
||||
server.setValidatorCompiler(validatorCompiler);
|
||||
server.setSerializerCompiler(serializerCompiler);
|
||||
|
||||
const db = initDbConnection(envCfg.DB_CONNECTION_URI);
|
||||
const smtp = smtpServiceFactory(formatSmtpConfig());
|
||||
|
||||
try {
|
||||
// TODO(akhilmhdh:pg): change this to environment variable with default
|
||||
await server.register<FastifyCookieOptions>(cookie, {
|
||||
secret: "infisical-cookie-secret"
|
||||
secret: envCfg.COOKIE_SECRET_SIGN_KEY
|
||||
});
|
||||
|
||||
await server.register<FastifyCorsOptions>(cors, {
|
||||
credentials: true,
|
||||
origin: "http://localhost:3000"
|
||||
origin: true
|
||||
});
|
||||
// pull ip based on various proxy headers
|
||||
await server.register(fastifyIp);
|
||||
|
||||
// Rate limiters and security headers
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
|
||||
await server.register(helmet);
|
||||
|
||||
await server.register(fastifySwagger);
|
||||
|
||||
// Declare a route
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({ hello: z.string() })
|
||||
}
|
||||
},
|
||||
handler: () => ({
|
||||
hello: "world"
|
||||
})
|
||||
});
|
||||
// Rate limiters and security headers
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
|
||||
await server.register(helmet, { contentSecurityPolicy: false });
|
||||
|
||||
await server.register(registerRoutes, { prefix: "/api", smtp, db });
|
||||
await server.ready();
|
||||
server.swagger();
|
||||
await server.listen({ port: 8000 });
|
||||
await server.listen({ port: envCfg.PORT, host: envCfg.HOST });
|
||||
} catch (err) {
|
||||
server.log.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -4,6 +4,7 @@ import swaggerUI from "@fastify/swagger-ui";
|
||||
|
||||
import { jsonSchemaTransform } from "./fastify-zod";
|
||||
|
||||
// TODO(akhilmhdh-pg): change the localhost port later
|
||||
export const fastifySwagger = fp(async (fastify) => {
|
||||
await fastify.register(swagger, {
|
||||
transform: jsonSchemaTransform,
|
||||
@@ -15,12 +16,12 @@ export const fastifySwagger = fp(async (fastify) => {
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.infisical.com",
|
||||
description: "Production server"
|
||||
url: "http://localhost:4000",
|
||||
description: "Local server"
|
||||
},
|
||||
{
|
||||
url: "http://localhost:8000",
|
||||
description: "Local server"
|
||||
url: "https://app.infisical.com",
|
||||
description: "Production server"
|
||||
}
|
||||
],
|
||||
components: {
|
||||
|
||||
44
backend-pg/src/server/routes/index.ts
Normal file
44
backend-pg/src/server/routes/index.ts
Normal file
@@ -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" });
|
||||
};
|
||||
5
backend-pg/src/server/routes/v1/index.ts
Normal file
5
backend-pg/src/server/routes/v1/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
|
||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
};
|
||||
115
backend-pg/src/server/routes/v1/password-router.ts
Normal file
115
backend-pg/src/server/routes/v1/password-router.ts
Normal file
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
5
backend-pg/src/server/routes/v2/index.ts
Normal file
5
backend-pg/src/server/routes/v2/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { registerMfaRouter } from "./mfa-router";
|
||||
|
||||
export const registerV2Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerMfaRouter, { prefix: "/auth" });
|
||||
};
|
||||
89
backend-pg/src/server/routes/v2/mfa-router.ts
Normal file
89
backend-pg/src/server/routes/v2/mfa-router.ts
Normal file
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
7
backend-pg/src/server/routes/v3/index.ts
Normal file
7
backend-pg/src/server/routes/v3/index.ts
Normal file
@@ -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" });
|
||||
};
|
||||
97
backend-pg/src/server/routes/v3/login-router.ts
Normal file
97
backend-pg/src/server/routes/v3/login-router.ts
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
103
backend-pg/src/server/routes/v3/signup-router.ts
Normal file
103
backend-pg/src/server/routes/v3/signup-router.ts
Normal file
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
131
backend-pg/src/services/auth/auth-dal.ts
Normal file
131
backend-pg/src/services/auth/auth-dal.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
249
backend-pg/src/services/auth/auth-login-service.ts
Normal file
249
backend-pg/src/services/auth/auth-login-service.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
20
backend-pg/src/services/auth/auth-login-type.ts
Normal file
20
backend-pg/src/services/auth/auth-login-type.ts
Normal file
@@ -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;
|
||||
};
|
||||
257
backend-pg/src/services/auth/auth-password-service.ts
Normal file
257
backend-pg/src/services/auth/auth-password-service.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
35
backend-pg/src/services/auth/auth-password-type.ts
Normal file
35
backend-pg/src/services/auth/auth-password-type.ts
Normal file
@@ -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;
|
||||
};
|
||||
169
backend-pg/src/services/auth/auth-signup-service.ts
Normal file
169
backend-pg/src/services/auth/auth-signup-service.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
38
backend-pg/src/services/auth/auth-signup-type.ts
Normal file
38
backend-pg/src/services/auth/auth-signup-type.ts
Normal file
@@ -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;
|
||||
};
|
||||
78
backend-pg/src/services/smtp/smtp-service.ts
Normal file
78
backend-pg/src/services/smtp/smtp-service.ts
Normal file
@@ -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 };
|
||||
};
|
||||
19
backend-pg/src/services/smtp/templates/emailMfa.handlebars
Normal file
19
backend-pg/src/services/smtp/templates/emailMfa.handlebars
Normal file
@@ -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 support@infisical.com</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>
|
||||
19
backend-pg/src/services/smtp/templates/newDevice.handlebars
Normal file
19
backend-pg/src/services/smtp/templates/newDevice.handlebars
Normal file
@@ -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 team@infisical.com</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>
|
||||
98
backend-pg/src/services/token/token-dal.ts
Normal file
98
backend-pg/src/services/token/token-dal.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
134
backend-pg/src/services/token/token-service.ts
Normal file
134
backend-pg/src/services/token/token-service.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
41
backend-pg/src/services/token/token-types.ts
Normal file
41
backend-pg/src/services/token/token-types.ts
Normal file
@@ -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;
|
||||
};
|
||||
58
docker-compose.pg.yml
Normal file
58
docker-compose.pg.yml
Normal file
@@ -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 = () => {
|
||||
const ctx = useContext(ServerConfigContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useServerConfig has to be used within <UserContext.Provider>");
|
||||
}
|
||||
// if (!ctx) {
|
||||
// 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 = () => {
|
||||
return useMutation({
|
||||
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
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ export const useSendVerificationEmail = () => {
|
||||
export const useVerifyEmailVerificationCode = () => {
|
||||
return useMutation({
|
||||
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,
|
||||
code
|
||||
});
|
||||
|
||||
@@ -82,14 +82,13 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
publicPaths.includes(`/${appProps.router.pathname.split("/")[1]}`) ||
|
||||
!Component.requireAuth
|
||||
) {
|
||||
// TODO(akhilmhdh): bring back server config later
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NotificationProvider>
|
||||
<ServerConfigProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
</ServerConfigProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
</NotificationProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user