From f3b37de3f3d608d1286848c1eb3375cb72c45ab6 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 5 Dec 2023 14:53:45 +0530 Subject: [PATCH] feat(infisical-pg): auth injection completed and validation in password router --- backend-pg/.eslintrc.js | 1 + backend-pg/package.json | 1 + backend-pg/scripts/create-backend-file.ts | 24 - backend-pg/scripts/generate-schema-types.ts | 163 ++++++ backend-pg/src/@types/fastify.d.ts | 9 + backend-pg/src/@types/knex.d.ts | 62 +- .../src/db/migrations/20231128072457_user.ts | 2 +- .../20231128092347_user-encryption-key.ts | 4 +- .../migrations/20231129072939_auth-token.ts | 1 + .../20231130072734_auth-token-session.ts | 2 +- .../migrations/20231201151432_backup-key.ts | 2 +- .../migrations/20231204092737_organization.ts | 24 + .../20231204092747_org-membership.ts | 30 + .../src/db/schemas/auth-token-sessions.ts | 24 + backend-pg/src/db/schemas/auth-tokens.ts | 24 + .../src/db/schemas/backup-private-key.ts | 17 +- backend-pg/src/db/schemas/index.ts | 23 +- backend-pg/src/db/schemas/models.ts | 25 +- .../db/schemas/organization-memberships.ts | 23 + backend-pg/src/db/schemas/organizations.ts | 20 + backend-pg/src/db/schemas/token-session.ts | 17 - backend-pg/src/db/schemas/token.ts | 19 - .../src/db/schemas/user-encryption-key.ts | 26 - .../src/db/schemas/user-encryption-keys.ts | 29 + backend-pg/src/db/schemas/user.ts | 38 -- backend-pg/src/db/schemas/users.ts | 27 + backend-pg/src/lib/errors/index.ts | 2 +- backend-pg/src/lib/knex/index.ts | 10 + backend-pg/src/lib/zod/index.ts | 4 +- .../server/plugins/auth/inject-identity.ts | 75 +++ .../src/server/plugins/auth/verify-auth.ts | 17 + backend-pg/src/server/routes/index.ts | 3 + .../src/server/routes/v1/password-router.ts | 6 + backend-pg/src/server/routes/v2/mfa-router.ts | 15 +- .../src/server/routes/v3/login-router.ts | 2 +- .../src/server/routes/v3/signup-router.ts | 6 +- backend-pg/src/services/auth/auth-dal.ts | 36 +- .../src/services/auth/auth-login-service.ts | 15 +- .../services/auth/auth-password-service.ts | 6 +- .../src/services/auth/auth-signup-service.ts | 16 +- .../src/services/auth/auth-signup-type.ts | 19 - backend-pg/src/services/auth/auth-type.ts | 35 ++ backend-pg/src/services/token/token-dal.ts | 33 +- .../src/services/token/token-service.ts | 29 +- backend/src/utils/authn/helpers/index.ts | 145 +++-- .../AddTagPopoverContent.tsx | 2 +- .../components/basic/dialog/UpgradePlan.tsx | 2 +- .../basic/table/ProjectUsersTable.tsx | 2 +- .../src/components/dashboard/AddTagsMenu.tsx | 2 +- frontend/src/components/dashboard/KeyPair.tsx | 4 +- .../src/components/navigation/NavHeader.tsx | 2 +- .../src/components/signup/UserInfoStep.tsx | 4 +- .../tags/CreateTagModal/CreateTagModal.tsx | 2 +- .../src/components/utilities/config/index.ts | 4 +- .../components/utilities/intercom/intercom.ts | 6 +- .../v2/UpgradePlanModal/UpgradePlanModal.tsx | 4 +- .../OrgPermissionContext.tsx | 2 +- .../OrganizationContext.tsx | 2 +- .../ProjectPermissionContext.tsx | 2 +- .../SubscriptionContext.tsx | 2 +- .../WorkspaceContext/WorkspaceContext.tsx | 2 +- frontend/src/hooks/api/apiKeys/types.ts | 2 +- frontend/src/hooks/api/bots/types.ts | 2 +- .../src/hooks/api/incidentContacts/types.ts | 2 +- .../src/hooks/api/integrationAuth/types.ts | 2 +- frontend/src/hooks/api/integrations/types.ts | 2 +- frontend/src/hooks/api/keys/types.ts | 4 +- frontend/src/hooks/api/organization/types.ts | 10 +- frontend/src/hooks/api/roles/types.ts | 2 +- .../src/hooks/api/secretApproval/types.ts | 2 +- .../api/secretApprovalRequest/queries.tsx | 2 +- .../hooks/api/secretApprovalRequest/types.ts | 6 +- .../src/hooks/api/secretImports/queries.tsx | 2 +- frontend/src/hooks/api/secretImports/types.ts | 2 +- .../src/hooks/api/secretRotation/types.ts | 2 +- .../src/hooks/api/secretSnapshots/queries.tsx | 2 +- .../src/hooks/api/secretSnapshots/types.ts | 2 +- frontend/src/hooks/api/secrets/queries.tsx | 6 +- frontend/src/hooks/api/secrets/types.ts | 6 +- frontend/src/hooks/api/subscriptions/types.ts | 2 +- frontend/src/hooks/api/tags/types.ts | 10 +- frontend/src/hooks/api/trustedIps/types.ts | 2 +- frontend/src/hooks/api/webhooks/types.ts | 2 +- frontend/src/hooks/api/workspace/types.ts | 6 +- .../src/layouts/AdminLayout/AdminLayout.tsx | 4 +- .../AppLayout/components/NavBar/NavBar.tsx | 8 +- .../secret-scanning/getRisksByOrganization.ts | 2 +- frontend/src/pages/dashboard.tsx | 2 +- .../aws-parameter-store/authorize.tsx | 2 +- .../aws-parameter-store/create.tsx | 4 +- .../aws-secret-manager/authorize.tsx | 2 +- .../aws-secret-manager/create.tsx | 4 +- .../integrations/azure-key-vault/create.tsx | 4 +- .../azure-key-vault/oauth2/callback.tsx | 2 +- .../pages/integrations/bitbucket/create.tsx | 4 +- .../bitbucket/oauth2/callback.tsx | 2 +- .../pages/integrations/checkly/authorize.tsx | 2 +- .../src/pages/integrations/checkly/create.tsx | 4 +- .../pages/integrations/circleci/authorize.tsx | 2 +- .../pages/integrations/circleci/create.tsx | 4 +- .../pages/integrations/cloud-66/authorize.tsx | 2 +- .../pages/integrations/cloud-66/create.tsx | 4 +- .../cloudflare-pages/authorize.tsx | 2 +- .../integrations/cloudflare-pages/create.tsx | 4 +- .../cloudflare-workers/authorize.tsx | 2 +- .../cloudflare-workers/create.tsx | 4 +- .../integrations/codefresh/authorize.tsx | 2 +- .../pages/integrations/codefresh/create.tsx | 4 +- .../digital-ocean-app-platform/authorize.tsx | 2 +- .../digital-ocean-app-platform/create.tsx | 4 +- .../pages/integrations/flyio/authorize.tsx | 2 +- .../src/pages/integrations/flyio/create.tsx | 4 +- .../gcp-secret-manager/authorize.tsx | 4 +- .../gcp-secret-manager/create.tsx | 4 +- .../gcp-secret-manager/oauth2/callback.tsx | 2 +- .../src/pages/integrations/github/create.tsx | 4 +- .../integrations/github/oauth2/callback.tsx | 2 +- .../pages/integrations/gitlab/authorize.tsx | 2 +- .../src/pages/integrations/gitlab/create.tsx | 4 +- .../integrations/gitlab/oauth2/callback.tsx | 2 +- .../hashicorp-vault/authorize.tsx | 2 +- .../integrations/hashicorp-vault/create.tsx | 4 +- .../integrations/hasura-cloud/authorize.tsx | 2 +- .../integrations/hasura-cloud/create.tsx | 4 +- .../src/pages/integrations/heroku/create.tsx | 4 +- .../integrations/heroku/oauth2/callback.tsx | 2 +- .../integrations/laravel-forge/authorize.tsx | 2 +- .../integrations/laravel-forge/create.tsx | 4 +- .../src/pages/integrations/netlify/create.tsx | 4 +- .../integrations/netlify/oauth2/callback.tsx | 2 +- .../integrations/northflank/authorize.tsx | 2 +- .../pages/integrations/northflank/create.tsx | 4 +- .../pages/integrations/qovery/authorize.tsx | 2 +- .../src/pages/integrations/qovery/create.tsx | 4 +- .../pages/integrations/railway/authorize.tsx | 2 +- .../src/pages/integrations/railway/create.tsx | 4 +- .../pages/integrations/render/authorize.tsx | 2 +- .../src/pages/integrations/render/create.tsx | 4 +- .../pages/integrations/supabase/authorize.tsx | 2 +- .../pages/integrations/supabase/create.tsx | 4 +- .../pages/integrations/teamcity/authorize.tsx | 2 +- .../pages/integrations/teamcity/create.tsx | 4 +- .../terraform-cloud/authorize.tsx | 2 +- .../integrations/terraform-cloud/create.tsx | 4 +- .../pages/integrations/travisci/authorize.tsx | 2 +- .../pages/integrations/travisci/create.tsx | 4 +- .../src/pages/integrations/vercel/create.tsx | 4 +- .../integrations/vercel/oauth2/callback.tsx | 2 +- .../pages/integrations/windmill/authorize.tsx | 2 +- .../pages/integrations/windmill/create.tsx | 4 +- .../pages/org/[id]/secret-scanning/index.tsx | 6 +- frontend/src/pages/signup/index.tsx | 4 +- frontend/src/pages/signupinvite.tsx | 4 +- .../IntegrationPage.utils.tsx | 10 +- .../IntegrationsPage/IntegrationsPage.tsx | 19 +- .../IntegrationsSection.tsx | 2 +- frontend/src/views/Login/Login.utils.tsx | 2 +- .../components/IPAllowlistModal.tsx | 6 +- .../components/IPAllowlistSection.tsx | 4 +- .../components/IPAllowlistTable.tsx | 10 +- .../AddServiceTokenModal.tsx | 6 +- .../ServiceTokenSection/ServiceTokenTable.tsx | 8 +- .../AddServiceTokenV3Modal.tsx | 539 ++++++++++++++++++ .../ServiceTokenV3Table.tsx | 218 +++++++ .../SecretApprovalPage/SecretApprovalPage.tsx | 2 +- .../SecretApprovalPolicyList.tsx | 4 +- .../SecretApprovalRequestChangeItem.tsx | 14 +- .../views/SecretMainPage/SecretMainPage.tsx | 2 +- .../components/ActionBar/ActionBar.tsx | 10 +- .../components/PitDrawer/PitDrawer.tsx | 12 +- .../SecretDropzone/CopySecretsFromBoard.tsx | 6 +- .../SecretImportListView.tsx | 6 +- .../SecretListView/SecretDetaiSidebar.tsx | 16 +- .../components/SecretListView/SecretItem.tsx | 14 +- .../SecretListView/SecretListView.utils.ts | 2 +- .../components/SnapshotView/SecretItem.tsx | 8 +- .../components/SnapshotView/SnapshotView.tsx | 4 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 4 +- .../ProjectIndexSecretsSection.tsx | 10 +- .../SecretOverviewTableRow.tsx | 2 +- .../SecretRotationPage/SecretRotationPage.tsx | 18 +- .../steps/RotationOutputForm.tsx | 10 +- .../components/SecretScanningLogsTable.tsx | 2 +- .../BillingCloudTab/CurrentPlanSection.tsx | 2 +- .../BillingCloudTab/ManagePlansTable.tsx | 6 +- .../BillingCloudTab/PreviewSection.tsx | 8 +- .../BillingSelfHostedTab/LicensesSection.tsx | 6 +- .../components/OrgAuthTab/OrgSSOSection.tsx | 10 +- .../components/OrgAuthTab/SSOModal.tsx | 8 +- .../OrgDeleteSection/OrgDeleteSection.tsx | 4 +- .../AddOrgIncidentContactModal.tsx | 4 +- .../OrgIncidentContactsTable.tsx | 6 +- .../OrgNameChangeSection.tsx | 4 +- .../APIKeyV2Section/APIKeyV2Table.tsx | 8 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 4 +- .../admin/DashboardPage/DashboardPage.tsx | 4 +- 196 files changed, 1871 insertions(+), 624 deletions(-) create mode 100644 backend-pg/scripts/generate-schema-types.ts create mode 100644 backend-pg/src/db/migrations/20231204092737_organization.ts create mode 100644 backend-pg/src/db/migrations/20231204092747_org-membership.ts create mode 100644 backend-pg/src/db/schemas/auth-token-sessions.ts create mode 100644 backend-pg/src/db/schemas/auth-tokens.ts create mode 100644 backend-pg/src/db/schemas/organization-memberships.ts create mode 100644 backend-pg/src/db/schemas/organizations.ts delete mode 100644 backend-pg/src/db/schemas/token-session.ts delete mode 100644 backend-pg/src/db/schemas/token.ts delete mode 100644 backend-pg/src/db/schemas/user-encryption-key.ts create mode 100644 backend-pg/src/db/schemas/user-encryption-keys.ts delete mode 100644 backend-pg/src/db/schemas/user.ts create mode 100644 backend-pg/src/db/schemas/users.ts create mode 100644 backend-pg/src/lib/knex/index.ts create mode 100644 backend-pg/src/server/plugins/auth/inject-identity.ts create mode 100644 backend-pg/src/server/plugins/auth/verify-auth.ts create mode 100644 backend-pg/src/services/auth/auth-type.ts create mode 100644 frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx create mode 100644 frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx diff --git a/backend-pg/.eslintrc.js b/backend-pg/.eslintrc.js index 5d564d416..6d6217634 100644 --- a/backend-pg/.eslintrc.js +++ b/backend-pg/.eslintrc.js @@ -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", { diff --git a/backend-pg/package.json b/backend-pg/package.json index 6499ab5e9..93e94eac3 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -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", diff --git a/backend-pg/scripts/create-backend-file.ts b/backend-pg/scripts/create-backend-file.ts index 2ea0a773a..3b1cb96c8 100644 --- a/backend-pg/scripts/create-backend-file.ts +++ b/backend-pg/scripts/create-backend-file.ts @@ -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; -export type T${pascalCase}Insert = Omit; -export type T${pascalCase}Update = Partial>; -` - ); } diff --git a/backend-pg/scripts/generate-schema-types.ts b/backend-pg/scripts/generate-schema-types.ts new file mode 100644 index 000000000..5c279f7f3 --- /dev/null +++ b/backend-pg/scripts/generate-schema-types.ts @@ -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; +export type T${pascalCase}Insert = Omit; +export type T${pascalCase}Update = Partial>; +` + ); + + // 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(); diff --git a/backend-pg/src/@types/fastify.d.ts b/backend-pg/src/@types/fastify.d.ts index 33b744166..01833e08f 100644 --- a/backend-pg/src/@types/fastify.d.ts +++ b/backend-pg/src/@types/fastify.d.ts @@ -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 diff --git a/backend-pg/src/@types/knex.d.ts b/backend-pg/src/@types/knex.d.ts index 8b3776fe3..09e8935f5 100644 --- a/backend-pg/src/@types/knex.d.ts +++ b/backend-pg/src/@types/knex.d.ts @@ -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 } { - [TableName.Users]: Knex.CompositeTableType; + [TableName.Users]: Knex.CompositeTableType; [TableName.UserEncryptionKey]: Knex.CompositeTableType< - TUserEncryptionKey, - TUserEncryptionKeyInsert, - TUserEncryptionKeyUpdate + TUserEncryptionKeys, + TUserEncryptionKeysInsert, + TUserEncryptionKeysUpdate + >; + [TableName.AuthTokens]: Knex.CompositeTableType< + TAuthTokens, + TAuthTokensInsert, + TAuthTokensUpdate >; - [TableName.AuthTokens]: Knex.CompositeTableType; [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 >; } } diff --git a/backend-pg/src/db/migrations/20231128072457_user.ts b/backend-pg/src/db/migrations/20231128072457_user.ts index 9fd26031c..f0572824e 100644 --- a/backend-pg/src/db/migrations/20231128072457_user.ts +++ b/backend-pg/src/db/migrations/20231128072457_user.ts @@ -12,7 +12,7 @@ export async function up(knex: Knex): Promise { 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); diff --git a/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts b/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts index b9b312456..153aeb077 100644 --- a/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts +++ b/backend-pg/src/db/migrations/20231128092347_user-encryption-key.ts @@ -6,10 +6,10 @@ export async function up(knex: Knex): Promise { 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(); diff --git a/backend-pg/src/db/migrations/20231129072939_auth-token.ts b/backend-pg/src/db/migrations/20231129072939_auth-token.ts index 0b186b8b0..ed23ebe9d 100644 --- a/backend-pg/src/db/migrations/20231129072939_auth-token.ts +++ b/backend-pg/src/db/migrations/20231129072939_auth-token.ts @@ -6,6 +6,7 @@ export async function up(knex: Knex): Promise { 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(); diff --git a/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts b/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts index 4acde142f..9d5b98910 100644 --- a/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts +++ b/backend-pg/src/db/migrations/20231130072734_auth-token-session.ts @@ -7,7 +7,7 @@ export async function up(knex: Knex): Promise { 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); diff --git a/backend-pg/src/db/migrations/20231201151432_backup-key.ts b/backend-pg/src/db/migrations/20231201151432_backup-key.ts index 2f7ff9665..3672fface 100644 --- a/backend-pg/src/db/migrations/20231201151432_backup-key.ts +++ b/backend-pg/src/db/migrations/20231201151432_backup-key.ts @@ -6,7 +6,7 @@ export async function up(knex: Knex): Promise { 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(); diff --git a/backend-pg/src/db/migrations/20231204092737_organization.ts b/backend-pg/src/db/migrations/20231204092737_organization.ts new file mode 100644 index 000000000..45badacb9 --- /dev/null +++ b/backend-pg/src/db/migrations/20231204092737_organization.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + 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 { + await knex.schema.dropTableIfExists(TableName.Organization); + await dropOnUpdateTrigger(knex, TableName.Organization); +} diff --git a/backend-pg/src/db/migrations/20231204092747_org-membership.ts b/backend-pg/src/db/migrations/20231204092747_org-membership.ts new file mode 100644 index 000000000..20e6e2fef --- /dev/null +++ b/backend-pg/src/db/migrations/20231204092747_org-membership.ts @@ -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 { + 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 { + await knex.schema.dropTableIfExists(TableName.OrgMembership); + await dropOnUpdateTrigger(knex, TableName.OrgMembership); +} diff --git a/backend-pg/src/db/schemas/auth-token-sessions.ts b/backend-pg/src/db/schemas/auth-token-sessions.ts new file mode 100644 index 000000000..dd4fd9416 --- /dev/null +++ b/backend-pg/src/db/schemas/auth-token-sessions.ts @@ -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; +export type TAuthTokenSessionsInsert = Omit; +export type TAuthTokenSessionsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/auth-tokens.ts b/backend-pg/src/db/schemas/auth-tokens.ts new file mode 100644 index 000000000..a47146170 --- /dev/null +++ b/backend-pg/src/db/schemas/auth-tokens.ts @@ -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; +export type TAuthTokensInsert = Omit; +export type TAuthTokensUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/backup-private-key.ts b/backend-pg/src/db/schemas/backup-private-key.ts index 73bcc59a1..bb2b8a048 100644 --- a/backend-pg/src/db/schemas/backup-private-key.ts +++ b/backend-pg/src/db/schemas/backup-private-key.ts @@ -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; diff --git a/backend-pg/src/db/schemas/index.ts b/backend-pg/src/db/schemas/index.ts index 33c7fea0f..b63b35e14 100644 --- a/backend-pg/src/db/schemas/index.ts +++ b/backend-pg/src/db/schemas/index.ts @@ -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"; diff --git a/backend-pg/src/db/schemas/models.ts b/backend-pg/src/db/schemas/models.ts index 8c9cc0daa..9be605cd2 100644 --- a/backend-pg/src/db/schemas/models.ts +++ b/backend-pg/src/db/schemas/models.ts @@ -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" } diff --git a/backend-pg/src/db/schemas/organization-memberships.ts b/backend-pg/src/db/schemas/organization-memberships.ts new file mode 100644 index 000000000..97cf01dbd --- /dev/null +++ b/backend-pg/src/db/schemas/organization-memberships.ts @@ -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; +export type TOrganizationMembershipsInsert = Omit; +export type TOrganizationMembershipsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/organizations.ts b/backend-pg/src/db/schemas/organizations.ts new file mode 100644 index 000000000..a79b7c57b --- /dev/null +++ b/backend-pg/src/db/schemas/organizations.ts @@ -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; +export type TOrganizationsInsert = Omit; +export type TOrganizationsUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/token-session.ts b/backend-pg/src/db/schemas/token-session.ts deleted file mode 100644 index 196871aa0..000000000 --- a/backend-pg/src/db/schemas/token-session.ts +++ /dev/null @@ -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; -export type TTokenSessionInsert = Omit; -export type TTokenSessionUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/token.ts b/backend-pg/src/db/schemas/token.ts deleted file mode 100644 index 401d5db87..000000000 --- a/backend-pg/src/db/schemas/token.ts +++ /dev/null @@ -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; -export type TTokenInsert = Omit; -export type TTokenUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/user-encryption-key.ts b/backend-pg/src/db/schemas/user-encryption-key.ts deleted file mode 100644 index 103ee5bd2..000000000 --- a/backend-pg/src/db/schemas/user-encryption-key.ts +++ /dev/null @@ -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; -export type TUserEncryptionKeyInsert = Omit, TImmutableDBKeys>; -export type TUserEncryptionKeyUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/user-encryption-keys.ts b/backend-pg/src/db/schemas/user-encryption-keys.ts new file mode 100644 index 000000000..5d71c7f57 --- /dev/null +++ b/backend-pg/src/db/schemas/user-encryption-keys.ts @@ -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; +export type TUserEncryptionKeysInsert = Omit; +export type TUserEncryptionKeysUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/user.ts b/backend-pg/src/db/schemas/user.ts deleted file mode 100644 index 59428f08d..000000000 --- a/backend-pg/src/db/schemas/user.ts +++ /dev/null @@ -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; -export type TUserInsert = Omit; -export type TUserUpdate = Partial>; diff --git a/backend-pg/src/db/schemas/users.ts b/backend-pg/src/db/schemas/users.ts new file mode 100644 index 000000000..7e12344e1 --- /dev/null +++ b/backend-pg/src/db/schemas/users.ts @@ -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; +export type TUsersInsert = Omit; +export type TUsersUpdate = Partial>; diff --git a/backend-pg/src/lib/errors/index.ts b/backend-pg/src/lib/errors/index.ts index ab090fa46..2786194e9 100644 --- a/backend-pg/src/lib/errors/index.ts +++ b/backend-pg/src/lib/errors/index.ts @@ -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; diff --git a/backend-pg/src/lib/knex/index.ts b/backend-pg/src/lib/knex/index.ts new file mode 100644 index 000000000..d480f7301 --- /dev/null +++ b/backend-pg/src/lib/knex/index.ts @@ -0,0 +1,10 @@ +import { Knex } from "knex"; + +export const withTransaction = (db: Knex, dal: K) => ({ + transaction: async (cb: (tx: Knex) => T) => + db.transaction(async (trx) => { + const res = await cb(trx); + return res; + }), + ...dal +}); diff --git a/backend-pg/src/lib/zod/index.ts b/backend-pg/src/lib/zod/index.ts index 3b04f033e..331a5fbb6 100644 --- a/backend-pg/src/lib/zod/index.ts +++ b/backend-pg/src/lib/zod/index.ts @@ -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 = ( +export const zpStr = ( schema: T, opt: { stripNull: boolean } = { stripNull: true } ) => diff --git a/backend-pg/src/server/plugins/auth/inject-identity.ts b/backend-pg/src/server/plugins/auth/inject-identity.ts new file mode 100644 index 000000000..6fcff596e --- /dev/null +++ b/backend-pg/src/server/plugins/auth/inject-identity.ts @@ -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" }); + } + }); +}; diff --git a/backend-pg/src/server/plugins/auth/verify-auth.ts b/backend-pg/src/server/plugins/auth/verify-auth.ts new file mode 100644 index 000000000..90432e380 --- /dev/null +++ b/backend-pg/src/server/plugins/auth/verify-auth.ts @@ -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 = + (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` }); + } + }; diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index 3488843ad..8d81b238a 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -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" }); diff --git a/backend-pg/src/server/routes/v1/password-router.ts b/backend-pg/src/server/routes/v1/password-router.ts index d506f9814..92da5eeea 100644 --- a/backend-pg/src/server/routes/v1/password-router.ts +++ b/backend-pg/src/server/routes/v1/password-router.ts @@ -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({ diff --git a/backend-pg/src/server/routes/v2/mfa-router.ts b/backend-pg/src/server/routes/v2/mfa-router.ts index b5fbfadcb..71f841ad7 100644 --- a/backend-pg/src/server/routes/v2/mfa-router.ts +++ b/backend-pg/src/server/routes/v2/mfa-router.ts @@ -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(), diff --git a/backend-pg/src/server/routes/v3/login-router.ts b/backend-pg/src/server/routes/v3/login-router.ts index 83fa5f89e..5af1ba448 100644 --- a/backend-pg/src/server/routes/v3/login-router.ts +++ b/backend-pg/src/server/routes/v3/login-router.ts @@ -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(), diff --git a/backend-pg/src/server/routes/v3/signup-router.ts b/backend-pg/src/server/routes/v3/signup-router.ts index ff6ec2036..731b79548 100644 --- a/backend-pg/src/server/routes/v3/signup-router.ts +++ b/backend-pg/src/server/routes/v3/signup-router.ts @@ -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() }) } diff --git a/backend-pg/src/services/auth/auth-dal.ts b/backend-pg/src/services/auth/auth-dal.ts index eb539f911..004cde5b4 100644 --- a/backend-pg/src/services/auth/auth-dal.ts +++ b/backend-pg/src/services/auth/auth-dal.ts @@ -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; export const authDalFactory = (db: TDbClient) => { // getters - const getUserByEmail = async (email: string): Promise => + const getUserByEmail = async (email: string): Promise => db(TableName.Users).where({ email }).select("*").first(); - const getUserById = async (userId: string): Promise => + const getUserById = async (userId: string): Promise => 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 = {} - ): Promise => { + data: Partial = {} + ): Promise => { 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 = {} - ): Promise => { + data: Partial = {} + ): Promise => { 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 = {}, + data: Partial = {}, tx?: Knex - ): Promise => { + ): Promise => { 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 = {}, + data: Partial = {}, tx?: Knex - ): Promise => { + ): Promise => { 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, + data: Partial, 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 (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 - }; + }); }; diff --git a/backend-pg/src/services/auth/auth-login-service.ts b/backend-pg/src/services/auth/auth-login-service.ts index b1850e789..4b44adcc6 100644 --- a/backend-pg/src/services/auth/auth-login-service.ts +++ b/backend-pg/src/services/auth/auth-login-service.ts @@ -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({ diff --git a/backend-pg/src/services/auth/auth-password-service.ts b/backend-pg/src/services/auth/auth-password-service.ts index ea41198a6..cf748958e 100644 --- a/backend-pg/src/services/auth/auth-password-service.ts +++ b/backend-pg/src/services/auth/auth-password-service.ts @@ -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; }; diff --git a/backend-pg/src/services/auth/auth-signup-service.ts b/backend-pg/src/services/auth/auth-signup-service.ts index ef2fafa3b..adefb385a 100644 --- a/backend-pg/src/services/auth/auth-signup-service.ts +++ b/backend-pg/src/services/auth/auth-signup-service.ts @@ -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, diff --git a/backend-pg/src/services/auth/auth-signup-type.ts b/backend-pg/src/services/auth/auth-signup-type.ts index ddfa15336..2d1310a24 100644 --- a/backend-pg/src/services/auth/auth-signup-type.ts +++ b/backend-pg/src/services/auth/auth-signup-type.ts @@ -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; diff --git a/backend-pg/src/services/auth/auth-type.ts b/backend-pg/src/services/auth/auth-type.ts new file mode 100644 index 000000000..b80cb6d42 --- /dev/null +++ b/backend-pg/src/services/auth/auth-type.ts @@ -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; +}; diff --git a/backend-pg/src/services/token/token-dal.ts b/backend-pg/src/services/token/token-dal.ts index 823508610..16db26468 100644 --- a/backend-pg/src/services/token/token-dal.ts +++ b/backend-pg/src/services/token/token-dal.ts @@ -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 => { + }: TUpsertTokenForUserDalDTO): Promise => { 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 => + }: TGetTokenForUserDalDTO): Promise => db(TableName.AuthTokens).where({ userId, type }).first(); + const getTokenSession = async ( + userId: string, + ip: string, + userAgent: string + ): Promise => + 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 => + }: TDeleteTokenForUserDalDTO): Promise => 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 => - db(TableName.AuthTokenSession).where({ userId, ip, userAgent }).first(); - const insertTokenSession = async ( userId: string, ip: string, userAgent: string - ): Promise => { + ): Promise => { 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 => { + ): Promise => { 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, diff --git a/backend-pg/src/services/token/token-service.ts b/backend-pg/src/services/token/token-service.ts index 1b7c80c43..37f577a7d 100644 --- a/backend-pg/src/services/token/token-service.ts +++ b/backend-pg/src/services/token/token-service.ts @@ -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; +export type TAuthTokenServiceFactory = ReturnType; 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 => { + }: TValidateTokenForUserDTO): Promise => { 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 => { + }: TIssueAuthTokenDTO): Promise => { 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 => tokenDal.incrementVersion(userId, sessionId); - - const createJwtToken = ( - payload: string | Buffer | object, - secret: string, - options?: SignOptions - ) => jwt.sign(payload, secret, options); + ): Promise => tokenDal.incrementVersion(userId, sessionId); return { createTokenForUser, validateTokenForUser, - createJwtToken, getUserTokenSession, - clearTokenSessionById + clearTokenSessionById, + getUserTokenSessionById }; }; diff --git a/backend/src/utils/authn/helpers/index.ts b/backend/src/utils/authn/helpers/index.ts index 7f3998e57..45849c989 100644 --- a/backend/src/utils/authn/helpers/index.ts +++ b/backend/src/utils/authn/helpers/index.ts @@ -16,19 +16,19 @@ import { getUserAgentType } from "../../posthog"; export * from "./authDataExtractors"; interface ExtractAuthModeParams { - headers: { [key: string]: string | string[] | undefined } + headers: { [key: string]: string | string[] | undefined }; } interface ExtractAuthModeReturn { - authMode: AuthMode; - authTokenValue: string; + authMode: AuthMode; + authTokenValue: string; } interface GetAuthDataParams { - authMode: AuthMode; - authTokenValue: string; - ipAddress: string; - userAgent: string; + authMode: AuthMode; + authTokenValue: string; + ipAddress: string; + userAgent: string; } /** @@ -44,33 +44,30 @@ interface GetAuthDataParams { * @throws {UnauthorizedError} Throws an error if no applicable authMode is found. */ export const extractAuthMode = async ({ - headers + headers }: ExtractAuthModeParams): Promise => { + const apiKey = headers["x-api-key"] as string; + const authHeader = headers["authorization"] as string; - const apiKey = headers["x-api-key"] as string; - const authHeader = headers["authorization"] as string; - - if (apiKey) { - return { authMode: AuthMode.API_KEY, authTokenValue: apiKey }; - } - - if (!authHeader) throw UnauthorizedRequestError({ - message: "Failed to authenticate unknown authentication method" + if (apiKey) { + return { authMode: AuthMode.API_KEY, authTokenValue: apiKey }; + } + + if (!authHeader) + throw UnauthorizedRequestError({ + message: "Failed to authenticate unknown authentication method" }); - if (!authHeader.startsWith("Bearer ")) throw UnauthorizedRequestError({ - message: "Failed to authenticate unknown authentication method" + if (!authHeader.startsWith("Bearer ")) + throw UnauthorizedRequestError({ + message: "Failed to authenticate unknown authentication method" }); - const authTokenValue = authHeader.slice(7); - - if (authTokenValue.startsWith("st.")) { - return { authMode: AuthMode.SERVICE_TOKEN, authTokenValue }; - } + const authTokenValue = authHeader.slice(7); - const decodedToken = ( - jwt.verify(authTokenValue, await getAuthSecret()) - ); + if (authTokenValue.startsWith("st.")) { + return { authMode: AuthMode.SERVICE_TOKEN, authTokenValue }; + } switch (decodedToken.authTokenType) { case AuthTokenType.ACCESS_TOKEN: @@ -87,13 +84,18 @@ export const extractAuthMode = async ({ } export const getAuthData = async ({ - authMode, - authTokenValue, - ipAddress, - userAgent + authMode, + authTokenValue, + ipAddress, + userAgent }: GetAuthDataParams): Promise => { + const userAgentType = getUserAgentType(userAgent); - const userAgentType = getUserAgentType(userAgent); + switch (authMode) { + case AuthMode.SERVICE_TOKEN: { + const serviceTokenData = await validateServiceTokenV2({ + authTokenValue + }); switch (authMode) { case AuthMode.SERVICE_TOKEN: { @@ -193,4 +195,81 @@ export const getAuthData = async ({ } } } -} \ No newline at end of file + case AuthMode.SERVICE_ACCESS_TOKEN: { + const serviceTokenData = await validateServiceTokenV3({ + authTokenValue + }); + + return { + actor: { + type: ActorType.SERVICE_V3, + metadata: { + serviceId: serviceTokenData._id.toString(), + name: serviceTokenData.name + } + }, + authPayload: serviceTokenData, + ipAddress, + userAgent, + userAgentType + }; + } + case AuthMode.API_KEY: { + const user = await validateAPIKey({ + authTokenValue + }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress, + userAgent, + userAgentType + }; + } + case AuthMode.API_KEY_V2: { + const user = await validateAPIKeyV2({ + authTokenValue + }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress, + userAgent, + userAgentType + }; + } + case AuthMode.JWT: { + const user = await validateJWT({ + authTokenValue + }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress, + userAgent, + userAgentType + }; + } + } +}; diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx index 77ca8c24f..d69cc76c8 100644 --- a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -41,7 +41,7 @@ const AddTagPopoverContent = ({
{wsTags?.map((wsTag: WsTag) => (
handleSelectTag(wsTag)} onMouseEnter={() => handleTagOnMouseEnter(wsTag)} diff --git a/frontend/src/components/basic/dialog/UpgradePlan.tsx b/frontend/src/components/basic/dialog/UpgradePlan.tsx index 0fd9af404..517d5a233 100644 --- a/frontend/src/components/basic/dialog/UpgradePlan.tsx +++ b/frontend/src/components/basic/dialog/UpgradePlan.tsx @@ -64,7 +64,7 @@ const UpgradePlanModal = ({ diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 5838aaf41..a00eb6543 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -41,7 +41,7 @@ type EnvironmentProps = { const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => { const { currentWorkspace } = useWorkspace(); const { subscription } = useSubscription(); - const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); + const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? ""); const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace(); const { mutateAsync: uploadWsKeyMutateAsync } = useUploadWsKey(); diff --git a/frontend/src/components/dashboard/AddTagsMenu.tsx b/frontend/src/components/dashboard/AddTagsMenu.tsx index e97c979a9..dfa902b87 100644 --- a/frontend/src/components/dashboard/AddTagsMenu.tsx +++ b/frontend/src/components/dashboard/AddTagsMenu.tsx @@ -37,7 +37,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, id }: { allTags: Tag[]; > {allTags?.map((tag) => { return ( - + +
+ ( + + + + )} + /> +
+ ( + onChange(isChecked)} + isChecked={value} + > + Refresh Token Rotation + + )} + /> +

When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.

+
+
+ + +
+ + +
+ + ) : ( +
+

{newServiceTokenJSON}

+ + + + Click to copy + + +
+ )} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use IP allowlisting if you switch to Infisical's Pro plan." + /> + + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx new file mode 100644 index 000000000..43fb841e3 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/ServiceTokenTab/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -0,0 +1,218 @@ +import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + EmptyState, + IconButton, + Switch, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context"; +import { + useGetWorkspaceServiceTokenDataV3, + useUpdateServiceTokenV3 +} from "@app/hooks/api"; +import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types" +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>, + data?: { + serviceTokenDataId?: string; + name?: string; + role?: string; + customRole?: { + name: string; + slug: string; + }; + trustedIps?: ServiceTokenV3TrustedIp[]; + accessTokenTTL?: number; + isRefreshTokenRotationEnabled?: boolean; + } + ) => void; + }; + +export const ServiceTokenV3Table = ({ + handlePopUpOpen +}: Props) => { + const { createNotification } = useNotificationContext(); + const { currentWorkspace } = useWorkspace(); + const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?.id || ""); + const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); + + const handleToggleServiceTokenDataStatus = async ({ + serviceTokenDataId, + isActive + }: { + serviceTokenDataId: string; + isActive: boolean; + }) => { + try { + await updateMutateAsync({ + serviceTokenDataId, + isActive + }); + + createNotification({ + text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`, + type: "success" + }); + } catch (err) { + console.log(err); + createNotification({ + text: `Failed to ${isActive ? "enable" : "disable"} service token v3`, + type: "error" + }); + } + } + + return ( + + + + + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ + id, + name, + isActive, + role, + customRole, + trustedIps, + createdAt, + expiresAt, + accessTokenTTL, + isRefreshTokenRotationEnabled + }) => { + return ( + + + + + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
NameStatusRoleTrusted IPsAccess Token TTLCreated AtValid Until +
{name} + + {(isAllowed) => ( + handleToggleServiceTokenDataStatus({ + serviceTokenDataId: id, + isActive: value + })} + isChecked={isActive} + isDisabled={!isAllowed} + > +

{isActive ? "Active" : "Inactive"}

+
+ )} +
+
{customRole?.slug ?? role} + {trustedIps.map(({ + id: trustedIpId, + ipAddress, + prefix + }) => { + return ( +

+ {`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`} +

+ ); + })} +
{accessTokenTTL}{format(new Date(createdAt), "yyyy-MM-dd")}{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"} + + {(isAllowed) => ( + { + handlePopUpOpen("serviceTokenV3", { + serviceTokenDataId: id, + name, + role, + customRole, + trustedIps, + accessTokenTTL, + isRefreshTokenRotationEnabled + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + isDisabled={!isAllowed} + > + + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("deleteServiceTokenV3", { + serviceTokenDataId: id, + name + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index eee81608c..5a9649897 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -15,7 +15,7 @@ enum TabSection { export const SecretApprovalPage = () => { const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?._id || ""; + const workspaceId = currentWorkspace?.id || ""; return (
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index 0eb0a849d..2ea6a67f8 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -59,7 +59,7 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); const handleDeletePolicy = async () => { - const { _id: id } = popUp.deletePolicy.data as TSecretApprovalPolicy; + const { id: id } = popUp.deletePolicy.data as TSecretApprovalPolicy; try { await deleteSecretApprovalPolicy({ workspaceId, @@ -138,7 +138,7 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { handlePopUpOpen("secretPolicyForm", policy)} onDelete={() => handlePopUpOpen("deletePolicy", policy)} diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index d921f9975..99d06c9c1 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -54,7 +54,7 @@ export const SecretApprovalRequestChangeItem = ({ // meaning request has changed const isStale = (secretVersion?.version || 1) < presentSecretVersionNumber; const itemConflict = - hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?._id); + hasMerged && conflicts.find((el) => el.op === op && el.secretId === newVersion?.id); const hasConflict = Boolean(itemConflict); return ( @@ -97,10 +97,10 @@ export const SecretApprovalRequestChangeItem = ({ {secretVersion?.comment} - {secretVersion?.tags?.map(({ name, _id: tagId, tagColor }) => ( + {secretVersion?.tags?.map(({ name, id: tagId, tagColor }) => (
{newVersion?.secretComment} - {newVersion?.tags?.map(({ name, _id: tagId, tagColor }) => ( + {newVersion?.tags?.map(({ name, id: tagId, tagColor }) => (
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map( - ({ name, _id: tagId, tagColor }) => ( + ({ name, id: tagId, tagColor }) => (
{ // env slug const environment = router.query.env as string; - const workspaceId = currentWorkspace?._id || ""; + const workspaceId = currentWorkspace?.id || ""; const secretPath = (router.query.secretPath as string) || "/"; const canReadSecret = permission.can( ProjectPermissionActions.Read, diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index be511486d..e530ea68f 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -163,7 +163,7 @@ export const ActionBar = ({ }; const handleSecretBulkDelete = async () => { - const bulkDeletedSecrets = secrets.filter(({ _id }) => Boolean(selectedSecrets?.[_id])); + const bulkDeletedSecrets = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id])); try { await deleteBatchSecretV3({ secretPath, @@ -233,14 +233,14 @@ export const ActionBar = ({ Apply tags to filter secrets - {tags.map(({ _id, name, tagColor }) => ( + {tags.map(({ id, name, tagColor }) => ( { evt.preventDefault(); - onToggleTagFilter(_id); + onToggleTagFilter(id); }} - key={_id} - icon={filter?.tags[_id] && } + key={id} + icon={filter?.tags[id] && } iconPos="right" >
diff --git a/frontend/src/views/SecretMainPage/components/PitDrawer/PitDrawer.tsx b/frontend/src/views/SecretMainPage/components/PitDrawer/PitDrawer.tsx index 73a0028b9..9b844887a 100644 --- a/frontend/src/views/SecretMainPage/components/PitDrawer/PitDrawer.tsx +++ b/frontend/src/views/SecretMainPage/components/PitDrawer/PitDrawer.tsx @@ -41,26 +41,26 @@ export const PitDrawer = ({
{secretSnaphots?.pages?.map((group, i) => ( - {group.map(({ _id, createdAt }, index) => ( + {group.map(({ id, createdAt }, index) => ( ))} diff --git a/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx b/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx index 12eaf840c..9606f8141 100644 --- a/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx @@ -240,14 +240,14 @@ export const CopySecretsFromBoard = ({ {secrets ?.filter(({ key }) => key.toLowerCase().includes(searchFilter.toLowerCase())) - ?.map(({ _id, key, value: secVal }) => ( + ?.map(({ id, key, value: secVal }) => ( ( onChange(isChecked ? secVal : "")} > diff --git a/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportListView.tsx b/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportListView.tsx index 3b8f91235..6b216b8f8 100644 --- a/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportListView.tsx @@ -132,12 +132,12 @@ export const SecretImportListView = ({ const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport ?.data as TDeleteSecretImport; try { - if (secretImports?._id) { + if (secretImports?.id) { await deleteSecretImport({ workspaceId, environment, directory: secretPath, - id: secretImports?._id, + id: secretImports?.id, secretImportEnv: importEnv, secretImportPath: impSecPath }); @@ -166,7 +166,7 @@ export const SecretImportListView = ({ workspaceId, environment, directory: secretPath, - id: secretImports?._id || "", + id: secretImports?.id || "", secretImports: newImportOrder }); } diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx index 0a9f38bc4..1c8063f43 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx @@ -49,7 +49,7 @@ type Props = { onDeleteSecret: () => void; onSaveSecret: ( orgSec: DecryptedSecret, - modSec: Omit & { tags: { _id: string }[] }, + modSec: Omit & { tags: { id: string }[] }, cb?: () => void ) => Promise; tags: WsTag[]; @@ -98,7 +98,7 @@ export const SecretDetailSidebar = ({ }); const selectedTags = watch("tags", []); const selectedTagsGroupById = selectedTags.reduce>( - (prev, curr) => ({ ...prev, [curr._id]: true }), + (prev, curr) => ({ ...prev, [curr.id]: true }), {} ); @@ -109,7 +109,7 @@ export const SecretDetailSidebar = ({ const { data: secretVersion } = useGetSecretVersion({ limit: 10, offset: 0, - secretId: secret?._id, + secretId: secret?.id, decryptFileKey }); @@ -133,8 +133,8 @@ export const SecretDetailSidebar = ({ }; const handleTagSelect = (tag: WsTag) => { - if (selectedTagsGroupById?.[tag._id]) { - const tagPos = selectedTags.findIndex(({ _id }) => _id === tag._id); + if (selectedTagsGroupById?.[tag.id]) { + const tagPos = selectedTags.findIndex(({ id }) => id === tag.id); if (tagPos !== -1) { remove(tagPos); } @@ -227,7 +227,7 @@ export const SecretDetailSidebar = ({ )}
- {fields.map(({ tagColor, id: formId, name, _id }) => ( + {fields.map(({ tagColor, id: formId, name, id }) => ( id === _id); + const tag = tags?.find(({ id: id }) => id === id); if (tag) handleTagSelect(tag); }} > @@ -269,7 +269,7 @@ export const SecretDetailSidebar = ({ Apply tags to this secrets {tags.map((tag) => { - const { _id: tagId, name, tagColor } = tag; + const { id: tagId, name, tagColor } = tag; const isSelected = selectedTagsGroupById?.[tagId]; return ( diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx index 9fdd1e3f8..1050923b3 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx @@ -56,7 +56,7 @@ type Props = { secret: DecryptedSecret; onSaveSecret: ( orgSec: DecryptedSecret, - modSec: Omit & { tags: { _id: string }[] }, + modSec: Omit & { tags: { id: string }[] }, cb?: () => void ) => Promise; onDeleteSecret: (sec: DecryptedSecret) => void; @@ -117,7 +117,7 @@ export const SecretItem = memo( const selectedTags = watch("tags", []); const selectedTagsGroupById = selectedTags.reduce>( - (prev, curr) => ({ ...prev, [curr._id]: true }), + (prev, curr) => ({ ...prev, [curr.id]: true }), {} ); const { fields, append, remove } = useFieldArray({ @@ -168,8 +168,8 @@ export const SecretItem = memo( }; const handleTagSelect = (tag: WsTag) => { - if (selectedTagsGroupById?.[tag._id]) { - const tagPos = selectedTags.findIndex(({ _id }) => _id === tag._id); + if (selectedTagsGroupById?.[tag.id]) { + const tagPos = selectedTags.findIndex(({ id }) => id === tag.id); if (tagPos !== -1) { remove(tagPos); } @@ -217,9 +217,9 @@ export const SecretItem = memo( )} > onToggleSecretSelect(secret._id)} + onCheckedChange={() => onToggleSecretSelect(secret.id)} className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")} /> Apply tags to this secrets {tags.map((tag) => { - const { _id: tagId, name, tagColor } = tag; + const { id: tagId, name, tagColor } = tag; const isTagSelected = selectedTagsGroupById?.[tagId]; return ( diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts index 6dcf6110a..fc8a745d5 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts @@ -31,7 +31,7 @@ export const formSchema = z.object({ tags: z .object({ - _id: z.string(), + id: z.string(), name: z.string(), slug: z.string(), tagColor: z.string().optional() diff --git a/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx index 8b661415e..2b0110ee7 100644 --- a/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx @@ -151,10 +151,10 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => { Tags {isModified && ( - {preSecret?.tags?.map(({ name, _id: tagId, tagColor }) => ( + {preSecret?.tags?.map(({ name, id: tagId, tagColor }) => (
{ )} - {postSecret?.tags?.map(({ name, _id: tagId, tagColor }) => ( + {postSecret?.tags?.map(({ name, id: tagId, tagColor }) => (
{ const secretGroupById = secrets.reduce>( - (prev, curr) => ({ ...prev, [curr._id]: curr }), + (prev, curr) => ({ ...prev, [curr.id]: curr }), {} ); const diffView: Array> = []; rollingSecrets.forEach((rollSecret) => { - const { _id: id } = rollSecret; + const { id: id } = rollSecret; const doesExist = Boolean(secretGroupById?.[id]); if (doesExist) { diffView.push({ diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index f63350ca7..99c167890 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -74,14 +74,14 @@ export const SecretOverviewPage = () => { const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace(); const { currentOrg } = useOrganization(); - const workspaceId = currentWorkspace?._id as string; + const workspaceId = currentWorkspace?.id as string; const { data: latestFileKey } = useGetUserWsKey(workspaceId); const [searchFilter, setSearchFilter] = useState(""); const secretPath = (router.query?.secretPath as string) || "/"; useEffect(() => { if (!isWorkspaceLoading && !workspaceId && router.isReady) { - router.push(`/org/${currentOrg?._id}/overview`); + router.push(`/org/${currentOrg?.id}/overview`); } }, [isWorkspaceLoading, workspaceId, router.isReady]); diff --git a/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx index af2edb0ce..bdb63e021 100644 --- a/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx +++ b/frontend/src/views/SecretOverviewPage/components/ProjectIndexSecretsSection/ProjectIndexSecretsSection.tsx @@ -19,16 +19,16 @@ type Props = { export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => { const { currentWorkspace } = useWorkspace(); const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus( - currentWorkspace?._id ?? "" + currentWorkspace?.id ?? "" ); const [isIndexing, setIsIndexing] = useToggle(); const nameWorkspaceSecrets = useNameWorkspaceSecrets(); const onEnableBlindIndices = async () => { - if (!currentWorkspace?._id) return; + if (!currentWorkspace?.id) return; setIsIndexing.on(); try { - const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace._id); + const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace.id); const key = decryptAssymmetric({ ciphertext: decryptFileKey.encryptedKey, @@ -47,11 +47,11 @@ export const ProjectIndexSecretsSection = ({ decryptFileKey }: Props) => { return { secretName, - _id: encryptedSecret._id + id: encryptedSecret.id }; }); await nameWorkspaceSecrets.mutateAsync({ - workspaceId: currentWorkspace._id, + workspaceId: currentWorkspace.id, secretsToUpdate }); } catch (err) { diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index 8671d2da9..08774535b 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -149,7 +149,7 @@ export const SecretOverviewTableRow = ({ isVisible={isSecretVisible} secretName={secretKey} defaultValue={secret?.value} - secretId={secret?._id} + secretId={secret?.id} isCreatable={isCreatable} onSecretDelete={onSecretDelete} onSecretCreate={onSecretCreate} diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx index 3a502a35d..cdce4f0b1 100644 --- a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx @@ -70,7 +70,7 @@ export const SecretRotationPage = withProjectPermission( "deleteRotation", "upgradePlan" ] as const); - const workspaceId = currentWorkspace?._id || ""; + const workspaceId = currentWorkspace?.id || ""; const canCreateRotation = permission.can( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation @@ -145,11 +145,11 @@ export const SecretRotationPage = withProjectPermission( const handleUserAcceptBotCondition = async () => { const provider = popUp.activeBot?.data as TSecretRotationProvider; try { - if (bot?._id) { + if (bot?.id) { const botKey = generateBotKey(bot.publicKey, userWsKey!); await updateBotActiveStatus({ isActive: true, - botId: bot._id, + botId: bot.id, workspaceId, botKey }); @@ -232,16 +232,16 @@ export const SecretRotationPage = withProjectPermission( secretPath, outputs, provider, - _id, + id, lastRotatedAt, status, statusMessage }) => { - const isDeleting = deleteSecretRotationVars?.id === _id && isDeletingRotation; + const isDeleting = deleteSecretRotationVars?.id === id && isDeletingRotation; const isRestarting = - restartSecretRotationVar?.id === _id && isRestartingRotation; + restartSecretRotationVar?.id === id && isRestartingRotation; return ( - + {outputs .map(({ key }) => key) @@ -291,7 +291,7 @@ export const SecretRotationPage = withProjectPermission( colorSchema="danger" ariaLabel="delete-rotation" isDisabled={isDeleting || !isAllowed} - onClick={() => handleRestartRotation(_id)} + onClick={() => handleRestartRotation(id)} > {isRestarting ? ( @@ -313,7 +313,7 @@ export const SecretRotationPage = withProjectPermission( colorSchema="danger" ariaLabel="delete-rotation" isDisabled={isDeleting || !isAllowed} - onClick={() => handlePopUpOpen("deleteRotation", { id: _id })} + onClick={() => handlePopUpOpen("deleteRotation", { id: id })} > {isDeleting ? ( diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx index e11038f7c..e41bac258 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx @@ -23,7 +23,7 @@ type Props = { export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Props) => { const { currentWorkspace } = useWorkspace(); const environments = currentWorkspace?.environments || []; - const workspaceId = currentWorkspace?._id || ""; + const workspaceId = currentWorkspace?.id || ""; const { control, handleSubmit, @@ -117,11 +117,11 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr {!isSecretsLoading && secrets ?.filter( - ({ _id }) => - value === _id || !Object.values(selectedSecrets || {}).includes(_id) + ({ id }) => + value === id || !Object.values(selectedSecrets || {}).includes(id) ) - ?.map(({ key, _id }) => ( - + ?.map(({ key, id }) => ( + {key} ))} diff --git a/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx b/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx index 4add11835..ad60f9a7f 100644 --- a/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx +++ b/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx @@ -83,7 +83,7 @@ export const SecretScanningLogsTable = () => { {risk.isResolved ? "Resolved" : "Needs Attention"} - + ); diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx index 75fdf2f99..a3135d85f 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx @@ -17,7 +17,7 @@ import { useGetOrgPlanTable } from "@app/hooks/api"; export const CurrentPlanSection = () => { const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? ""); + const { data, isLoading } = useGetOrgPlanTable(currentOrg?.id ?? ""); const displayCell = (value: null | number | string | boolean) => { if (value === null) return "-"; diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx index f057f7ff0..2aa552f52 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx @@ -24,7 +24,7 @@ export const ManagePlansTable = ({ billingCycle }: Props) => { const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ - organizationId: currentOrg?._id ?? "", + organizationId: currentOrg?.id ?? "", billingCycle }); const createCustomerPortalSession = useCreateCustomerPortalSession(); @@ -110,11 +110,11 @@ export const ManagePlansTable = ({ billingCycle }: Props) => {