mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 00:29:34 +00:00
feat(infisical-pg): auth injection completed and validation in password router
This commit is contained in:
@@ -18,6 +18,7 @@ module.exports = {
|
||||
"import/first": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"consistent-return": "off",
|
||||
"simple-import-sort/imports": [
|
||||
"warn",
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"lint:fix": "eslint --fix 'src/**/*.ts'",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.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",
|
||||
|
||||
@@ -10,7 +10,6 @@ console.log(`
|
||||
Component List
|
||||
--------------
|
||||
1. Service component
|
||||
2. Schema file
|
||||
`);
|
||||
const componentType = parseInt(prompt("Select a component: "), 10);
|
||||
|
||||
@@ -55,27 +54,4 @@ export const ${serviceName} = ({ ${componentName}Dal }: ${serviceTypeName}Dep) =
|
||||
`
|
||||
);
|
||||
writeFileSync(path.join(dir, `${componentName}-types.ts`), "");
|
||||
} else if (componentType === 2) {
|
||||
const componentName = prompt("Type component name in lowercase with space seperated: ");
|
||||
const dashcase = componentName.split(" ").join("-");
|
||||
const pascalCase = componentName
|
||||
.split(" ")
|
||||
.reduce(
|
||||
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
|
||||
""
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||
`
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ${pascalCase}Schema = z.object({});
|
||||
|
||||
export type T${pascalCase} = z.infer<typeof ${pascalCase}Schema>;
|
||||
export type T${pascalCase}Insert = Omit<T${pascalCase}, TImmutableDBKeys>;
|
||||
export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>;
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import knex from "knex";
|
||||
import { appendFileSync, readFileSync, writeFileSync } from "fs";
|
||||
import promptSync from "prompt-sync";
|
||||
|
||||
const prompt = promptSync();
|
||||
|
||||
dotenv.config({
|
||||
path: path.join(__dirname, "../.env"),
|
||||
debug: true
|
||||
});
|
||||
|
||||
const db = knex({
|
||||
client: "pg",
|
||||
connection: process.env.DB_CONNECTION_URI
|
||||
});
|
||||
|
||||
const getZodPrimitiveType = (type: string) => {
|
||||
switch (type) {
|
||||
case "uuid":
|
||||
return "z.string().uuid()";
|
||||
case "character varying":
|
||||
return "z.string()";
|
||||
case "ARRAY":
|
||||
return "z.string().array()";
|
||||
case "boolean":
|
||||
return "z.boolean()";
|
||||
case "jsonb":
|
||||
return "z.string()";
|
||||
case "json":
|
||||
return "z.string()";
|
||||
case "timestamp with time zone":
|
||||
return "z.string().datetime()";
|
||||
case "integer":
|
||||
return "z.number()";
|
||||
case "text":
|
||||
return "z.string()";
|
||||
default:
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getZodDefaultValue = (type: unknown, value: string | number | boolean | Object) => {
|
||||
if (!value || value === "null") return;
|
||||
switch (type) {
|
||||
case "uuid":
|
||||
return;
|
||||
case "character varying": {
|
||||
if (typeof value === "string" && value.includes("::")) {
|
||||
return `.default(${value.split("::")[0]})`;
|
||||
}
|
||||
return `.default(${value})`;
|
||||
}
|
||||
case "ARRAY":
|
||||
return `.default(${value})`;
|
||||
case "boolean":
|
||||
return `.default(${value})`;
|
||||
case "jsonb":
|
||||
return "z.string()";
|
||||
case "json":
|
||||
return "z.string()";
|
||||
case "timestamp with time zone": {
|
||||
if (value === "CURRENT_TIMESTAMP") return;
|
||||
return "z.string().datetime()";
|
||||
}
|
||||
case "integer": {
|
||||
if ((value as string).includes("nextval")) return;
|
||||
return `.default(${value})`;
|
||||
}
|
||||
case "text":
|
||||
if (typeof value === "string" && value.includes("::")) {
|
||||
return `.default(${value.split("::")[0]})`;
|
||||
}
|
||||
return `.default(${value})`;
|
||||
default:
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const tables = (
|
||||
await db("information_schema.tables")
|
||||
.whereRaw("table_schema = current_schema()")
|
||||
.select<{ tableName: string }[]>("table_name as tableName")
|
||||
.orderBy("table_name")
|
||||
).filter(
|
||||
(el) => el.tableName !== "infisical_migrations_lock" && el.tableName !== "infisical_migrations"
|
||||
);
|
||||
|
||||
console.log("Select a table to generate schema");
|
||||
console.table(tables);
|
||||
console.log("all: all tables");
|
||||
const selectedTables = prompt("Type table numbers comma seperated: ");
|
||||
const tableNumbers =
|
||||
selectedTables !== "all" ? selectedTables.split(",").map((el) => Number(el)) : [];
|
||||
|
||||
for (let i = 0; i < tables.length; i++) {
|
||||
// skip if not desired table
|
||||
if (selectedTables !== "all" && !tableNumbers.includes(i)) continue;
|
||||
|
||||
const tableName = tables[i].tableName;
|
||||
const columns = await db(tableName).columnInfo();
|
||||
const columnNames = Object.keys(columns);
|
||||
|
||||
let schema = "";
|
||||
for (let colNum = 0; colNum < columnNames.length; colNum++) {
|
||||
const columnName = columnNames[colNum];
|
||||
const colInfo = columns[columnName];
|
||||
let ztype = getZodPrimitiveType(colInfo.type);
|
||||
if (colInfo.defaultValue) {
|
||||
const defaultValue = colInfo.defaultValue;
|
||||
const zSchema = getZodDefaultValue(colInfo.type, defaultValue);
|
||||
if (zSchema) {
|
||||
ztype = ztype.concat(zSchema);
|
||||
}
|
||||
}
|
||||
if (colInfo.nullable) {
|
||||
ztype = ztype.concat(".nullable().optional()");
|
||||
}
|
||||
schema = schema.concat(`${!schema ? "\n" : ""} ${columnName}: ${ztype},\n`);
|
||||
}
|
||||
|
||||
const dashcase = tableName.split("_").join("-");
|
||||
const pascalCase = tableName
|
||||
.split("_")
|
||||
.reduce(
|
||||
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
|
||||
""
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||
`// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ${pascalCase}Schema = z.object({${schema}});
|
||||
|
||||
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>>;
|
||||
`
|
||||
);
|
||||
|
||||
// const file = readFileSync(path.join(__dirname, "../src/db/schemas/index.ts"), "utf8");
|
||||
// if (!file.includes(`export * from "./${dashcase};"`)) {
|
||||
// appendFileSync(
|
||||
// path.join(__dirname, "../src/db/schemas/index.ts"),
|
||||
// `\nexport * from "./${dashcase}";`,
|
||||
// "utf8"
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
main();
|
||||
Vendored
+9
@@ -3,6 +3,8 @@ 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 { AuthMode } from "@app/services/auth/auth-signup-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/token/token-service";
|
||||
|
||||
import "fastify";
|
||||
|
||||
@@ -14,6 +16,12 @@ declare module "fastify" {
|
||||
userId: string;
|
||||
user: TUser;
|
||||
};
|
||||
// identity injection. depending on which kinda of token the information is filled in auth
|
||||
auth: {
|
||||
authMode: AuthMode.JWT | AuthMode.API_KEY_V2 | AuthMode.API_KEY;
|
||||
userId: string;
|
||||
user: TUser;
|
||||
};
|
||||
}
|
||||
|
||||
interface FastifyInstance {
|
||||
@@ -21,6 +29,7 @@ declare module "fastify" {
|
||||
login: TAuthLoginFactory;
|
||||
password: TAuthPasswordFactory;
|
||||
signup: TAuthSignupFactory;
|
||||
authToken: TAuthTokenServiceFactory;
|
||||
};
|
||||
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
|
||||
Vendored
+39
-23
@@ -2,42 +2,58 @@ import { Knex } from "knex";
|
||||
|
||||
import {
|
||||
TableName,
|
||||
TAuthTokens,
|
||||
TAuthTokenSessions,
|
||||
TAuthTokenSessionsInsert,
|
||||
TAuthTokenSessionsUpdate,
|
||||
TAuthTokensUpdate,
|
||||
TBackupPrivateKey,
|
||||
TBackupPrivateKeyInsert,
|
||||
TToken,
|
||||
TTokenInsert,
|
||||
TTokenUpdate,
|
||||
TUser,
|
||||
TUserEncryptionKey,
|
||||
TUserEncryptionKeyInsert,
|
||||
TUserEncryptionKeyUpdate,
|
||||
TUserInsert,
|
||||
TUserUpdate
|
||||
TBackupPrivateKeyUpdate,
|
||||
TOrganizationMemberships,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate,
|
||||
TUserEncryptionKeys,
|
||||
TUserEncryptionKeysInsert,
|
||||
TUserEncryptionKeysUpdate,
|
||||
TUsers,
|
||||
TUsersInsert,
|
||||
TUsersUpdate
|
||||
} 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.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
|
||||
TUserEncryptionKey,
|
||||
TUserEncryptionKeyInsert,
|
||||
TUserEncryptionKeyUpdate
|
||||
TUserEncryptionKeys,
|
||||
TUserEncryptionKeysInsert,
|
||||
TUserEncryptionKeysUpdate
|
||||
>;
|
||||
[TableName.AuthTokens]: Knex.CompositeTableType<
|
||||
TAuthTokens,
|
||||
TAuthTokensInsert,
|
||||
TAuthTokensUpdate
|
||||
>;
|
||||
[TableName.AuthTokens]: Knex.CompositeTableType<TToken, TTokenInsert, TTokenUpdate>;
|
||||
[TableName.AuthTokenSession]: Knex.CompositeTableType<
|
||||
TTokenSession,
|
||||
TTokenSessionInsert,
|
||||
TTokenSessionUpdate
|
||||
TAuthTokenSessions,
|
||||
TAuthTokenSessionsInsert,
|
||||
TAuthTokenSessionsUpdate
|
||||
>;
|
||||
[TableName.BackupPrivateKey]: Knex.CompositeTableType<
|
||||
TBackupPrivateKey,
|
||||
TBackupPrivateKeyInsert,
|
||||
TTokenSessionUpdate
|
||||
TBackupPrivateKeyUpdate
|
||||
>;
|
||||
[TableName.Organization]: Knex.CompositeTableType<
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate
|
||||
>;
|
||||
[TableName.OrgMembership]: Knex.CompositeTableType<
|
||||
TOrganizationMemberships,
|
||||
TOrganizationsInsert,
|
||||
TOrganizationsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("email").notNullable();
|
||||
t.specificType("authMethods", "text[]");
|
||||
t.boolean("superAdmin").defaultTo(false);
|
||||
|
||||
@@ -6,10 +6,10 @@ 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.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.text("clientPublicKey");
|
||||
t.text("serverPrivateKey");
|
||||
t.text("encryptionVersion").defaultTo(1);
|
||||
t.integer("encryptionVersion").defaultTo(1);
|
||||
t.text("protectedKey").notNullable();
|
||||
t.text("protectedKeyIV").notNullable();
|
||||
t.text("protectedKeyTag").notNullable();
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("type").notNullable();
|
||||
t.string("phoneNumber");
|
||||
t.string("tokenHash").notNullable();
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("ip").notNullable();
|
||||
t.string("userAgent");
|
||||
t.integer("refreshVersion").notNullable().defaultTo(1);
|
||||
|
||||
@@ -6,7 +6,7 @@ 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.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("encryptedPrivateKey").notNullable();
|
||||
t.string("iv").notNullable();
|
||||
t.string("tag").notNullable();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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.Organization);
|
||||
if (!isTablePresent) {
|
||||
await knex.schema.createTable(TableName.Organization, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.string("customerId");
|
||||
// does not need update trigger we will do it manually
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
// this is a one time function
|
||||
await createOnUpdateTrigger(knex, TableName.Organization);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.Organization);
|
||||
await dropOnUpdateTrigger(knex, TableName.Organization);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { OrgMembershipStatus } from "../schemas/models";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.OrgMembership);
|
||||
if (!isTablePresent) {
|
||||
await knex.schema.createTable(TableName.OrgMembership, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("role").notNullable();
|
||||
t.string("status").notNullable().defaultTo(OrgMembershipStatus.Invited);
|
||||
t.string("inviteEmail");
|
||||
// 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");
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
// this is a one time function
|
||||
await createOnUpdateTrigger(knex, TableName.OrgMembership);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.OrgMembership);
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgMembership);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AuthTokenSessionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
ip: z.string(),
|
||||
userAgent: z.string().nullable().optional(),
|
||||
refreshVersion: z.number().default(1),
|
||||
accessVersion: z.number().default(1),
|
||||
lastUsed: z.string().datetime(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TAuthTokenSessions = z.infer<typeof AuthTokenSessionsSchema>;
|
||||
export type TAuthTokenSessionsInsert = Omit<TAuthTokenSessions, TImmutableDBKeys>;
|
||||
export type TAuthTokenSessionsUpdate = Partial<Omit<TAuthTokenSessions, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AuthTokensSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: z.string(),
|
||||
phoneNumber: z.string().nullable().optional(),
|
||||
tokenHash: z.string(),
|
||||
triesLeft: z.number().nullable().optional(),
|
||||
expiresAt: z.string().datetime(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
export type TAuthTokensInsert = Omit<TAuthTokens, TImmutableDBKeys>;
|
||||
export type TAuthTokensUpdate = Partial<Omit<TAuthTokens, TImmutableDBKeys>>;
|
||||
@@ -1,19 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TImmutableDBKeys } from "./models";
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const BackupPrivateKeySchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
id: z.string().uuid(),
|
||||
encryptedPrivateKey: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
algorithm: z.nativeEnum(SecretEncryptionAlgo),
|
||||
keyEncoding: z.nativeEnum(SecretKeyEncoding),
|
||||
algorithm: z.string(),
|
||||
keyEncoding: z.string(),
|
||||
salt: z.string(),
|
||||
verifier: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime()
|
||||
updatedAt: z.string().datetime(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TBackupPrivateKey = z.infer<typeof BackupPrivateKeySchema>;
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
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";
|
||||
export * from "./auth-token-sessions";
|
||||
export * from "./auth-tokens";
|
||||
export * from "./backup-private-key";
|
||||
export * from "./models";
|
||||
export * from "./organization-memberships";
|
||||
export * from "./organizations";
|
||||
export * from "./user-encryption-keys";
|
||||
export * from "./users";
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum TableName {
|
||||
Users = "users",
|
||||
UserEncryptionKey = "user_encryption_keys",
|
||||
AuthTokens = "auth_tokens",
|
||||
AuthTokenSession = "auth_token_sessions",
|
||||
BackupPrivateKey = "backup_private_key"
|
||||
BackupPrivateKey = "backup_private_key",
|
||||
Organization = "organizations",
|
||||
OrgMembership = "organization_memberships"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
||||
export const UserDeviceSchema = z
|
||||
.object({
|
||||
ip: z.string(),
|
||||
userAgent: z.string()
|
||||
})
|
||||
.array()
|
||||
.default([]);
|
||||
|
||||
export enum OrgMembershipRole {
|
||||
Admin = "admin",
|
||||
Member = "member",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
export enum OrgMembershipStatus {
|
||||
Invited = "invited",
|
||||
Accepted = "accepted"
|
||||
}
|
||||
|
||||
export enum SecretEncryptionAlgo {
|
||||
AES_256_GCM = "aes-256-gcm"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrganizationMembershipsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
role: z.string(),
|
||||
status: z.string().default('invited'),
|
||||
inviteEmail: z.string().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
userId: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TOrganizationMemberships = z.infer<typeof OrganizationMembershipsSchema>;
|
||||
export type TOrganizationMembershipsInsert = Omit<TOrganizationMemberships, TImmutableDBKeys>;
|
||||
export type TOrganizationMembershipsUpdate = Partial<Omit<TOrganizationMemberships, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrganizationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
customerId: z.string().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
export type TOrganizationsInsert = Omit<TOrganizations, TImmutableDBKeys>;
|
||||
export type TOrganizationsUpdate = Partial<Omit<TOrganizations, TImmutableDBKeys>>;
|
||||
@@ -1,17 +0,0 @@
|
||||
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>>;
|
||||
@@ -1,19 +0,0 @@
|
||||
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>>;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PickRequired } from "@app/lib/types";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const UserEncryptionKey = z.object({
|
||||
id: z.number(),
|
||||
userId: z.string(),
|
||||
serverPrivateKey: z.string().optional().nullable(),
|
||||
clientPublicKey: z.string().optional().nullable(),
|
||||
encryptionVersion: z.number().default(1).optional(),
|
||||
protectedKey: z.string(),
|
||||
protectedKeyIV: z.string(),
|
||||
protectedKeyTag: z.string(),
|
||||
publicKey: z.string(),
|
||||
encryptedPrivateKey: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
salt: z.string(),
|
||||
verifier: z.string()
|
||||
});
|
||||
|
||||
export type TUserEncryptionKey = z.infer<typeof UserEncryptionKey>;
|
||||
export type TUserEncryptionKeyInsert = Omit<PickRequired<TUserEncryptionKey>, TImmutableDBKeys>;
|
||||
export type TUserEncryptionKeyUpdate = Partial<Omit<TUserEncryptionKey, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const UserEncryptionKeysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
clientPublicKey: z.string().nullable().optional(),
|
||||
serverPrivateKey: z.string().nullable().optional(),
|
||||
encryptionVersion: z.number().default(1).nullable().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(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;
|
||||
export type TUserEncryptionKeysInsert = Omit<TUserEncryptionKeys, TImmutableDBKeys>;
|
||||
export type TUserEncryptionKeysUpdate = Partial<Omit<TUserEncryptionKeys, TImmutableDBKeys>>;
|
||||
@@ -1,38 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||
}
|
||||
|
||||
export const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
authMethods: z.nativeEnum(AuthMethod).array().default([AuthMethod.EMAIL]).optional().nullable(),
|
||||
email: z.string(),
|
||||
isSuperAdmin: z.boolean().default(false).optional(),
|
||||
firstName: z.string().optional().nullable(),
|
||||
lastName: z.string().optional().nullable(),
|
||||
isMfaEnabled: z.boolean().default(false).optional(),
|
||||
mfaMethods: z.string().array().default([]).optional().nullable(),
|
||||
isAccepted: z.boolean().default(false).optional(),
|
||||
devices: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const UserDeviceSchema = z
|
||||
.object({
|
||||
ip: z.string(),
|
||||
userAgent: z.string()
|
||||
})
|
||||
.array()
|
||||
.default([]);
|
||||
|
||||
export type TUser = z.infer<typeof UserSchema>;
|
||||
export type TUserInsert = Omit<TUser, TImmutableDBKeys>;
|
||||
export type TUserUpdate = Partial<Omit<TUser, "id" | "email">>;
|
||||
@@ -0,0 +1,27 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const UsersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string(),
|
||||
authMethods: z.string().array().nullable().optional(),
|
||||
superAdmin: z.boolean().default(false).nullable().optional(),
|
||||
firstName: z.string().nullable().optional(),
|
||||
lastName: z.string().nullable().optional(),
|
||||
isAccepted: z.boolean().default(false).nullable().optional(),
|
||||
isMfaEnabled: z.boolean().default(false).nullable().optional(),
|
||||
mfaMethods: z.string().array().nullable().optional(),
|
||||
devices: z.string().nullable().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
export type TUsersInsert = Omit<TUsers, TImmutableDBKeys>;
|
||||
export type TUsersUpdate = Partial<Omit<TUsers, TImmutableDBKeys>>;
|
||||
@@ -16,7 +16,7 @@ export class UnauthorizedError extends Error {
|
||||
|
||||
error: unknown;
|
||||
|
||||
constructor({ name, error, message }: { message?: string; 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;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||
transaction: async <T>(cb: (tx: Knex) => T) =>
|
||||
db.transaction(async (trx) => {
|
||||
const res = await cb(trx);
|
||||
return res;
|
||||
}),
|
||||
...dal
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { z,ZodTypeAny } from "zod";
|
||||
|
||||
// this is a patched zod string to remove empty string to undefined
|
||||
export const zpStr = <T extends z.ZodString>(
|
||||
export const zpStr = <T extends ZodTypeAny>(
|
||||
schema: T,
|
||||
opt: { stripNull: boolean } = { stripNull: true }
|
||||
) =>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FastifyRequest } from "fastify";
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
|
||||
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
const apiKey = req.headers?.["x-api-key"];
|
||||
if (apiKey) {
|
||||
return { authMode: AuthMode.API_KEY, token: apiKey };
|
||||
}
|
||||
const authHeader = req.headers?.authorization;
|
||||
if (!authHeader) return { authMode: null, token: null };
|
||||
|
||||
const authTokenValue = authHeader.slice(7); // slice of after Bearer
|
||||
if (authTokenValue.startsWith("st.")) {
|
||||
return { authMode: AuthMode.SERVICE_TOKEN, token: authTokenValue } as const;
|
||||
}
|
||||
|
||||
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
|
||||
switch (decodedToken.authTokenType) {
|
||||
case AuthTokenType.ACCESS_TOKEN:
|
||||
return { authMode: AuthMode.JWT, token: decodedToken as AuthModeJwtTokenPayload } as const;
|
||||
case AuthTokenType.API_KEY:
|
||||
return { authMode: AuthMode.API_KEY_V2, token: decodedToken } as const;
|
||||
case AuthMode.SERVICE_ACCESS_TOKEN:
|
||||
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, token: decodedToken } as const;
|
||||
default:
|
||||
throw new UnauthorizedError({ name: "Invalid token type" });
|
||||
}
|
||||
};
|
||||
|
||||
const getJwtIdentity = async (server: FastifyZodProvider, token: AuthModeJwtTokenPayload) => {
|
||||
const session = await server.services.authToken.getUserTokenSessionById(
|
||||
token.tokenVersionId,
|
||||
token.userId
|
||||
);
|
||||
|
||||
if (!session) throw new UnauthorizedError({ name: "Session not found" });
|
||||
if (token.accessVersion !== session.accessVersion)
|
||||
throw new UnauthorizedError({ name: "Stale session" });
|
||||
|
||||
const user = await server.store.user.getUserById(session.userId);
|
||||
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export const injectIdentity = (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("auth", null);
|
||||
server.addHook("onRequest", async (req) => {
|
||||
const appCfg = getConfig();
|
||||
const { authMode, token } = await extractAuth(req, appCfg.JWT_AUTH_SECRET);
|
||||
if (!authMode) return;
|
||||
// TODO(akhilmhdh-pg): fill in rest of auth mode logic
|
||||
switch (authMode) {
|
||||
case AuthMode.JWT: {
|
||||
const user = await getJwtIdentity(server, token as AuthModeJwtTokenPayload);
|
||||
req.auth = { authMode: AuthMode.JWT, user, userId: user.id };
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN:
|
||||
break;
|
||||
case AuthMode.SERVICE_ACCESS_TOKEN:
|
||||
break;
|
||||
case AuthMode.API_KEY:
|
||||
break;
|
||||
case AuthMode.API_KEY_V2:
|
||||
break;
|
||||
default:
|
||||
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const verifyAuth =
|
||||
<T extends FastifyRequest>(authStrats: AuthMode[]) =>
|
||||
(req: T) => {
|
||||
if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array");
|
||||
if (!req.auth)
|
||||
throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" });
|
||||
|
||||
const isAccessAllowed = authStrats.some((strat) => strat === req.auth.authMode);
|
||||
if (!isAccessAllowed) {
|
||||
throw new UnauthorizedError({ name: `${req.url} Unauthorized Access` });
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { tokenServiceFactory } from "@app/services/token/token-service";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
|
||||
export const registerRoutes = async (
|
||||
server: FastifyZodProvider,
|
||||
@@ -37,6 +38,8 @@ export const registerRoutes = async (
|
||||
user: authDal
|
||||
} as FastifyZodProvider["store"]);
|
||||
|
||||
await server.register(injectIdentity);
|
||||
|
||||
// register routes for v1
|
||||
await server.register(registerV1Routes, { prefix: "/v1" });
|
||||
await server.register(registerV2Routes, { prefix: "/v2" });
|
||||
|
||||
@@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
import { BackupPrivateKeySchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -18,6 +20,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
|
||||
req.auth.userId,
|
||||
@@ -48,6 +51,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req, res) => {
|
||||
const appCfg = getConfig();
|
||||
await server.services.password.changePassword({ ...req.body, userId: req.auth.userId });
|
||||
@@ -65,6 +69,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/backup-private-key",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
clientProof: z.string().trim(),
|
||||
@@ -95,6 +100,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/backup-private-key",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -2,20 +2,24 @@ 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";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
server.decorateRequest("mfa", null);
|
||||
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;
|
||||
return res;
|
||||
}
|
||||
const token = authorizationHeader.split(" ")[1];
|
||||
if (!token) res.status(401).send({ error: "Missing bearer token" });
|
||||
if (!token) {
|
||||
res.status(401).send({ error: "Missing bearer token" });
|
||||
return res;
|
||||
}
|
||||
|
||||
const decodedToken = jwt.verify(token, cfg.JWT_AUTH_SECRET) as JwtPayload;
|
||||
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN)
|
||||
@@ -23,8 +27,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
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;
|
||||
req.mfa = { userId: user.id, user };
|
||||
});
|
||||
|
||||
server.route({
|
||||
@@ -52,7 +55,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
encryptionVersion: z.number().default(1).optional(),
|
||||
encryptionVersion: z.number().default(1).nullable().optional(),
|
||||
protectedKey: z.string(),
|
||||
protectedKeyIV: z.string(),
|
||||
protectedKeyTag: z.string(),
|
||||
|
||||
@@ -44,7 +44,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
z.object({ mfaEnabled: z.literal(true), token: z.string() }),
|
||||
z.object({
|
||||
mfaEnabled: z.literal(false),
|
||||
encryptionVersion: z.number().default(1).optional(),
|
||||
encryptionVersion: z.number().default(1).nullable().optional(),
|
||||
protectedKey: z.string(),
|
||||
protectedKeyIV: z.string(),
|
||||
protectedKeyTag: z.string(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UserSchema } from "@app/db/schemas";
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -35,7 +35,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
token: z.string(),
|
||||
user: UserSchema
|
||||
user: UsersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -72,7 +72,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
user: UserSchema,
|
||||
user: UsersSchema,
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TBackupPrivateKey, TUser, TUserEncryptionKey } from "@app/db/schemas";
|
||||
import { TableName, TBackupPrivateKey, TUserEncryptionKeys, TUsers } from "@app/db/schemas";
|
||||
import { withTransaction } from "@app/lib/knex";
|
||||
|
||||
export type TAuthDalFactory = ReturnType<typeof authDalFactory>;
|
||||
|
||||
export const authDalFactory = (db: TDbClient) => {
|
||||
// getters
|
||||
const getUserByEmail = async (email: string): Promise<TUser | undefined> =>
|
||||
const getUserByEmail = async (email: string): Promise<TUsers | undefined> =>
|
||||
db(TableName.Users).where({ email }).select("*").first();
|
||||
|
||||
const getUserById = async (userId: string): Promise<TUser | undefined> =>
|
||||
const getUserById = async (userId: string): Promise<TUsers | undefined> =>
|
||||
db(TableName.Users).where({ id: userId }).select("*").first();
|
||||
|
||||
const getUserEncKeyByEmail = async (email: string) =>
|
||||
@@ -39,8 +40,8 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
// all inserts and updates
|
||||
const createUser = async (
|
||||
email: string,
|
||||
data: Partial<TUser> = {}
|
||||
): Promise<TUser | undefined> => {
|
||||
data: Partial<TUsers> = {}
|
||||
): Promise<TUsers | undefined> => {
|
||||
const [user] = await db(TableName.Users)
|
||||
.insert({ email, ...data })
|
||||
.returning("*");
|
||||
@@ -49,8 +50,8 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
|
||||
const updateUser = async (
|
||||
email: string,
|
||||
data: Partial<TUser> = {}
|
||||
): Promise<TUser | undefined> => {
|
||||
data: Partial<TUsers> = {}
|
||||
): Promise<TUsers | undefined> => {
|
||||
const [user] = await db(TableName.Users)
|
||||
.where({ email })
|
||||
.update({ ...data })
|
||||
@@ -60,9 +61,9 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
|
||||
const updateUserById = async (
|
||||
id: string,
|
||||
data: Partial<TUser> = {},
|
||||
data: Partial<TUsers> = {},
|
||||
tx?: Knex
|
||||
): Promise<TUser | undefined> => {
|
||||
): Promise<TUsers | undefined> => {
|
||||
const [user] = await (tx ? tx(TableName.Users) : db(TableName.Users))
|
||||
.where({ id })
|
||||
.update({ ...data })
|
||||
@@ -72,9 +73,9 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
|
||||
const updateUserEncryptionByUserId = async (
|
||||
userId: string,
|
||||
data: Partial<TUserEncryptionKey> = {},
|
||||
data: Partial<TUserEncryptionKeys> = {},
|
||||
tx?: Knex
|
||||
): Promise<TUserEncryptionKey | undefined> => {
|
||||
): Promise<TUserEncryptionKeys | undefined> => {
|
||||
const [userEnc] = await (tx ? tx(TableName.UserEncryptionKey) : db(TableName.UserEncryptionKey))
|
||||
.where({ userId })
|
||||
.update({ ...data })
|
||||
@@ -85,12 +86,12 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
// all upserts
|
||||
const upsertUserEncryptionKey = async (
|
||||
userId: string,
|
||||
data: Partial<TUserEncryptionKey>,
|
||||
data: Partial<TUserEncryptionKeys>,
|
||||
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)
|
||||
.insert({ userId, ...data } as TUserEncryptionKeys)
|
||||
.onConflict("userId")
|
||||
.merge()
|
||||
.returning("*");
|
||||
@@ -110,12 +111,7 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
return backupKey;
|
||||
};
|
||||
|
||||
return {
|
||||
transaction: async <T>(cb: (tx: Knex) => T) =>
|
||||
db.transaction(async (trx) => {
|
||||
const res = await cb(trx);
|
||||
return res;
|
||||
}),
|
||||
return withTransaction(db, {
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
getUserEncKeyByEmail,
|
||||
@@ -127,5 +123,5 @@ export const authDalFactory = (db: TDbClient) => {
|
||||
updateUserEncryptionByUserId,
|
||||
upsertUserEncryptionKey,
|
||||
upsertBackupKey
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { AuthMethod, TUser } from "@app/db/schemas";
|
||||
import { UserDeviceSchema } from "@app/db/schemas/user";
|
||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
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 { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TAuthDalFactory } from "./auth-dal";
|
||||
import {
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
TLoginGenServerPublicKeyDTO,
|
||||
TVerifyMfaTokenDTO
|
||||
} from "./auth-login-type";
|
||||
import { AuthTokenType } from "./auth-signup-type";
|
||||
import { AuthMethod, AuthTokenType } from "./auth-type";
|
||||
|
||||
const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuthToken?: string) => {
|
||||
if (!providerAuthToken) return false;
|
||||
@@ -27,7 +26,7 @@ const isValidProviderAuthToken = (email: string, jwtSecret: string, providerAuth
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
tokenService: TTokenServiceFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
|
||||
@@ -42,7 +41,7 @@ export const authLoginServiceFactory = ({
|
||||
* 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 updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
|
||||
const devices = await UserDeviceSchema.parseAsync(JSON.parse(user.devices || "[]"));
|
||||
const isDeviceSeen = devices.some(
|
||||
(device) => device.ip === ip && device.userAgent === userAgent
|
||||
@@ -69,7 +68,7 @@ export const authLoginServiceFactory = ({
|
||||
* Private
|
||||
* Send mfa code via email
|
||||
* */
|
||||
const sendUserMfaCode = async (user: TUser) => {
|
||||
const sendUserMfaCode = async (user: TUsers) => {
|
||||
const code = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_MFA,
|
||||
userId: user.id
|
||||
@@ -89,7 +88,7 @@ export const authLoginServiceFactory = ({
|
||||
* 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 generateUserTokens = async (user: TUsers, ip: string, userAgent: string) => {
|
||||
const cfg = getConfig();
|
||||
await updateUserDeviceSession(user, ip, userAgent);
|
||||
const tokenSession = await tokenService.getUserTokenSession({
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TAuthDalFactory } from "./auth-dal";
|
||||
import {
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
TCreateBackupPrivateKeyDTO,
|
||||
TResetPasswordViaBackupKeyDTO
|
||||
} from "./auth-password-type";
|
||||
import { AuthTokenType } from "./auth-signup-type";
|
||||
import { AuthTokenType } from "./auth-type";
|
||||
|
||||
type TAuthPasswordServiceFactoryDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
tokenService: TTokenServiceFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { AuthMethod } from "@app/db/schemas";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
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 { TAuthTokenServiceFactory } from "../token/token-service";
|
||||
import { TokenType } from "../token/token-types";
|
||||
import { TAuthDalFactory } from "./auth-dal";
|
||||
import { AuthTokenType, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||
import { TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||
import { AuthMethod, AuthTokenType } from "./auth-type";
|
||||
|
||||
type TAuthSignupDep = {
|
||||
authDal: TAuthDalFactory;
|
||||
tokenService: TTokenServiceFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
|
||||
@@ -66,7 +68,7 @@ export const authSignupServiceFactory = ({
|
||||
});
|
||||
|
||||
// generate jwt token this is a temporary token
|
||||
const jwtToken = tokenService.createJwtToken(
|
||||
const jwtToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user.id.toString()
|
||||
@@ -136,7 +138,7 @@ export const authSignupServiceFactory = ({
|
||||
if (!tokenSession) throw new Error("Failed to create token");
|
||||
const appCfg = getConfig();
|
||||
|
||||
const accessToken = tokenService.createJwtToken(
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: updateduser.info.id,
|
||||
@@ -147,7 +149,7 @@ export const authSignupServiceFactory = ({
|
||||
{ expiresIn: appCfg.JWT_SIGNUP_LIFETIME }
|
||||
);
|
||||
|
||||
const refreshToken = tokenService.createJwtToken(
|
||||
const refreshToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||
userId: updateduser.info.id,
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||
}
|
||||
|
||||
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 AuthModeJwtTokenPayload = {
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN;
|
||||
userId: string;
|
||||
tokenVersionId: string;
|
||||
accessVersion: number;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TToken } from "@app/db/schemas";
|
||||
import { TTokenSession } from "@app/db/schemas/token-session";
|
||||
import { TableName, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
|
||||
import {
|
||||
TDeleteTokenForUserDalDTO,
|
||||
@@ -19,7 +18,7 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
userId,
|
||||
type,
|
||||
triesLeft
|
||||
}: TUpsertTokenForUserDalDTO): Promise<TToken | undefined> => {
|
||||
}: TUpsertTokenForUserDalDTO): Promise<TAuthTokens | undefined> => {
|
||||
const token = await db.transaction(async (tx) => {
|
||||
await tx(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
|
||||
const [newToken] = await tx(TableName.AuthTokens)
|
||||
@@ -33,13 +32,23 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
const getTokenForUser = async ({
|
||||
userId,
|
||||
type
|
||||
}: TGetTokenForUserDalDTO): Promise<TToken | undefined> =>
|
||||
}: TGetTokenForUserDalDTO): Promise<TAuthTokens | undefined> =>
|
||||
db(TableName.AuthTokens).where({ userId, type }).first();
|
||||
|
||||
const getTokenSession = async (
|
||||
userId: string,
|
||||
ip: string,
|
||||
userAgent: string
|
||||
): Promise<TAuthTokenSessions | undefined> =>
|
||||
db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first();
|
||||
|
||||
const getTokenSessionById = async (id: string, userId: string) =>
|
||||
db(TableName.AuthTokenSession).where({ id, userId }).first();
|
||||
|
||||
const deleteTokenForUser = async ({
|
||||
userId,
|
||||
type
|
||||
}: TDeleteTokenForUserDalDTO): Promise<TToken[] | undefined> =>
|
||||
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> =>
|
||||
db(TableName.AuthTokens).where({ userId, type }).delete().returning("*");
|
||||
|
||||
const decrementTriesField = async ({
|
||||
@@ -49,18 +58,11 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
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> => {
|
||||
): Promise<TAuthTokenSessions | undefined> => {
|
||||
const [session] = await db(TableName.AuthTokenSession)
|
||||
.insert({
|
||||
userId,
|
||||
@@ -77,7 +79,7 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
const incrementVersion = async (
|
||||
userId: string,
|
||||
sessionId: string
|
||||
): Promise<TTokenSession | undefined> => {
|
||||
): Promise<TAuthTokenSessions | undefined> => {
|
||||
const [session] = await db(TableName.AuthTokenSession)
|
||||
.where({ userId, id: sessionId })
|
||||
.increment("accessVersion", 1)
|
||||
@@ -87,8 +89,9 @@ export const tokenDalFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
return {
|
||||
upsertTokenForUser,
|
||||
getTokenForUser,
|
||||
getTokenSessionById,
|
||||
upsertTokenForUser,
|
||||
deleteTokenForUser,
|
||||
decrementTriesField,
|
||||
getTokenSession,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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 { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
import { TTokenDalFactory } from "./token-dal";
|
||||
@@ -14,11 +12,11 @@ import {
|
||||
TValidateTokenForUserDTO
|
||||
} from "./token-types";
|
||||
|
||||
type TTokenServiceFactoryDep = {
|
||||
type TAuthTokenServiceFactoryDep = {
|
||||
tokenDal: TTokenDalFactory;
|
||||
// adjust the expiry from env through here
|
||||
};
|
||||
export type TTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||
|
||||
export const getTokenConfig = (tokenType: TokenType) => {
|
||||
// generate random token based on specified token use-case
|
||||
@@ -57,7 +55,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const tokenServiceFactory = ({ tokenDal }: TTokenServiceFactoryDep) => {
|
||||
export const tokenServiceFactory = ({ tokenDal }: TAuthTokenServiceFactoryDep) => {
|
||||
const createTokenForUser = async ({ type, userId }: TCreateTokenForUserDTO) => {
|
||||
const { token, ...tkCfg } = getTokenConfig(type);
|
||||
const appCfg = getConfig();
|
||||
@@ -76,7 +74,7 @@ export const tokenServiceFactory = ({ tokenDal }: TTokenServiceFactoryDep) => {
|
||||
type,
|
||||
userId,
|
||||
code
|
||||
}: TValidateTokenForUserDTO): Promise<TToken | undefined> => {
|
||||
}: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => {
|
||||
const token = await tokenDal.getTokenForUser({ type, userId });
|
||||
// validate token
|
||||
if (!token) throw new Error("Failed to find token");
|
||||
@@ -105,7 +103,7 @@ export const tokenServiceFactory = ({ tokenDal }: TTokenServiceFactoryDep) => {
|
||||
userId,
|
||||
ip,
|
||||
userAgent
|
||||
}: TIssueAuthTokenDTO): Promise<TTokenSession | undefined> => {
|
||||
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
|
||||
let session = await tokenDal.getTokenSession(userId, ip, userAgent);
|
||||
if (!session) {
|
||||
session = await tokenDal.insertTokenSession(userId, ip, userAgent);
|
||||
@@ -113,22 +111,19 @@ export const tokenServiceFactory = ({ tokenDal }: TTokenServiceFactoryDep) => {
|
||||
return session;
|
||||
};
|
||||
|
||||
const getUserTokenSessionById = async (id: string, userId: string) =>
|
||||
tokenDal.getTokenSessionById(id, userId);
|
||||
|
||||
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);
|
||||
): Promise<TAuthTokenSessions | undefined> => tokenDal.incrementVersion(userId, sessionId);
|
||||
|
||||
return {
|
||||
createTokenForUser,
|
||||
validateTokenForUser,
|
||||
createJwtToken,
|
||||
getUserTokenSession,
|
||||
clearTokenSessionById
|
||||
clearTokenSessionById,
|
||||
getUserTokenSessionById
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user