From 142fcf0a01aa9333abc5bdc0e3147c819f860e2e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 5 Aug 2023 16:55:06 +0700 Subject: [PATCH 01/64] Finish preliminary v2 audit logs --- backend/src/controllers/v1/authController.ts | 10 +- .../controllers/v1/membershipOrgController.ts | 4 +- .../src/controllers/v1/passwordController.ts | 6 +- .../src/controllers/v1/workspaceController.ts | 7 +- backend/src/controllers/v2/authController.ts | 6 +- .../controllers/v2/environmentController.ts | 7 +- .../src/controllers/v2/secretsController.ts | 10 +- .../v2/serviceTokenDataController.ts | 18 +- backend/src/controllers/v3/authController.ts | 4 +- .../controllers/v1/organizationsController.ts | 5 +- .../src/ee/controllers/v1/ssoController.ts | 4 +- .../src/ee/controllers/v1/usersController.ts | 2 +- .../ee/controllers/v1/workspaceController.ts | 92 +++++- backend/src/ee/models/auditLog/auditLog.ts | 76 +++++ backend/src/ee/models/auditLog/enums.ts | 20 ++ backend/src/ee/models/auditLog/index.ts | 3 + backend/src/ee/models/auditLog/types.ts | 88 ++++++ backend/src/ee/models/index.ts | 3 +- backend/src/ee/routes/v1/cloudProducts.ts | 3 +- backend/src/ee/routes/v1/organizations.ts | 32 +- backend/src/ee/routes/v1/secret.ts | 5 +- backend/src/ee/routes/v1/secretSnapshot.ts | 4 +- backend/src/ee/routes/v1/sso.ts | 9 +- backend/src/ee/routes/v1/users.ts | 4 +- backend/src/ee/routes/v1/workspace.ts | 53 +++- backend/src/ee/services/EEAuditLogService.ts | 46 +++ backend/src/ee/services/EELicenseService.ts | 17 +- backend/src/ee/services/EESecretService.ts | 4 +- backend/src/ee/services/index.ts | 2 + backend/src/helpers/auth.ts | 117 ++++--- backend/src/helpers/organization.ts | 2 +- backend/src/helpers/secrets.ts | 129 ++++++-- backend/src/helpers/workspace.ts | 3 +- backend/src/interfaces/middleware/index.ts | 32 +- backend/src/middleware/requireAuth.ts | 81 ++--- .../src/middleware/requireWorkspaceAuth.ts | 2 + backend/src/routes/v1/auth.ts | 10 +- backend/src/routes/v1/bot.ts | 6 +- backend/src/routes/v1/integration.ts | 9 +- backend/src/routes/v1/integrationAuth.ts | 27 +- backend/src/routes/v1/inviteOrg.ts | 4 +- backend/src/routes/v1/key.ts | 6 +- backend/src/routes/v1/membership.ts | 10 +- backend/src/routes/v1/membershipOrg.ts | 6 +- backend/src/routes/v1/organization.ts | 26 +- backend/src/routes/v1/password.ts | 10 +- backend/src/routes/v1/secret.ts | 6 +- backend/src/routes/v1/secretImport.ts | 12 +- backend/src/routes/v1/secretScanning.ts | 12 +- backend/src/routes/v1/secretsFolder.ts | 10 +- backend/src/routes/v1/serviceToken.ts | 4 +- backend/src/routes/v1/user.ts | 6 +- backend/src/routes/v1/userAction.ts | 6 +- backend/src/routes/v1/webhook.ts | 12 +- backend/src/routes/v1/workspace.ts | 25 +- backend/src/routes/v2/environment.ts | 10 +- backend/src/routes/v2/organizations.ts | 13 +- backend/src/routes/v2/secret.ts | 19 +- backend/src/routes/v2/secrets.ts | 20 +- backend/src/routes/v2/serviceAccounts.ts | 291 +++++++++--------- backend/src/routes/v2/serviceTokenData.ts | 10 +- backend/src/routes/v2/tags.ts | 8 +- backend/src/routes/v2/users.ts | 25 +- backend/src/routes/v2/workspace.ts | 20 +- backend/src/routes/v3/secrets.ts | 75 ++--- backend/src/routes/v3/workspaces.ts | 8 +- backend/src/types/express/index.d.ts | 4 +- backend/src/utils/posthog.ts | 20 +- backend/src/validation/bot.ts | 83 +---- backend/src/validation/integration.ts | 67 +--- backend/src/validation/integrationAuth.ts | 67 +--- backend/src/validation/membership.ts | 70 ++--- backend/src/validation/membershipOrg.ts | 71 +---- backend/src/validation/organization.ts | 84 +---- backend/src/validation/secrets.ts | 136 +++----- backend/src/validation/serviceAccount.ts | 65 +--- backend/src/validation/serviceTokenData.ts | 82 ++--- backend/src/validation/workspace.ts | 141 ++++----- backend/src/variables/authentication.ts | 9 +- .../src/ee/components/ActivitySideBar.tsx | 1 + frontend/src/ee/components/ActivityTable.tsx | 2 + .../src/hooks/api/auditLogs/constants.tsx | 16 + frontend/src/hooks/api/auditLogs/enums.tsx | 19 ++ frontend/src/hooks/api/auditLogs/index.tsx | 1 + frontend/src/hooks/api/auditLogs/queries.tsx | 53 ++++ frontend/src/hooks/api/auditLogs/types.tsx | 103 +++++++ frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/subscriptions/types.ts | 1 + frontend/src/hooks/api/workspace/queries.tsx | 4 +- frontend/src/hooks/api/workspace/types.ts | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 12 + .../src/pages/integrations/checkly/create.tsx | 2 +- .../src/pages/project/[id]/logs/index.tsx | 23 ++ .../src/views/Project/LogsPage/LogsPage.tsx | 17 + .../LogsPage/components/LogsFilter.tsx | 147 +++++++++ .../LogsPage/components/LogsSection.tsx | 49 +++ .../Project/LogsPage/components/LogsTable.tsx | 70 +++++ .../LogsPage/components/LogsTableRow.tsx | 114 +++++++ .../Project/LogsPage/components/index.tsx | 1 + frontend/src/views/Project/LogsPage/index.tsx | 1 + 100 files changed, 1885 insertions(+), 1269 deletions(-) create mode 100644 backend/src/ee/models/auditLog/auditLog.ts create mode 100644 backend/src/ee/models/auditLog/enums.ts create mode 100644 backend/src/ee/models/auditLog/index.ts create mode 100644 backend/src/ee/models/auditLog/types.ts create mode 100644 backend/src/ee/services/EEAuditLogService.ts create mode 100644 frontend/src/hooks/api/auditLogs/constants.tsx create mode 100644 frontend/src/hooks/api/auditLogs/enums.tsx create mode 100644 frontend/src/hooks/api/auditLogs/index.tsx create mode 100644 frontend/src/hooks/api/auditLogs/queries.tsx create mode 100644 frontend/src/hooks/api/auditLogs/types.tsx create mode 100644 frontend/src/pages/project/[id]/logs/index.tsx create mode 100644 frontend/src/views/Project/LogsPage/LogsPage.tsx create mode 100644 frontend/src/views/Project/LogsPage/components/LogsFilter.tsx create mode 100644 frontend/src/views/Project/LogsPage/components/LogsSection.tsx create mode 100644 frontend/src/views/Project/LogsPage/components/LogsTable.tsx create mode 100644 frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx create mode 100644 frontend/src/views/Project/LogsPage/components/index.tsx create mode 100644 frontend/src/views/Project/LogsPage/index.tsx diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index c3ab670b6..03a9a7717 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -15,20 +15,20 @@ import { checkUserDevice } from "../../helpers/user"; import { ACTION_LOGIN, ACTION_LOGOUT, - AUTH_MODE_JWT, } from "../../variables"; import { BadRequestError, UnauthorizedRequestError, } from "../../utils/errors"; import { EELogService } from "../../ee/services"; -import { getChannelFromUserAgent } from "../../utils/posthog"; +import { getUserAgentType } from "../../utils/posthog"; import { getHttpsEnabled, getJwtAuthLifetime, getJwtAuthSecret, getJwtRefreshSecret, } from "../../config"; +import { ActorType } from "../../ee/models"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -142,7 +142,7 @@ export const login2 = async (req: Request, res: Response) => { loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers["user-agent"]), + channel: getUserAgentType(req.headers["user-agent"]), ipAddress: req.realIP, }); @@ -170,7 +170,7 @@ export const login2 = async (req: Request, res: Response) => { * @returns */ export const logout = async (req: Request, res: Response) => { - if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) { + if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { await clearTokens(req.authData.tokenVersionId) } @@ -190,7 +190,7 @@ export const logout = async (req: Request, res: Response) => { logoutAction && await EELogService.createLog({ userId: req.user._id, actions: [logoutAction], - channel: getChannelFromUserAgent(req.headers["user-agent"]), + channel: getUserAgentType(req.headers["user-agent"]), ipAddress: req.realIP, }); diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 02b99537f..0c319fe62 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -103,14 +103,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { // validate membership const membershipOrg = await MembershipOrg.findOne({ user: req.user._id, - organization: organizationId + organization: new Types.ObjectId(organizationId) }); if (!membershipOrg) { throw new Error("Failed to validate organization membership"); } - const plan = await EELicenseService.getPlan(organizationId); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const ssoConfig = await SSOConfig.findOne({ organization: new Types.ObjectId(organizationId) diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 5a2699225..22b90a61a 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -5,7 +5,7 @@ import * as bigintConversion from "bigint-conversion"; import { BackupPrivateKey, LoginSRPDetail, User } from "../../models"; import { clearTokens, createToken, sendMail } from "../../helpers"; import { TokenService } from "../../services"; -import { AUTH_MODE_JWT, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; +import { TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; import { BadRequestError } from "../../utils/errors"; import { getHttpsEnabled, @@ -13,6 +13,7 @@ import { getJwtSignupSecret, getSiteURL } from "../../config"; +import { ActorType } from "../../ee/models"; /** * Password reset step 1: Send email verification link to email [email] @@ -208,8 +209,7 @@ export const changePassword = async (req: Request, res: Response) => { ); if ( - req.authData.authMode === AUTH_MODE_JWT && - req.authData.authPayload instanceof User && + req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId ) { await clearTokens(req.authData.tokenVersionId); diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index f26078ace..1425c142c 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -1,3 +1,4 @@ +import { Types } from "mongoose"; import { Request, Response } from "express"; import { IUser, @@ -108,14 +109,14 @@ export const createWorkspace = async (req: Request, res: Response) => { // validate organization membership const membershipOrg = await MembershipOrg.findOne({ user: req.user._id, - organization: organizationId, + organization: new Types.ObjectId(organizationId), }); if (!membershipOrg) { throw new Error("Failed to validate organization membership"); } - const plan = await EELicenseService.getPlan(organizationId); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); if (plan.workspaceLimit !== null) { // case: limit imposed on number of workspaces allowed @@ -134,7 +135,7 @@ export const createWorkspace = async (req: Request, res: Response) => { // create workspace and add user as member const workspace = await create({ name: workspaceName, - organizationId, + organizationId: new Types.ObjectId(organizationId), }); await addMemberships({ diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 282c15288..8f2f4cea0 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -14,7 +14,7 @@ import { ACTION_LOGIN, TOKEN_EMAIL_MFA, } from "../../variables"; -import { getChannelFromUserAgent } from "../../utils/posthog"; // TODO: move this +import { getUserAgentType } from "../../utils/posthog"; // TODO: move this import { getHttpsEnabled, getJwtMfaLifetime, @@ -203,7 +203,7 @@ export const login2 = async (req: Request, res: Response) => { loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers["user-agent"]), + channel: getUserAgentType(req.headers["user-agent"]), ipAddress: req.ip, }); @@ -336,7 +336,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => { loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers["user-agent"]), + channel: getUserAgentType(req.headers["user-agent"]), ipAddress: req.realIP, }); diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 58f1e10f8..e7de3a8cf 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -1,4 +1,5 @@ import { Request, Response } from "express"; +import { Types } from "mongoose"; import { Integration, Membership, @@ -30,7 +31,7 @@ export const createWorkspaceEnvironment = async ( if (!workspace) throw WorkspaceNotFoundError(); - const plan = await EELicenseService.getPlan(workspace.organization.toString()); + const plan = await EELicenseService.getPlan(workspace.organization); if (plan.environmentLimit !== null) { // case: limit imposed on number of environments allowed @@ -58,7 +59,7 @@ export const createWorkspaceEnvironment = async ( }); await workspace.save(); - await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId); + await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId)); return res.status(200).send({ message: "Successfully created new environment", @@ -215,7 +216,7 @@ export const deleteWorkspaceEnvironment = async ( { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } ); - await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId); + await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId)); return res.status(200).send({ message: "Successfully deleted environment", diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 284c69c09..14efd452a 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -16,7 +16,7 @@ import { EventService } from "../../services"; import { eventPushSecrets } from "../../events"; import { EELogService, EESecretService } from "../../ee/services"; import { SecretService, TelemetryService } from "../../services"; -import { getChannelFromUserAgent } from "../../utils/posthog"; +import { getUserAgentType } from "../../utils/posthog"; import { PERMISSION_WRITE_SECRETS } from "../../variables"; import { userHasNoAbility, @@ -44,7 +44,7 @@ import { getAllImportedSecrets } from "../../services/SecretImportService"; * @param res */ export const batchSecrets = async (req: Request, res: Response) => { - const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const channel = getUserAgentType(req.headers["user-agent"]); const postHogClient = await TelemetryService.getPostHogClient(); const { @@ -416,7 +416,7 @@ export const createSecrets = async (req: Request, res: Response) => { } */ - const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const channel = getUserAgentType(req.headers["user-agent"]); const { workspaceId, environment, @@ -834,7 +834,7 @@ export const getSecrets = async (req: Request, res: Response) => { importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string); } - const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const channel = getUserAgentType(req.headers["user-agent"]); const readAction = await EELogService.createAction({ name: ACTION_READ_SECRETS, @@ -1170,7 +1170,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { } */ - const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const channel = getUserAgentType(req.headers["user-agent"]); const toDelete = req.secrets.map((s: any) => s._id); await Secret.deleteMany({ diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 25176a5ff..10fb50ef1 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -1,10 +1,10 @@ import { Request, Response } from "express"; import crypto from "crypto"; import bcrypt from "bcrypt"; -import { ServiceAccount, ServiceTokenData, User } from "../../models"; -import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables"; +import { ServiceTokenData } from "../../models"; import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; +import { ActorType } from "../../ee/models"; /** * Return service token data associated with service token on request @@ -73,24 +73,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => { expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); } - let user, serviceAccount; - - if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { + let user; + + if (req.authData.actor.type === ActorType.USER) { user = req.authData.authPayload._id; } - if ( - req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && - req.authData.authPayload instanceof ServiceAccount - ) { - serviceAccount = req.authData.authPayload._id; - } - serviceTokenData = await new ServiceTokenData({ name, workspace: workspaceId, user, - serviceAccount, scopes, lastUsed: new Date(), expiresAt, diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 08ef7fa8d..1a91573e4 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -15,7 +15,7 @@ import { ACTION_LOGIN, TOKEN_EMAIL_MFA, } from "../../variables"; -import { getChannelFromUserAgent } from "../../utils/posthog"; // TODO: move this +import { getUserAgentType } from "../../utils/posthog"; // TODO: move this import { getHttpsEnabled, getJwtMfaLifetime, @@ -241,7 +241,7 @@ export const login2 = async (req: Request, res: Response) => { loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers["user-agent"]), + channel: getUserAgentType(req.headers["user-agent"]), ipAddress: req.realIP, }); diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index b473bcbd2..b641de985 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -1,3 +1,4 @@ +import { Types } from "mongoose"; import { Request, Response } from "express"; import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; @@ -20,7 +21,7 @@ export const getOrganizationPlan = async (req: Request, res: Response) => { const { organizationId } = req.params; const workspaceId = req.query.workspaceId as string; - const plan = await EELicenseService.getPlan(organizationId, workspaceId); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId), new Types.ObjectId(workspaceId)); return res.status(200).send({ plan, @@ -44,7 +45,7 @@ export const startOrganizationTrial = async (req: Request, res: Response) => { } ); - EELicenseService.delPlan(organizationId); + EELicenseService.delPlan(new Types.ObjectId(organizationId)); return res.status(200).send({ url diff --git a/backend/src/ee/controllers/v1/ssoController.ts b/backend/src/ee/controllers/v1/ssoController.ts index 42b4d12be..6f14314b1 100644 --- a/backend/src/ee/controllers/v1/ssoController.ts +++ b/backend/src/ee/controllers/v1/ssoController.ts @@ -59,7 +59,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => { cert, } = req.body; - const plan = await EELicenseService.getPlan(organizationId); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); if (!plan.samlSSO) return res.status(400).send({ message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." @@ -194,7 +194,7 @@ export const createSSOConfig = async (req: Request, res: Response) => { cert } = req.body; - const plan = await EELicenseService.getPlan(organizationId); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); if (!plan.samlSSO) return res.status(400).send({ message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." diff --git a/backend/src/ee/controllers/v1/usersController.ts b/backend/src/ee/controllers/v1/usersController.ts index 13e36a883..a492404f5 100644 --- a/backend/src/ee/controllers/v1/usersController.ts +++ b/backend/src/ee/controllers/v1/usersController.ts @@ -8,6 +8,6 @@ import { Request, Response } from "express"; */ export const getMyIp = (req: Request, res: Response) => { return res.status(200).send({ - ip: req.authData.authIP + ip: req.authData.ipAddress }); } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index d4f097ea2..c865dc8eb 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { PipelineStage, Types } from "mongoose"; -import { Secret } from "../../../models"; +import { Secret, Membership, User, ServiceTokenData } from "../../../models"; import { FolderVersion, IPType, @@ -9,7 +9,12 @@ import { SecretSnapshot, SecretVersion, TFolderRootVersionSchema, - TrustedIP + TrustedIP, + AuditLog, + Actor, + ActorType, + UserActor, + ServiceActor } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; @@ -593,6 +598,83 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { }); }; +/** + * Return trusted ips for workspace with id [workspaceId] + * @param req + * @param res + */ +export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { + const { workspaceId } = req.params; + const eventType = req.query.eventType; + const userAgentType = req.query.userAgentType; + const actor = req.query.actor as string | undefined; + + const auditLogs = await AuditLog.find({ + workspace: new Types.ObjectId(workspaceId), + ...(eventType ? { + "event.type": eventType + } : {}), + ...(userAgentType ? { + userAgentType + } : {}), + ...(actor ? { + "actor.type": actor.split("-", 2)[0], + ...(actor.split("-", 2)[0] === ActorType.USER ? { + "actor.metadata.userId": actor.split("-", 2)[1] + } : { + "actor.metadata.serviceId": actor.split("-", 2)[1] + }) + } : {}) + }) + .sort({ createdAt: -1 }); + + return res.status(200).send({ + auditLogs + }); +} + +/** + * Return trusted ips for workspace with id [workspaceId] + * @param req + * @param res + */ +export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Response) => { + const { workspaceId } = req.params; + + const userIds = await Membership.distinct("user", { + workspace: new Types.ObjectId(workspaceId) + }); + const userActors: UserActor[] = (await User.find({ + _id: { + $in: userIds + } + }) + .select("email")) + .map((user) => ({ + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + })); + + const serviceActors: ServiceActor[] = (await ServiceTokenData.find({ + workspace: new Types.ObjectId(workspaceId) + }) + .select("name")) + .map((serviceTokenData) => ({ + type: ActorType.SERVICE, + metadata: { + serviceId: serviceTokenData._id.toString(), + name: serviceTokenData.name + } + })); + + return res.status(200).send({ + actors: [...userActors, ...serviceActors] + }); +} + /** * Return trusted ips for workspace with id [workspaceId] * @param req @@ -623,7 +705,7 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { isActive } = req.body; - const plan = await EELicenseService.getPlan(req.workspace.organization.toString()); + const plan = await EELicenseService.getPlan(req.workspace.organization); if (!plan.ipAllowlisting) return res.status(400).send({ message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range." @@ -663,7 +745,7 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { comment } = req.body; - const plan = await EELicenseService.getPlan(req.workspace.organization.toString()); + const plan = await EELicenseService.getPlan(req.workspace.organization); if (!plan.ipAllowlisting) return res.status(400).send({ message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range." @@ -721,7 +803,7 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { const { workspaceId, trustedIpId } = req.params; - const plan = await EELicenseService.getPlan(req.workspace.organization.toString()); + const plan = await EELicenseService.getPlan(req.workspace.organization); if (!plan.ipAllowlisting) return res.status(400).send({ message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range." diff --git a/backend/src/ee/models/auditLog/auditLog.ts b/backend/src/ee/models/auditLog/auditLog.ts new file mode 100644 index 000000000..a38527e85 --- /dev/null +++ b/backend/src/ee/models/auditLog/auditLog.ts @@ -0,0 +1,76 @@ +import { Schema, Types, model } from "mongoose"; +import { + ActorType, + EventType, + UserAgentType +} from "./enums"; +import { + Actor, + Event +} from "./types"; + +export interface IAuditLog { + actor: Actor; + organization: Types.ObjectId; + workspace: Types.ObjectId; + ipAddress: string; + event: Event; + userAgent: string; + userAgentType: UserAgentType; + expiresAt: Date; +} + +const auditLogSchema = new Schema( + { + actor: { + type: { + type: String, + enum: ActorType, + required: true + }, + metadata: { + type: Schema.Types.Mixed + } + }, + organization: { + type: Schema.Types.ObjectId, + required: false + }, + workspace: { + type: Schema.Types.ObjectId, + required: false + }, + ipAddress: { + type: String, + required: true + }, + event: { + type: { + type: String, + enum: EventType, + required: true + }, + metadata: { + type: Schema.Types.Mixed + } + }, + userAgent: { + type: String, + required: true + }, + userAgentType: { + type: String, + enum: UserAgentType, + required: true + }, + expiresAt: { + type: Date, + expires: 0 + } + }, + { + timestamps: true + } +); + +export const AuditLog = model("AuditLog", auditLogSchema); diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts new file mode 100644 index 000000000..ce415e8c9 --- /dev/null +++ b/backend/src/ee/models/auditLog/enums.ts @@ -0,0 +1,20 @@ +export enum ActorType { + USER = "user", + SERVICE = "service" +} + +export enum UserAgentType { + WEB = "web", + CLI = "cli", + K8_OPERATOR = "k8-operator", + OTHER = "other" +} + +export enum EventType { + GET_SECRETS = "get-secrets", + GET_SECRET = "get-secret", + REVEAL_SECRET = "reveal-secret", + CREATE_SECRET = "create-secret", + UPDATE_SECRET = "update-secret", + DELETE_SECRET = "delete-secret" +} \ No newline at end of file diff --git a/backend/src/ee/models/auditLog/index.ts b/backend/src/ee/models/auditLog/index.ts new file mode 100644 index 000000000..37b86b5d1 --- /dev/null +++ b/backend/src/ee/models/auditLog/index.ts @@ -0,0 +1,3 @@ +export * from "./auditLog"; +export * from "./enums"; +export * from "./types"; \ No newline at end of file diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts new file mode 100644 index 000000000..848fb88db --- /dev/null +++ b/backend/src/ee/models/auditLog/types.ts @@ -0,0 +1,88 @@ +import { + ActorType, + EventType +} from "./enums"; + +interface UserActorMetadata { + userId: string; + email: string; +} + +interface ServiceActorMetadata { + serviceId: string; + name: string; +} + +export interface UserActor { + type: ActorType.USER; + metadata: UserActorMetadata; +} + +export interface ServiceActor { + type: ActorType.SERVICE; + metadata: ServiceActorMetadata; +} + +export type Actor = + | UserActor + | ServiceActor; + +interface GetSecretsEvent { + type: EventType.GET_SECRETS; + metadata: { + environment: string; + secretPath: string; + numberOfSecrets: number; + }; +} + +interface GetSecretEvent { + type: EventType.GET_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + }; +} + +interface CreateSecretEvent { + type: EventType.CREATE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + } +} + +interface UpdateSecretEvent { + type: EventType.UPDATE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + } +} + +interface DeleteSecretEvent { + type: EventType.DELETE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + } +} + +export type Event = + | GetSecretsEvent + | GetSecretEvent + | CreateSecretEvent + | UpdateSecretEvent + | DeleteSecretEvent; \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts index 1def9073d..420e51d3a 100644 --- a/backend/src/ee/models/index.ts +++ b/backend/src/ee/models/index.ts @@ -4,4 +4,5 @@ export * from "./folderVersion"; export * from "./log"; export * from "./action"; export * from "./ssoConfig"; -export * from "./trustedIp"; \ No newline at end of file +export * from "./trustedIp"; +export * from "./auditLog"; diff --git a/backend/src/ee/routes/v1/cloudProducts.ts b/backend/src/ee/routes/v1/cloudProducts.ts index a9be34747..81256f378 100644 --- a/backend/src/ee/routes/v1/cloudProducts.ts +++ b/backend/src/ee/routes/v1/cloudProducts.ts @@ -6,11 +6,12 @@ import { } from "../../../middleware"; import { query } from "express-validator"; import { cloudProductsController } from "../../controllers/v1"; +import { AuthMode } from "../../../variables"; router.get( "/", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), query("billing-cycle").exists().isIn(["monthly", "yearly"]), validateRequest, diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 5c308466d..d41d232da 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -8,13 +8,13 @@ import { import { body, param, query } from "express-validator"; import { organizationsController } from "../../controllers/v1"; import { - ACCEPTED, ADMIN, MEMBER, OWNER, + ACCEPTED, ADMIN, MEMBER, OWNER, AuthMode } from "../../../variables"; router.get( "/:organizationId/plans/table", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -29,7 +29,7 @@ router.get( router.get( "/:organizationId/plan", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -44,7 +44,7 @@ router.get( router.post( "/:organizationId/session/trial", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -59,7 +59,7 @@ router.post( router.get( "/:organizationId/plan/billing", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -74,7 +74,7 @@ router.get( router.get( "/:organizationId/plan/table", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -89,7 +89,7 @@ router.get( router.get( "/:organizationId/billing-details", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -103,7 +103,7 @@ router.get( router.patch( "/:organizationId/billing-details", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -119,7 +119,7 @@ router.patch( router.get( "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -133,7 +133,7 @@ router.get( router.post( "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -149,7 +149,7 @@ router.post( router.delete( "/:organizationId/billing-details/payment-methods/:pmtMethodId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -164,7 +164,7 @@ router.delete( router.get( "/:organizationId/billing-details/tax-ids", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -178,7 +178,7 @@ router.get( router.post( "/:organizationId/billing-details/tax-ids", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -194,7 +194,7 @@ router.post( router.delete( "/:organizationId/billing-details/tax-ids/:taxId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -209,7 +209,7 @@ router.delete( router.get( "/:organizationId/invoices", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -223,7 +223,7 @@ router.get( router.get( "/:organizationId/licenses", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 7be6f311a..376922e74 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -12,12 +12,13 @@ import { MEMBER, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, + AuthMode } from "../../../variables"; router.get( "/:secretId/secret-versions", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -33,7 +34,7 @@ router.get( router.post( "/:secretId/secret-versions/rollback", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index fe0c2690c..c6201cc12 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -8,13 +8,13 @@ import { validateRequest, } from "../../../middleware"; import { param } from "express-validator"; -import { ADMIN, MEMBER } from "../../../variables"; +import { ADMIN, MEMBER, AuthMode } from "../../../variables"; import { secretSnapshotController } from "../../controllers/v1"; router.get( "/:secretSnapshotId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireSecretSnapshotAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index f76f19e7d..22ecee9f0 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -15,7 +15,8 @@ import { authLimiter } from "../../../helpers/rateLimiter"; import { ACCEPTED, ADMIN, - OWNER + OWNER, + AuthMode } from "../../../variables"; router.get( @@ -90,7 +91,7 @@ router.post("/saml2/:ssoIdentifier", router.get( "/config", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], @@ -105,7 +106,7 @@ router.get( router.post( "/config", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], @@ -125,7 +126,7 @@ router.post( router.patch( "/config", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], diff --git a/backend/src/ee/routes/v1/users.ts b/backend/src/ee/routes/v1/users.ts index 14dcaa49c..d5015401e 100644 --- a/backend/src/ee/routes/v1/users.ts +++ b/backend/src/ee/routes/v1/users.ts @@ -3,13 +3,13 @@ const router = express.Router(); import { requireAuth } from "../../../middleware"; -import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables"; +import { AuthMode } from "../../../variables"; import { usersController } from "../../controllers/v1"; router.get( "/me/ip", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), usersController.getMyIp ); diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 49b13e6a5..407f5d3f8 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -8,16 +8,16 @@ import { import { body, param, query } from "express-validator"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - MEMBER + MEMBER, + AuthMode } from "../../../variables"; import { workspaceController } from "../../controllers/v1"; +import { EventType, UserAgentType } from "../../models"; router.get( "/:workspaceId/secret-snapshots", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -35,7 +35,7 @@ router.get( router.get( "/:workspaceId/secret-snapshots/count", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -51,7 +51,7 @@ router.get( router.post( "/:workspaceId/secret-snapshots/rollback", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -68,7 +68,7 @@ router.post( router.get( "/:workspaceId/logs", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -84,11 +84,42 @@ router.get( workspaceController.getWorkspaceLogs ); +router.get( + "/:workspaceId/audit-logs", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params", + }), + param("workspaceId").exists().trim(), + query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), + query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), + query("actor").isString().optional({ nullable: true }), + validateRequest, + workspaceController.getWorkspaceAuditLogs +); + +router.get( + "/:workspaceId/audit-logs/filters/actors", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params", + }), + param("workspaceId").exists().trim(), + validateRequest, + workspaceController.getWorkspaceAuditLogActorFilterOpts +); + router.get( "/:workspaceId/trusted-ips", param("workspaceId").exists().isString().trim(), requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -105,7 +136,7 @@ router.post( body("isActive").exists().isBoolean(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -122,7 +153,7 @@ router.patch( body("comment").default("").isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -137,7 +168,7 @@ router.delete( param("trustedIpId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], diff --git a/backend/src/ee/services/EEAuditLogService.ts b/backend/src/ee/services/EEAuditLogService.ts new file mode 100644 index 000000000..02ea99225 --- /dev/null +++ b/backend/src/ee/services/EEAuditLogService.ts @@ -0,0 +1,46 @@ +import { Types } from "mongoose"; +import { AuditLog, Event } from "../models"; +import { AuthData } from "../../interfaces/middleware"; +import EELicenseService from "./EELicenseService"; +import { Workspace } from "../../models"; +import { OrganizationNotFoundError } from "../../utils/errors"; + +interface EventScope { + workspaceId?: Types.ObjectId; + organizationId?: Types.ObjectId; +} + +type ValidEventScope = + | Required> + | Required> + | Required + +export default class EEAuditLogService { + static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope) { + + const MS_IN_DAY = 24 * 60 * 60 * 1000; + + const organizationId = ("organizationId" in eventScope) + ? eventScope.organizationId + : (await Workspace.findById(eventScope.workspaceId).select("organization").lean())?.organization; + + if (!organizationId) throw OrganizationNotFoundError({ + message: "createAuditLog: Failed to create audit log due to missing organizationId" + }); + + const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY; + + const auditLog = await new AuditLog({ + actor: authData.actor, + organization: organizationId, + workspace: ("workspaceId" in eventScope) ? eventScope.workspaceId : undefined, + ipAddress: authData.ipAddress, + event, + userAgent: authData.userAgent, + userAgentType: authData.userAgentType, + expiresAt: new Date(Date.now() + ttl) + }).save(); + + return auditLog; + } +} \ No newline at end of file diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 12e3e496a..de8e4369d 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -1,3 +1,4 @@ +import { Types } from "mongoose"; import * as Sentry from "@sentry/node"; import NodeCache from "node-cache"; import { @@ -31,6 +32,7 @@ interface FeatureSet { customRateLimits: boolean; customAlerts: boolean; auditLogs: boolean; + auditLogsRetentionDays: number; samlSSO: boolean; status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; trial_end: number | null; @@ -66,6 +68,7 @@ class EELicenseService { customRateLimits: true, customAlerts: true, auditLogs: false, + auditLogsRetentionDays: 0, samlSSO: false, status: null, trial_end: null, @@ -81,10 +84,10 @@ class EELicenseService { }); } - public async getPlan(organizationId: string, workspaceId?: string): Promise { + public async getPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId): Promise { try { if (this.instanceType === "cloud") { - const cachedPlan = this.localFeatureSet.get(`${organizationId}-${workspaceId ?? ""}`); + const cachedPlan = this.localFeatureSet.get(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`); if (cachedPlan) { return cachedPlan; } @@ -101,7 +104,7 @@ class EELicenseService { const { data: { currentPlan } } = await licenseServerKeyRequest.get(url); // cache fetched plan for organization - this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ""}`, currentPlan); + this.localFeatureSet.set(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`, currentPlan); return currentPlan; } @@ -112,16 +115,16 @@ class EELicenseService { return this.globalFeatureSet; } - public async refreshPlan(organizationId: string, workspaceId?: string) { + public async refreshPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId) { if (this.instanceType === "cloud") { - this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ""}`); + this.localFeatureSet.del(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`); await this.getPlan(organizationId, workspaceId); } } - public async delPlan(organizationId: string) { + public async delPlan(organizationId: Types.ObjectId) { if (this.instanceType === "cloud") { - this.localFeatureSet.del(`${organizationId}-`); + this.localFeatureSet.del(`${organizationId.toString()}-`); } } diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts index 5a1c4d3cb..1e065b6cd 100644 --- a/backend/src/ee/services/EESecretService.ts +++ b/backend/src/ee/services/EESecretService.ts @@ -10,7 +10,7 @@ import EELicenseService from "./EELicenseService"; /** * Class to handle Enterprise Edition secret actions */ -class EESecretService { +export default class EESecretService { /** * Save a secret snapshot that is a copy of the current state of secrets in workspace with id * [workspaceId] under a new snapshot with incremented version under the @@ -71,5 +71,3 @@ class EESecretService { }); } } - -export default EESecretService; diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts index afc3fb80e..ba25df7fa 100644 --- a/backend/src/ee/services/index.ts +++ b/backend/src/ee/services/index.ts @@ -1,9 +1,11 @@ import EELicenseService from "./EELicenseService"; import EESecretService from "./EESecretService"; import EELogService from "./EELogService"; +import EEAuditLogService from "./EEAuditLogService"; export { EELicenseService, EESecretService, EELogService, + EEAuditLogService } \ No newline at end of file diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index eb6eeb4d2..50b68e62c 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,3 +1,4 @@ +import { Request } from "express"; import { Types } from "mongoose"; import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; @@ -5,7 +6,6 @@ import { APIKeyData, ITokenVersion, IUser, - ServiceAccount, ServiceTokenData, TokenVersion, User, @@ -14,7 +14,6 @@ import { APIKeyDataNotFoundError, AccountNotFoundError, BadRequestError, - ServiceAccountNotFoundError, ServiceTokenDataNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; @@ -26,11 +25,15 @@ import { getJwtRefreshSecret, } from "../config"; import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, + AuthMode } from "../variables"; +import { + UserAuthData, + ServiceTokenAuthData +} from "../interfaces/middleware"; + +import { ActorType } from "../ee/models"; +import { getUserAgentType } from "../utils/posthog"; /** * @@ -42,7 +45,7 @@ export const validateAuthMode = ({ acceptedAuthModes, }: { headers: { [key: string]: string | string[] | undefined }, - acceptedAuthModes: string[] + acceptedAuthModes: AuthMode[] }) => { const apiKey = headers["x-api-key"]; const authHeader = headers["authorization"]; @@ -55,7 +58,7 @@ export const validateAuthMode = ({ if (typeof apiKey === "string") { // case: treat request authentication type as via X-API-KEY (i.e. API Key) - authMode = AUTH_MODE_API_KEY; + authMode = AuthMode.API_KEY; authTokenValue = apiKey; } @@ -71,13 +74,10 @@ export const validateAuthMode = ({ switch (tokenValue.split(".", 1)[0]) { case "st": - authMode = AUTH_MODE_SERVICE_TOKEN; - break; - case "sa": - authMode = AUTH_MODE_SERVICE_ACCOUNT; + authMode = AuthMode.SERVICE_TOKEN; break; default: - authMode = AUTH_MODE_JWT; + authMode = AuthMode.JWT; } authTokenValue = tokenValue; @@ -100,10 +100,12 @@ export const validateAuthMode = ({ * @returns {User} user - user corresponding to JWT token */ export const getAuthUserPayload = async ({ + req, authTokenValue, }: { + req: Request, authTokenValue: string; -}) => { +}): Promise => { const decodedToken = ( jwt.verify(authTokenValue, await getJwtAuthSecret()) ); @@ -130,11 +132,25 @@ export const getAuthUserPayload = async ({ if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({ message: "Failed to validate access token", }); - - return ({ - user, - tokenVersionId: tokenVersion._id, - }); + + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress: req.realIP, + userAgent: req.headers["user-agent"] ?? "", + userAgentType: getUserAgentType(req.headers["user-agent"]) + } + + // return ({ + // user, + // tokenVersionId: tokenVersion._id, // what to do with this? // move this out + // }); } /** @@ -144,10 +160,12 @@ export const getAuthUserPayload = async ({ * @returns {ServiceTokenData} serviceTokenData - service token data */ export const getAuthSTDPayload = async ({ + req, authTokenValue, }: { + req: Request, authTokenValue: string; -}) => { +}): Promise => { const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); const serviceTokenData = await ServiceTokenData @@ -180,36 +198,21 @@ export const getAuthSTDPayload = async ({ if (!serviceTokenDataToReturn) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" }); - return serviceTokenDataToReturn; -} - -/** - * Return service account access key payload - * @param {Object} obj - * @param {String} obj.authTokenValue - service account access token value - * @returns {ServiceAccount} serviceAccount - */ -export const getAuthSAAKPayload = async ({ - authTokenValue, -}: { - authTokenValue: string; -}) => { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); - - const serviceAccount = await ServiceAccount.findById( - Buffer.from(TOKEN_IDENTIFIER, "base64").toString("hex") - ).select("+secretHash"); - - if (!serviceAccount) { - throw ServiceAccountNotFoundError({ message: "Failed to find service account" }); + return { + actor: { + type: ActorType.SERVICE, + metadata: { + serviceId: serviceTokenDataToReturn._id.toString(), + name: serviceTokenDataToReturn.name + } + }, + authPayload: serviceTokenDataToReturn, + ipAddress: req.realIP, + userAgent: req.headers["user-agent"] ?? "", + userAgentType: getUserAgentType(req.headers["user-agent"]) } - const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash); - if (!result) throw UnauthorizedRequestError({ - message: "Failed to authenticate service account access key", - }); - - return serviceAccount; + // return serviceTokenDataToReturn; } /** @@ -219,10 +222,12 @@ export const getAuthSAAKPayload = async ({ * @returns {APIKeyData} apiKeyData - API key data */ export const getAuthAPIKeyPayload = async ({ + req, authTokenValue, }: { + req: Request, authTokenValue: string; -}) => { +}): Promise => { const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); let apiKeyData = await APIKeyData @@ -264,7 +269,19 @@ export const getAuthAPIKeyPayload = async ({ }); } - return user; + return { + actor: { + type: ActorType.USER, + metadata: { + userId: user._id.toString(), + email: user.email + } + }, + authPayload: user, + ipAddress: req.realIP, + userAgent: req.headers["user-agent"] ?? "", + userAgentType: getUserAgentType(req.headers["user-agent"]) + } } /** diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 3123e1c16..1ba5c7abc 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -115,5 +115,5 @@ export const updateSubscriptionOrgQuantity = async ({ ); } - await EELicenseService.refreshPlan(organizationId); + await EELicenseService.refreshPlan(new Types.ObjectId(organizationId)); }; \ No newline at end of file diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 17e5c93ce..0d5562b4e 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -11,9 +11,9 @@ import { IServiceTokenData, Secret, SecretBlindIndexData, - ServiceTokenData + ServiceTokenData, } from "../models"; -import { SecretVersion } from "../ee/models"; +import { SecretVersion, EventType } from "../ee/models"; import { BadRequestError, InternalServerError, @@ -40,7 +40,7 @@ import { } from "../utils/crypto"; import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; -import { EELogService, EESecretService } from "../ee/services"; +import { EELogService, EESecretService, EEAuditLogService } from "../ee/services"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; @@ -433,10 +433,27 @@ export const createSecretHelper = async ({ ...getAuthDataPayloadIdObj(authData), workspaceId, actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP + channel: authData.userAgentType, + ipAddress: authData.ipAddress })); + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.CREATE_SECRET, + metadata: { + environment, + secretPath, + secretId: secret._id.toString(), + secretKey: secretName, + secretVersion: secret.version + } + }, + { + workspaceId + } + ); + // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId, @@ -457,8 +474,8 @@ export const createSecretHelper = async ({ environment, workspaceId, folderId, - channel: authData.authChannel, - userAgent: authData.authUserAgent + channel: authData.userAgentType, + userAgent: authData.userAgent } }); } @@ -528,9 +545,24 @@ export const getSecretsHelper = async ({ ...getAuthDataPayloadIdObj(authData), workspaceId, actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP + channel: authData.userAgentType, + ipAddress: authData.ipAddress })); + + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.GET_SECRETS, + metadata: { + environment, + secretPath, + numberOfSecrets: secrets.length + } + }, + { + workspaceId + } + ); const postHogClient = await TelemetryService.getPostHogClient(); @@ -545,8 +577,8 @@ export const getSecretsHelper = async ({ environment, workspaceId, folderId, - channel: authData.authChannel, - userAgent: authData.authUserAgent + channel: authData.userAgentType, + userAgent: authData.userAgent } }); } @@ -622,10 +654,27 @@ export const getSecretHelper = async ({ ...getAuthDataPayloadIdObj(authData), workspaceId, actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP + channel: authData.userAgentType, + ipAddress: authData.ipAddress })); + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.GET_SECRET, + metadata: { + environment, + secretPath, + secretId: secret._id.toString(), + secretKey: secretName, + secretVersion: secret.version + } + }, + { + workspaceId + } + ); + const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { @@ -639,8 +688,8 @@ export const getSecretHelper = async ({ environment, workspaceId, folderId, - channel: authData.authChannel, - userAgent: authData.authUserAgent + channel: authData.userAgentType, + userAgent: authData.userAgent } }); } @@ -771,9 +820,26 @@ export const updateSecretHelper = async ({ ...getAuthDataPayloadIdObj(authData), workspaceId, actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP + channel: authData.userAgentType, + ipAddress: authData.ipAddress })); + + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.UPDATE_SECRET, + metadata: { + environment, + secretPath, + secretId: secret._id.toString(), + secretKey: secretName, + secretVersion: secret.version + } + }, + { + workspaceId + } + ); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ @@ -795,8 +861,8 @@ export const updateSecretHelper = async ({ environment, workspaceId, folderId, - channel: authData.authChannel, - userAgent: authData.authUserAgent + channel: authData.userAgentType, + userAgent: authData.userAgent } }); } @@ -894,10 +960,27 @@ export const deleteSecretHelper = async ({ ...getAuthDataPayloadIdObj(authData), workspaceId, actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP + channel: authData.userAgentType, + ipAddress: authData.ipAddress })); + await EEAuditLogService.createAuditLog( + authData, + { + type: EventType.DELETE_SECRET, + metadata: { + environment, + secretPath, + secretId: secret._id.toString(), + secretKey: secretName, + secretVersion: secret.version + } + }, + { + workspaceId + } + ); + // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId, @@ -918,8 +1001,8 @@ export const deleteSecretHelper = async ({ environment, workspaceId, folderId, - channel: authData.authChannel, - userAgent: authData.authUserAgent + channel: authData.userAgentType, + userAgent: authData.userAgent } }); } diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index ef38e4fc1..b65a4543b 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,3 +1,4 @@ +import { Types } from "mongoose"; import { Bot, Key, @@ -25,7 +26,7 @@ export const createWorkspace = async ({ organizationId, }: { name: string; - organizationId: string; + organizationId: Types.ObjectId; }) => { // create workspace const workspace = await new Workspace({ diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index 3fba92348..bb2435ecf 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -1,15 +1,31 @@ import { Types } from "mongoose"; import { - IServiceAccount, IServiceTokenData, IUser, } from "../../models"; +import { + UserActor, + ServiceActor, + UserAgentType +} from "../../ee/models"; -export interface AuthData { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - authChannel: string; - authIP: string; - authUserAgent: string; +interface BaseAuthData { + ipAddress: string; + userAgent: string; + userAgentType: UserAgentType; tokenVersionId?: Types.ObjectId; -} \ No newline at end of file +} + +export interface UserAuthData extends BaseAuthData { + actor: UserActor; + authPayload: IUser; +} + +export interface ServiceTokenAuthData extends BaseAuthData { + actor: ServiceActor; + authPayload: IServiceTokenData; +} + +export type AuthData = + | UserAuthData + | ServiceTokenAuthData; \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 6c20be25a..e256f6665 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,25 +1,13 @@ import jwt from "jsonwebtoken"; -import { Types } from "mongoose"; import { NextFunction, Request, Response } from "express"; import { getAuthAPIKeyPayload, - getAuthSAAKPayload, getAuthSTDPayload, getAuthUserPayload, validateAuthMode, } from "../helpers/auth"; -import { - IServiceAccount, - IServiceTokenData, - IUser, -} from "../models"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; -import { getChannelFromUserAgent } from "../utils/posthog"; +import { AuthMode } from "../variables"; +import { AuthData } from "../interfaces/middleware"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -38,9 +26,9 @@ declare module "jsonwebtoken" { * @returns */ const requireAuth = ({ - acceptedAuthModes = [AUTH_MODE_JWT], + acceptedAuthModes = [AuthMode.JWT], }: { - acceptedAuthModes: string[]; + acceptedAuthModes: AuthMode[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -50,55 +38,36 @@ const requireAuth = ({ headers: req.headers, acceptedAuthModes, }); - - let authPayload: IUser | IServiceAccount | IServiceTokenData; - let authUserPayload: { - user: IUser; - tokenVersionId: Types.ObjectId; - }; + + let authData: AuthData; + switch (authMode) { - case AUTH_MODE_SERVICE_ACCOUNT: - authPayload = await getAuthSAAKPayload({ + case AuthMode.SERVICE_TOKEN: + authData = await getAuthSTDPayload({ + req, authTokenValue, }); - req.serviceAccount = authPayload; + req.serviceTokenData = authData.authPayload; break; - case AUTH_MODE_SERVICE_TOKEN: - authPayload = await getAuthSTDPayload({ - authTokenValue, + case AuthMode.API_KEY: + authData = await getAuthAPIKeyPayload({ + req, + authTokenValue }); - req.serviceTokenData = authPayload; + req.user = authData.authPayload; break; - case AUTH_MODE_API_KEY: - authPayload = await getAuthAPIKeyPayload({ - authTokenValue, + case AuthMode.JWT: + authData = await getAuthUserPayload({ + req, + authTokenValue }); - req.user = authPayload; - break; - default: - authUserPayload = await getAuthUserPayload({ - authTokenValue, - }); - authPayload = authUserPayload.user; - req.user = authUserPayload.user; - req.tokenVersionId = authUserPayload.tokenVersionId; + // authPayload = authUserPayload.user; + req.user = authData.authPayload; + // req.tokenVersionId = authUserPayload.tokenVersionId; // TODO break; } - - req.requestData = { - ...req.params, - ...req.query, - ...req.body, - } - - req.authData = { - authMode, - authPayload, // User, ServiceAccount, ServiceTokenData - authChannel: getChannelFromUserAgent(req.headers["user-agent"]), - authIP: req.realIP, - authUserAgent: req.headers["user-agent"] ?? "other", - tokenVersionId: req.tokenVersionId, - } + + req.authData = authData; return next(); } diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index f6f7405a9..e5e1e447d 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -32,6 +32,8 @@ const requireWorkspaceAuth = ({ const workspaceId = req[locationWorkspaceId]?.workspaceId; const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; + console.log("workspaceId: ", workspaceId); + // validate clients const { membership, workspace } = await validateClientForWorkspace({ authData: req.authData, diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index ae85aa36c..ce21f5136 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -4,7 +4,7 @@ import { body } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { authController } from "../../controllers/v1"; import { authLimiter } from "../../helpers/rateLimiter"; -import { AUTH_MODE_JWT } from "../../variables"; +import { AuthMode } from "../../variables"; router.post("/token", validateRequest, authController.getNewToken); @@ -30,7 +30,7 @@ router.post( "/logout", authLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), authController.logout ); @@ -38,7 +38,7 @@ router.post( router.post( "/checkAuth", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), authController.checkAuth ); @@ -53,9 +53,9 @@ router.delete( "/sessions", authLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), authController.revokeAllSessions ); -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index 0eafecad0..536e0a3bd 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -8,12 +8,12 @@ import { validateRequest, } from "../../middleware"; import { botController } from "../../controllers/v1"; -import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables"; +import { ADMIN, MEMBER, AuthMode } from "../../variables"; router.get( "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -27,7 +27,7 @@ router.get( router.patch( "/:botId/active", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireBotAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 1820f4bb8..0bd1dfbc6 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -8,9 +8,8 @@ import { } from "../../middleware"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; import { body, param } from "express-validator"; import { integrationController } from "../../controllers/v1"; @@ -18,7 +17,7 @@ import { integrationController } from "../../controllers/v1"; router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -44,7 +43,7 @@ router.post( router.patch( "/:integrationId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -64,7 +63,7 @@ router.patch( router.delete( "/:integrationId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 4fdc290e7..daf880c60 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -9,16 +9,15 @@ import { } from "../../middleware"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; import { integrationAuthController } from "../../controllers/v1"; router.get( "/integration-options", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), integrationAuthController.getIntegrationOptions ); @@ -26,7 +25,7 @@ router.get( router.get( "/:integrationAuthId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -39,7 +38,7 @@ router.get( router.post( "/oauth-token", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -62,7 +61,7 @@ router.post( body("integration").exists().trim().notEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -74,7 +73,7 @@ router.post( router.get( "/:integrationAuthId/apps", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -89,7 +88,7 @@ router.get( router.get( "/:integrationAuthId/teams", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -102,7 +101,7 @@ router.get( router.get( "/:integrationAuthId/vercel/branches", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -117,7 +116,7 @@ router.get( router.get( "/:integrationAuthId/railway/environments", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -131,7 +130,7 @@ router.get( router.get( "/:integrationAuthId/railway/services", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -145,7 +144,7 @@ router.get( router.get( "/:integrationAuthId/bitbucket/workspaces", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -158,7 +157,7 @@ router.get( router.get( "/:integrationAuthId/northflank/secret-groups", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -172,7 +171,7 @@ router.get( router.delete( "/:integrationAuthId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index edcb34c87..089c0e53a 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -3,12 +3,12 @@ const router = express.Router(); import { body } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { membershipOrgController } from "../../controllers/v1"; -import { AUTH_MODE_JWT } from "../../variables"; +import { AuthMode } from "../../variables"; router.post( "/signup", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("inviteeEmail").exists().trim().notEmpty().isEmail(), body("organizationId").exists().trim().notEmpty(), diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index fbd6e3eca..daa840ee7 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -6,13 +6,13 @@ import { validateRequest, } from "../../middleware"; import { body, param } from "express-validator"; -import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables"; +import { ADMIN, MEMBER, AuthMode } from "../../variables"; import { keyController } from "../../controllers/v1"; router.post( "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -27,7 +27,7 @@ router.post( router.get( "/:workspaceId/latest", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index 428213768..be495b62c 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -4,14 +4,14 @@ import { body, param } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { membershipController } from "../../controllers/v1"; import { membershipController as EEMembershipControllers } from "../../ee/controllers/v1"; -import { AUTH_MODE_JWT } from "../../variables"; +import { AuthMode } from "../../variables"; // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) router.get( // used for old CLI (deprecate) "/:workspaceId/connect", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("workspaceId").exists().trim(), validateRequest, @@ -21,7 +21,7 @@ router.get( // used for old CLI (deprecate) router.delete( "/:membershipId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("membershipId").exists().trim(), validateRequest, @@ -31,7 +31,7 @@ router.delete( router.post( "/:membershipId/change-role", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("role").exists().trim(), validateRequest, @@ -41,7 +41,7 @@ router.post( router.post( "/:membershipId/deny-permissions", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("membershipId").isMongoId().exists().trim(), body("permissions").isArray().exists(), diff --git a/backend/src/routes/v1/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts index 6b3c7d2e8..92472eb03 100644 --- a/backend/src/routes/v1/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -3,13 +3,13 @@ const router = express.Router(); import { param } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { membershipOrgController } from "../../controllers/v1"; -import { AUTH_MODE_JWT } from "../../variables"; +import { AuthMode } from "../../variables"; router.post( // TODO "/membershipOrg/:membershipOrgId/change-role", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("membershipOrgId"), validateRequest, @@ -19,7 +19,7 @@ router.post( router.delete( "/:membershipOrgId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("membershipOrgId").exists().trim(), validateRequest, diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 7cfd0e3fa..fdad1f9fa 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -9,16 +9,16 @@ import { import { ACCEPTED, ADMIN, - AUTH_MODE_JWT, MEMBER, OWNER, + AuthMode } from "../../variables"; import { organizationController } from "../../controllers/v1"; router.get( // deprecated (moved to api/v2/users/me/organizations) "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), organizationController.getOrganizations ); @@ -26,7 +26,7 @@ router.get( // deprecated (moved to api/v2/users/me/organizations) router.post( // not used on frontend "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("organizationName").exists().trim().notEmpty(), validateRequest, @@ -36,7 +36,7 @@ router.post( // not used on frontend router.get( "/:organizationId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -50,7 +50,7 @@ router.get( router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships) "/:organizationId/users", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -64,7 +64,7 @@ router.get( // deprecated (moved to api/v2/organizations/:organizationId/members router.get( "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -78,7 +78,7 @@ router.get( router.patch( "/:organizationId/name", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -93,7 +93,7 @@ router.patch( router.get( "/:organizationId/incidentContactOrg", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -107,7 +107,7 @@ router.get( router.post( "/:organizationId/incidentContactOrg", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -122,7 +122,7 @@ router.post( router.delete( "/:organizationId/incidentContactOrg", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -137,7 +137,7 @@ router.delete( router.post( "/:organizationId/customer-portal-session", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -151,7 +151,7 @@ router.post( router.get( "/:organizationId/subscriptions", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -165,7 +165,7 @@ router.get( router.get( "/:organizationId/workspace-memberships", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], diff --git a/backend/src/routes/v1/password.ts b/backend/src/routes/v1/password.ts index 7268b1a3c..3bccc0934 100644 --- a/backend/src/routes/v1/password.ts +++ b/backend/src/routes/v1/password.ts @@ -4,14 +4,12 @@ import { body } from "express-validator"; import { requireAuth, requireSignupAuth, validateRequest } from "../../middleware"; import { passwordController } from "../../controllers/v1"; import { passwordLimiter } from "../../helpers/rateLimiter"; -import { - AUTH_MODE_JWT, -} from "../../variables"; +import { AuthMode } from "../../variables"; router.post( "/srp1", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("clientPublicKey").exists().isString().trim().notEmpty(), validateRequest, @@ -22,7 +20,7 @@ router.post( "/change-password", passwordLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("clientProof").exists().trim().notEmpty(), body("protectedKey").exists().isString().trim().notEmpty(), @@ -65,7 +63,7 @@ router.post( "/backup-private-key", passwordLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("clientProof").exists().isString().trim().notEmpty(), body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key diff --git a/backend/src/routes/v1/secret.ts b/backend/src/routes/v1/secret.ts index 89ed3c975..6cebd4db8 100644 --- a/backend/src/routes/v1/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -10,8 +10,8 @@ import { body, param, query } from "express-validator"; import { secretController } from "../../controllers/v1"; import { ADMIN, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; // note to devs: these endpoints will be deprecated in favor of v2 @@ -19,7 +19,7 @@ import { router.post( "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -37,7 +37,7 @@ router.post( router.get( "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/secretImport.ts b/backend/src/routes/v1/secretImport.ts index 655fbd933..2e7e22bc3 100644 --- a/backend/src/routes/v1/secretImport.ts +++ b/backend/src/routes/v1/secretImport.ts @@ -3,12 +3,12 @@ const router = express.Router(); import { body, param, query } from "express-validator"; import { secretImportController } from "../../controllers/v1"; import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables"; +import { ADMIN, MEMBER, AuthMode } from "../../variables"; router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -27,7 +27,7 @@ router.post( router.put( "/:id", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT] }), param("id").exists().isString().trim(), body("secretImports").exists().isArray(), @@ -40,7 +40,7 @@ router.put( router.delete( "/:id", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT] }), param("id").exists().isString().trim(), body("secretImportPath").isString().exists().trim(), @@ -52,7 +52,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -68,7 +68,7 @@ router.get( router.get( "/secrets", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/secretScanning.ts b/backend/src/routes/v1/secretScanning.ts index fa162ce68..135556b1e 100644 --- a/backend/src/routes/v1/secretScanning.ts +++ b/backend/src/routes/v1/secretScanning.ts @@ -7,12 +7,12 @@ import { } from "../../middleware"; import { body, param } from "express-validator"; import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../controllers/v1/secretScanningController"; -import { ACCEPTED, ADMIN, MEMBER, OWNER } from "../../variables"; +import { ACCEPTED, ADMIN, MEMBER, OWNER, AuthMode } from "../../variables"; router.post( "/create-installation-session/organization/:organizationId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), param("organizationId").exists().trim(), requireOrganizationAuth({ @@ -26,7 +26,7 @@ router.post( router.post( "/link-installation", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), body("installationId").exists().trim(), body("sessionId").exists().trim(), @@ -37,7 +37,7 @@ router.post( router.get( "/installation-status/organization/:organizationId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), param("organizationId").exists().trim(), requireOrganizationAuth({ @@ -51,7 +51,7 @@ router.get( router.get( "/organization/:organizationId/risks", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), param("organizationId").exists().trim(), requireOrganizationAuth({ @@ -65,7 +65,7 @@ router.get( router.post( "/organization/:organizationId/risks/:riskId/status", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), param("organizationId").exists().trim(), param("riskId").exists().trim(), diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 83517f1ab..6a89e4b97 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -12,12 +12,12 @@ import { getFolders, updateFolderById, } from "../../controllers/v1/secretsFolderController"; -import { ADMIN, MEMBER } from "../../variables"; +import { ADMIN, MEMBER, AuthMode } from "../../variables"; router.post( "/", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -34,7 +34,7 @@ router.post( router.patch( "/:folderId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), body("workspaceId").exists(), body("environment").exists(), @@ -46,7 +46,7 @@ router.patch( router.delete( "/:folderId", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), body("workspaceId").exists(), body("environment").exists(), @@ -58,7 +58,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: ["jwt"], + acceptedAuthModes: [AuthMode.JWT], }), query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(), diff --git a/backend/src/routes/v1/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts index b3f3abb70..a3974e217 100644 --- a/backend/src/routes/v1/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -9,8 +9,8 @@ import { import { body } from "express-validator"; import { ADMIN, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; import { serviceTokenController } from "../../controllers/v1"; @@ -25,7 +25,7 @@ router.get( router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/user.ts b/backend/src/routes/v1/user.ts index d499a2377..73012c1ea 100644 --- a/backend/src/routes/v1/user.ts +++ b/backend/src/routes/v1/user.ts @@ -2,14 +2,12 @@ import express from "express"; const router = express.Router(); import { requireAuth } from "../../middleware"; import { userController } from "../../controllers/v1"; -import { - AUTH_MODE_JWT, -} from "../../variables"; +import { AuthMode } from "../../variables"; router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), userController.getUser ); diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index 29cc811bd..042f73c10 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -3,13 +3,13 @@ const router = express.Router(); import { requireAuth, validateRequest } from "../../middleware"; import { body, query } from "express-validator"; import { userActionController } from "../../controllers/v1"; -import { AUTH_MODE_JWT } from "../../variables"; +import { AuthMode } from "../../variables"; // note: [userAction] will be deprecated in /v2 in favor of [action] router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("action"), validateRequest, @@ -19,7 +19,7 @@ router.post( router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), query("action"), validateRequest, diff --git a/backend/src/routes/v1/webhook.ts b/backend/src/routes/v1/webhook.ts index 11507e68b..55c471ea3 100644 --- a/backend/src/routes/v1/webhook.ts +++ b/backend/src/routes/v1/webhook.ts @@ -2,13 +2,13 @@ import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { body, param, query } from "express-validator"; -import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables"; +import { ADMIN, MEMBER, AuthMode } from "../../variables"; import { webhookController } from "../../controllers/v1"; router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -27,7 +27,7 @@ router.post( router.patch( "/:webhookId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT], }), param("webhookId").exists().isString().trim(), body("isDisabled").default(false).isBoolean(), @@ -38,7 +38,7 @@ router.patch( router.post( "/:webhookId/test", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT], }), param("webhookId").exists().isString().trim(), validateRequest, @@ -48,7 +48,7 @@ router.post( router.delete( "/:webhookId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT], }), param("webhookId").exists().isString().trim(), validateRequest, @@ -58,7 +58,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 665473b1e..2fd178f52 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -8,16 +8,15 @@ import { } from "../../middleware"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; import { membershipController, workspaceController } from "../../controllers/v1"; router.get( "/:workspaceId/keys", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -31,7 +30,7 @@ router.get( router.get( "/:workspaceId/users", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -45,7 +44,7 @@ router.get( router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), workspaceController.getWorkspaces ); @@ -53,7 +52,7 @@ router.get( router.get( "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -67,7 +66,7 @@ router.get( router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("workspaceName").exists().trim().notEmpty(), body("organizationId").exists().trim().notEmpty(), @@ -78,7 +77,7 @@ router.post( router.delete( "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -92,7 +91,7 @@ router.delete( router.post( "/:workspaceId/name", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -107,7 +106,7 @@ router.post( router.post( "/:workspaceId/invite-signup", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -122,7 +121,7 @@ router.post( router.get( "/:workspaceId/integrations", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -136,7 +135,7 @@ router.get( router.get( "/:workspaceId/authorizations", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -150,7 +149,7 @@ router.get( router.get( "/:workspaceId/service-tokens", // deprecate requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index f9943f33d..f383aa2fd 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -9,14 +9,14 @@ import { } from "../../middleware"; import { ADMIN, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; router.post( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -32,7 +32,7 @@ router.post( router.put( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -49,7 +49,7 @@ router.put( router.delete( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -64,7 +64,7 @@ router.delete( router.get( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index 46223cf93..6196796cd 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -10,10 +10,9 @@ import { body, param } from "express-validator"; import { ACCEPTED, ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, MEMBER, OWNER, + AuthMode } from "../../variables"; import { organizationsController } from "../../controllers/v2"; @@ -24,7 +23,7 @@ router.get( param("organizationId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -40,7 +39,7 @@ router.patch( body("role").exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], @@ -59,7 +58,7 @@ router.delete( param("membershipId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], @@ -77,7 +76,7 @@ router.get( param("organizationId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], @@ -91,7 +90,7 @@ router.get( param("organizationId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index e577d2a47..9b7526a99 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -8,9 +8,8 @@ import { import { body, param, query } from "express-validator"; import { ADMIN, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_TOKEN, MEMBER, + AuthMode, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, } from "../../variables"; @@ -24,7 +23,7 @@ const router = express.Router(); router.post( "/batch-create/workspace/:workspaceId/environment/:environment", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -41,7 +40,7 @@ router.post( router.post( "/workspace/:workspaceId/environment/:environment", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -60,7 +59,7 @@ router.get( param("workspaceId").exists().trim(), query("environment").exists(), requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN], + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -74,7 +73,7 @@ router.get( router.get( "/:secretId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN], + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -87,7 +86,7 @@ router.get( router.delete( "/batch/workspace/:workspaceId/environment/:environmentName", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("workspaceId").exists().isMongoId().trim(), param("environmentName").exists().trim(), @@ -103,7 +102,7 @@ router.delete( router.delete( "/:secretId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -117,7 +116,7 @@ router.delete( router.patch( "/batch-modify/workspace/:workspaceId/environment/:environmentName", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("secrets").exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), param("workspaceId").exists().isMongoId().trim(), @@ -133,7 +132,7 @@ router.patch( router.patch( "/workspace/:workspaceId/environment/:environmentName", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), body("secret").isObject(), param("workspaceId").exists().isMongoId().trim(), diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 59665fd03..e347e11c3 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -12,11 +12,8 @@ import { body, query } from "express-validator"; import { secretsController } from "../../controllers/v2"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, MEMBER, + AuthMode, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, SECRET_PERSONAL, @@ -27,7 +24,7 @@ import { BatchSecretRequest } from "../../types/secret"; router.post( "/batch", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -109,7 +106,7 @@ router.post( }), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -130,12 +127,7 @@ router.get( query("include_imports").optional().default(false).isBoolean(), validateRequest, requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -172,7 +164,7 @@ router.patch( }), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -201,7 +193,7 @@ router.delete( .isEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v2/serviceAccounts.ts b/backend/src/routes/v2/serviceAccounts.ts index 244739e72..daf048a23 100644 --- a/backend/src/routes/v2/serviceAccounts.ts +++ b/backend/src/routes/v2/serviceAccounts.ts @@ -1,159 +1,158 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireOrganizationAuth, - requireServiceAccountAuth, - requireServiceAccountWorkspacePermissionAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { body, param, query } from "express-validator"; -import { - ACCEPTED, - ADMIN, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - MEMBER, - OWNER, -} from "../../variables"; -import { serviceAccountsController } from "../../controllers/v2"; +// import { +// requireAuth, +// requireOrganizationAuth, +// requireServiceAccountAuth, +// requireServiceAccountWorkspacePermissionAuth, +// requireWorkspaceAuth, +// validateRequest, +// } from "../../middleware"; +// import { body, param, query } from "express-validator"; +// import { +// ACCEPTED, +// ADMIN, +// MEMBER, +// OWNER, +// AuthMode +// } from "../../variables"; +// import { serviceAccountsController } from "../../controllers/v2"; -router.get( // TODO: check - "/me", - requireAuth({ - acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT], - }), - serviceAccountsController.getCurrentServiceAccount -); +// router.get( // TODO: check +// "/me", +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT], +// }), +// serviceAccountsController.getCurrentServiceAccount +// ); -router.get( - "/:serviceAccountId", - param("serviceAccountId").exists().isString().trim(), - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - serviceAccountsController.getServiceAccountById -); +// router.get( +// "/:serviceAccountId", +// param("serviceAccountId").exists().isString().trim(), +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// serviceAccountsController.getServiceAccountById +// ); -router.post( - "/", - body("organizationId").exists().isString().trim(), - body("name").exists().isString().trim(), - body("publicKey").exists().isString().trim(), - body("expiresIn").isNumeric(), // measured in ms - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "body", - }), - serviceAccountsController.createServiceAccount -); +// router.post( +// "/", +// body("organizationId").exists().isString().trim(), +// body("name").exists().isString().trim(), +// body("publicKey").exists().isString().trim(), +// body("expiresIn").isNumeric(), // measured in ms +// validateRequest, +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireOrganizationAuth({ +// acceptedRoles: [OWNER, ADMIN, MEMBER], +// acceptedStatuses: [ACCEPTED], +// locationOrganizationId: "body", +// }), +// serviceAccountsController.createServiceAccount +// ); -router.patch( - "/:serviceAccountId/name", - param("serviceAccountId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - serviceAccountsController.changeServiceAccountName -); +// router.patch( +// "/:serviceAccountId/name", +// param("serviceAccountId").exists().isString().trim(), +// validateRequest, +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// serviceAccountsController.changeServiceAccountName +// ); -router.delete( - "/:serviceAccountId", - param("serviceAccountId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - serviceAccountsController.deleteServiceAccount -); +// router.delete( +// "/:serviceAccountId", +// param("serviceAccountId").exists().isString().trim(), +// validateRequest, +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// serviceAccountsController.deleteServiceAccount +// ); -router.get( - "/:serviceAccountId/permissions/workspace", - param("serviceAccountId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - serviceAccountsController.getServiceAccountWorkspacePermissions -); +// router.get( +// "/:serviceAccountId/permissions/workspace", +// param("serviceAccountId").exists().isString().trim(), +// validateRequest, +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// serviceAccountsController.getServiceAccountWorkspacePermissions +// ); -router.post( - "/:serviceAccountId/permissions/workspace", - param("serviceAccountId").exists().isString().trim(), - body("workspaceId").exists().isString().notEmpty(), - body("environment").exists().isString().notEmpty(), - body("read").isBoolean().optional(), - body("write").isBoolean().optional(), - body("encryptedKey").exists().isString().notEmpty(), - body("nonce").exists().isString().notEmpty(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - serviceAccountsController.addServiceAccountWorkspacePermission -); +// router.post( +// "/:serviceAccountId/permissions/workspace", +// param("serviceAccountId").exists().isString().trim(), +// body("workspaceId").exists().isString().notEmpty(), +// body("environment").exists().isString().notEmpty(), +// body("read").isBoolean().optional(), +// body("write").isBoolean().optional(), +// body("encryptedKey").exists().isString().notEmpty(), +// body("nonce").exists().isString().notEmpty(), +// validateRequest, +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// requireWorkspaceAuth({ +// acceptedRoles: [ADMIN, MEMBER], +// locationWorkspaceId: "body", +// }), +// serviceAccountsController.addServiceAccountWorkspacePermission +// ); -router.delete( - "/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId", - param("serviceAccountId").exists().isString().trim(), - param("serviceAccountWorkspacePermissionId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - requireServiceAccountWorkspacePermissionAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - serviceAccountsController.deleteServiceAccountWorkspacePermission -); +// router.delete( +// "/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId", +// param("serviceAccountId").exists().isString().trim(), +// param("serviceAccountWorkspacePermissionId").exists().isString().trim(), +// validateRequest, +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// requireServiceAccountWorkspacePermissionAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// serviceAccountsController.deleteServiceAccountWorkspacePermission +// ); -router.get( - "/:serviceAccountId/keys", - query("workspaceId").optional().isString(), - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], - }), - requireServiceAccountAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - serviceAccountsController.getServiceAccountKeys -); +// router.get( +// "/:serviceAccountId/keys", +// query("workspaceId").optional().isString(), +// requireAuth({ +// acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], +// }), +// requireServiceAccountAuth({ +// acceptedRoles: [OWNER, ADMIN], +// acceptedStatuses: [ACCEPTED], +// }), +// serviceAccountsController.getServiceAccountKeys +// ); export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 84f443deb..aafd1cd47 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -9,10 +9,8 @@ import { import { body, param } from "express-validator"; import { ADMIN, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, MEMBER, + AuthMode, PERMISSION_WRITE_SECRETS } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; @@ -20,7 +18,7 @@ import { serviceTokenDataController } from "../../controllers/v2"; router.get( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.SERVICE_TOKEN] }), serviceTokenDataController.getServiceTokenData ); @@ -28,7 +26,7 @@ router.get( router.post( "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] + acceptedAuthModes: [AuthMode.JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -63,7 +61,7 @@ router.post( router.delete( "/:serviceTokenDataId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AuthMode.JWT] }), requireServiceTokenDataAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index 8974bd9fd..af82db6ac 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -9,14 +9,14 @@ import { } from "../../middleware"; import { ADMIN, - AUTH_MODE_JWT, MEMBER, + AuthMode } from "../../variables"; router.get( "/:workspaceId/tags", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], @@ -30,7 +30,7 @@ router.get( router.delete( "/tags/:tagId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), param("tagId").exists().trim(), validateRequest, @@ -40,7 +40,7 @@ router.delete( router.post( "/:workspaceId/tags", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index 334ef523b..378627296 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -6,10 +6,7 @@ import { } from "../../middleware"; import { body, param } from "express-validator"; import { usersController } from "../../controllers/v2"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, -} from "../../variables"; +import { AuthMode } from "../../variables"; import { AuthProvider } from "../../models"; @@ -17,7 +14,7 @@ import { router.get( "/me", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), usersController.getMe ); @@ -25,7 +22,7 @@ router.get( router.patch( "/me/mfa", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), body("isMfaEnabled").exists().isBoolean(), validateRequest, @@ -35,7 +32,7 @@ router.patch( router.patch( "/me/name", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), body("firstName").exists().isString(), body("lastName").isString(), @@ -46,7 +43,7 @@ router.patch( router.patch( "/me/auth-provider", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), body("authProvider").exists().isString().isIn([ AuthProvider.EMAIL, @@ -60,7 +57,7 @@ router.patch( router.get( "/me/organizations", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), usersController.getMyOrganizations ); @@ -68,7 +65,7 @@ router.get( router.get( "/me/api-keys", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), usersController.getMyAPIKeys ); @@ -76,7 +73,7 @@ router.get( router.post( "/me/api-keys", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), body("name").exists().isString().trim(), body("expiresIn").isNumeric(), @@ -87,7 +84,7 @@ router.post( router.delete( "/me/api-keys/:apiKeyDataId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), param("apiKeyDataId").exists().trim(), validateRequest, @@ -97,7 +94,7 @@ router.delete( router.get( "/me/sessions", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), usersController.getMySessions ); @@ -105,7 +102,7 @@ router.get( router.delete( "/me/sessions", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), usersController.deleteMySessions ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index c36a0b7f8..a93bad48e 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -9,17 +9,15 @@ import { } from "../../middleware"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_TOKEN, MEMBER, + AuthMode } from "../../variables"; import { workspaceController } from "../../controllers/v2"; router.post( "/:workspaceId/secrets", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -37,7 +35,7 @@ router.post( router.get( "/:workspaceId/secrets", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN], + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -53,7 +51,7 @@ router.get( router.get( "/:workspaceId/encrypted-key", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -67,7 +65,7 @@ router.get( router.get( "/:workspaceId/service-token-data", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -83,7 +81,7 @@ router.get( // new - TODO: rewire dashboard to this route param("workspaceId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -99,7 +97,7 @@ router.patch( // TODO - rewire dashboard to this route body("role").exists().isString().trim().isIn([ADMIN, MEMBER]), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -118,7 +116,7 @@ router.delete( // TODO - rewire dashboard to this route param("membershipId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -134,7 +132,7 @@ router.delete( // TODO - rewire dashboard to this route router.patch( "/:workspaceId/auto-capitalization", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 6d3b4911d..11500cb97 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -5,11 +5,8 @@ import { body, param, query } from "express-validator"; import { secretsController } from "../../controllers/v3"; import { ADMIN, - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, MEMBER, + AuthMode, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, SECRET_PERSONAL, @@ -25,10 +22,9 @@ router.get( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), secretsController.getSecretsRaw @@ -44,10 +40,9 @@ router.get( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -73,10 +68,9 @@ router.post( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -102,10 +96,9 @@ router.patch( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -130,10 +123,9 @@ router.delete( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -156,10 +148,9 @@ router.get( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -192,10 +183,9 @@ router.post( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -220,10 +210,9 @@ router.get( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -250,10 +239,9 @@ router.patch( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ @@ -278,10 +266,9 @@ router.delete( validateRequest, requireAuth({ acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT + AuthMode.JWT, + AuthMode.API_KEY, + AuthMode.SERVICE_TOKEN ] }), requireWorkspaceAuth({ diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts index 7aa909693..38695e8b9 100644 --- a/backend/src/routes/v3/workspaces.ts +++ b/backend/src/routes/v3/workspaces.ts @@ -8,7 +8,7 @@ import { import { workspacesController } from "../../controllers/v3"; import { ADMIN, - AUTH_MODE_JWT, + AuthMode } from "../../variables"; import { body, param } from "express-validator"; @@ -19,7 +19,7 @@ router.get( param("workspaceId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -33,7 +33,7 @@ router.get( // allow admins to get all workspace secrets (part of blind indices param("workspaceId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], @@ -65,7 +65,7 @@ router.post( // allow admins to name all workspace secrets (part of blind indice .withMessage("secretId must be a string"), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT], + acceptedAuthModes: [AuthMode.JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index c7713a327..d3a5fbc23 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,8 +1,6 @@ import { Types } from "mongoose"; - - import { - AuthData, + AuthData } from "../../interfaces/middleware"; declare module "express" { diff --git a/backend/src/utils/posthog.ts b/backend/src/utils/posthog.ts index 06c404888..0de0fa120 100644 --- a/backend/src/utils/posthog.ts +++ b/backend/src/utils/posthog.ts @@ -1,15 +1,15 @@ -const CLI_USER_AGENT_NAME = "cli" -const K8_OPERATOR_AGENT_NAME = "k8-operator" -export const getChannelFromUserAgent = function (userAgent: string | undefined) { +import { UserAgentType } from "../ee/models" + +export const getUserAgentType = function (userAgent: string | undefined) { if (userAgent == undefined) { - return "other" - } else if (userAgent == CLI_USER_AGENT_NAME) { - return "cli" - } else if (userAgent == K8_OPERATOR_AGENT_NAME) { - return "k8-operator" + return UserAgentType.OTHER; + } else if (userAgent == UserAgentType.CLI) { + return UserAgentType.CLI; + } else if (userAgent == UserAgentType.K8_OPERATOR) { + return UserAgentType.K8_OPERATOR; } else if (userAgent.toLowerCase().includes("mozilla")) { - return "web" + return UserAgentType.WEB; } else { - return "other" + return UserAgentType.OTHER; } } \ No newline at end of file diff --git a/backend/src/validation/bot.ts b/backend/src/validation/bot.ts index 7e19ca770..2bb6ec6dd 100644 --- a/backend/src/validation/bot.ts +++ b/backend/src/validation/bot.ts @@ -1,25 +1,15 @@ import { Types } from "mongoose"; import { Bot, - IServiceAccount, - IServiceTokenData, IUser, - ServiceAccount, - ServiceTokenData, - User, } from "../models"; -import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; import { validateUserClientForWorkspace } from "./user"; import { BotNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; /** * Validate authenticated clients for bot with id [botId] based @@ -34,65 +24,24 @@ export const validateClientForBot = async ({ botId, acceptedRoles, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; botId: Types.ObjectId; acceptedRoles: Array<"admin" | "member">; }) => { const bot = await Bot.findById(botId); - if (!bot) throw BotNotFoundError(); - - if ( - authData.authMode === AUTH_MODE_JWT && - authData.authPayload instanceof User - ) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: bot.workspace, - acceptedRoles, - }); - - return bot; + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: bot.workspace, + acceptedRoles, + }); + return bot; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for bot", + }); } - - if ( - authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && - authData.authPayload instanceof ServiceAccount - ) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: bot.workspace, - }); - - return bot; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_TOKEN && - authData.authPayload instanceof ServiceTokenData - ) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for bot", - }); - } - - if ( - authData.authMode === AUTH_MODE_API_KEY && - authData.authPayload instanceof User - ) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: bot.workspace, - acceptedRoles, - }); - - return bot; - } - - throw BotNotFoundError({ - message: "Failed client authorization for bot", - }); }; \ No newline at end of file diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index 3f39a87b6..b1143a9f7 100644 --- a/backend/src/validation/integration.ts +++ b/backend/src/validation/integration.ts @@ -1,15 +1,9 @@ import { Types } from "mongoose"; import { - IServiceAccount, - IServiceTokenData, IUser, Integration, IntegrationAuth, - ServiceAccount, - ServiceTokenData, - User, } from "../models"; -import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; import { validateUserClientForWorkspace } from "./user"; import { IntegrationService } from "../services"; import { @@ -17,12 +11,8 @@ import { IntegrationNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; /** * Validate authenticated clients for integration with id [integrationId] based @@ -39,10 +29,7 @@ export const validateClientForIntegration = async ({ integrationId, acceptedRoles, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; integrationId: Types.ObjectId; acceptedRoles: Array<"admin" | "member">; }) => { @@ -61,43 +48,19 @@ export const validateClientForIntegration = async ({ const accessToken = (await IntegrationService.getIntegrationAuthAccess({ integrationAuthId: integrationAuth._id, })).accessToken; - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: integration.workspace, - acceptedRoles, - }); - - return ({ integration, accessToken }); - } - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: integration.workspace, - }); + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: integration.workspace, + acceptedRoles, + }); - return ({ integration, accessToken }); + return ({ integration, accessToken }); + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration", + }); } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for integration", - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: integration.workspace, - acceptedRoles, - }); - - return ({ integration, accessToken }); - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for integration", - }); } \ No newline at end of file diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index fa77f3d62..676184324 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -1,27 +1,17 @@ import { Types } from "mongoose"; import { - IServiceAccount, - IServiceTokenData, IUser, IWorkspace, IntegrationAuth, - ServiceAccount, - ServiceTokenData, - User, } from "../models"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; import { IntegrationAuthNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; import { IntegrationService } from "../services"; import { validateUserClientForWorkspace } from "./user"; -import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; /** * Validate authenticated clients for integration authorization with id [integrationAuthId] based @@ -38,10 +28,7 @@ import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; acceptedRoles, attachAccessToken, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; integrationAuthId: Types.ObjectId; acceptedRoles: Array<"admin" | "member">; attachAccessToken?: boolean; @@ -66,44 +53,20 @@ import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; accessId = access.accessId; } - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: integrationAuth.workspace._id, - acceptedRoles, - }); + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: integrationAuth.workspace._id, + acceptedRoles, + }); - return ({ integrationAuth, accessToken, accessId }); + return ({ integrationAuth, accessToken, accessId }); + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration authorization", + }); } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: integrationAuth.workspace._id, - }); - - return ({ integrationAuth, accessToken, accessId }); - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for integration authorization", - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: integrationAuth.workspace._id, - acceptedRoles, - }); - - return ({ integrationAuth, accessToken, accessId }); - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for integration authorization", - }); } export { diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index aee5f8bca..d788f4461 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -15,12 +15,9 @@ import { MembershipNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; +import { auth } from "../routes/v1"; /** * Validate authenticated clients for membership with id [membershipId] based @@ -36,10 +33,7 @@ export const validateClientForMembership = async ({ membershipId, acceptedRoles, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; membershipId: Types.ObjectId; acceptedRoles: Array<"admin" | "member">; }) => { @@ -49,46 +43,22 @@ export const validateClientForMembership = async ({ if (!membership) throw MembershipNotFoundError({ message: "Failed to find membership", }); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: membership.workspace, - acceptedRoles, - }); - - return membership; - } - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: membership.workspace, - }); - - return membership; + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: membership.workspace, + acceptedRoles, + }); + + return membership; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(membership.workspace), + }); + + return membership; } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload, - workspaceId: new Types.ObjectId(membership.workspace), - }); - - return membership; - } - - if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: membership.workspace, - acceptedRoles, - }); - - return membership; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for membership", - }); } \ No newline at end of file diff --git a/backend/src/validation/membershipOrg.ts b/backend/src/validation/membershipOrg.ts index 444f2aa2a..b0ada6a61 100644 --- a/backend/src/validation/membershipOrg.ts +++ b/backend/src/validation/membershipOrg.ts @@ -1,12 +1,6 @@ import { Types } from "mongoose"; import { - IServiceAccount, - IServiceTokenData, - IUser, MembershipOrg, - ServiceAccount, - ServiceTokenData, - User, } from "../models"; import { validateMembershipOrg, @@ -15,12 +9,8 @@ import { MembershipOrgNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; /** * Validate authenticated clients for organization membership with id [membershipOrgId] based @@ -37,10 +27,7 @@ export const validateClientForMembershipOrg = async ({ acceptedRoles, acceptedStatuses, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; membershipOrgId: Types.ObjectId; acceptedRoles: Array<"owner" | "admin" | "member">; acceptedStatuses: Array<"invited" | "accepted">; @@ -50,44 +37,20 @@ export const validateClientForMembershipOrg = async ({ if (!membershipOrg) throw MembershipOrgNotFoundError({ message: "Failed to find organization membership ", }); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateMembershipOrg({ - userId: authData.authPayload._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses, - }); - - return membershipOrg; - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({ - message: "Failed service account client authorization for organization membership", - }); - - return membershipOrg; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: "Failed service account client authorization for organization membership", - }); - } - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateMembershipOrg({ - userId: authData.authPayload._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses, - }); - - return membershipOrg; + switch (authData.actor.type) { + case ActorType.USER: + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses, + }); + + return membershipOrg; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service account client authorization for organization membership", + }); } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for organization membership", - }); } \ No newline at end of file diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index 35e0aab04..c4838d154 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -1,25 +1,15 @@ import { Types } from "mongoose"; import { - IServiceAccount, - IServiceTokenData, IUser, Organization, - ServiceAccount, - ServiceTokenData, - User, } from "../models"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; import { OrganizationNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; import { validateUserClientForOrganization } from "./user"; -import { validateServiceAccountClientForOrganization } from "./serviceAccount"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; /** * Validate accepted clients for organization with id [organizationId] @@ -33,10 +23,7 @@ export const validateClientForOrganization = async ({ acceptedRoles, acceptedStatuses, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; organizationId: Types.ObjectId; acceptedRoles: Array<"owner" | "admin" | "member">; acceptedStatuses: Array<"invited" | "accepted">; @@ -48,57 +35,20 @@ export const validateClientForOrganization = async ({ message: "Failed to find organization", }); } + + switch (authData.actor.type) { + case ActorType.USER: + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload as IUser, + organization, + acceptedRoles, + acceptedStatuses, + }); - if ( - authData.authMode === AUTH_MODE_JWT && - authData.authPayload instanceof User - ) { - const membershipOrg = await validateUserClientForOrganization({ - user: authData.authPayload, - organization, - acceptedRoles, - acceptedStatuses, - }); - - return { organization, membershipOrg }; + return { organization, membershipOrg }; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for organization", + }); } - - if ( - authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && - authData.authPayload instanceof ServiceAccount - ) { - await validateServiceAccountClientForOrganization({ - serviceAccount: authData.authPayload, - organization, - }); - - return { organization }; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_TOKEN && - authData.authPayload instanceof ServiceTokenData - ) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for organization", - }); - } - - if ( - authData.authMode === AUTH_MODE_API_KEY && - authData.authPayload instanceof User - ) { - const membershipOrg = await validateUserClientForOrganization({ - user: authData.authPayload, - organization, - acceptedRoles, - acceptedStatuses, - }); - - return { organization, membershipOrg }; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for organization", - }); }; \ No newline at end of file diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 0de983dbe..c51e83e23 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -2,25 +2,17 @@ import { Types } from "mongoose"; import { ISecret, Secret, - ServiceAccount, - ServiceTokenData, - User, + IUser, + IServiceTokenData, } from "../models"; -import { validateServiceAccountClientForSecrets, validateServiceAccountClientForWorkspace } from "./serviceAccount"; import { validateUserClientForSecret, validateUserClientForSecrets } from "./user"; import { validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; -import { AuthData } from "../interfaces/middleware"; import { BadRequestError, SecretNotFoundError, - UnauthorizedRequestError, } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; /** * Validate authenticated clients for secrets with id [secretId] based @@ -47,53 +39,26 @@ export const validateClientForSecret = async ({ if (!secret) throw SecretNotFoundError({ message: "Failed to find secret", }); + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForSecret({ + user: authData.authPayload as IUser, + secret, + acceptedRoles, + requiredPermissions, + }); - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForSecret({ - user: authData.authPayload, - secret, - acceptedRoles, - requiredPermissions, - }); - - return secret; - } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: secret.workspace, - environment: secret.environment, - requiredPermissions, - }); + return secret; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: secret.workspace, + environment: secret.environment, + }); - return secret; + return secret; } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload, - workspaceId: secret.workspace, - environment: secret.environment, - }); - - return secret; - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForSecret({ - user: authData.authPayload, - secret, - acceptedRoles, - requiredPermissions, - }); - - return secret; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for secret", - }); } /** @@ -127,48 +92,23 @@ export const validateClientForSecrets = async ({ if (secrets.length != secretIds.length) { throw BadRequestError({ message: "Failed to validate non-existent secrets" }) } - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForSecrets({ - user: authData.authPayload, - secrets, - requiredPermissions, - }); - - return secrets; - } - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForSecrets({ - serviceAccount: authData.authPayload, - secrets, - requiredPermissions, - }); - - return secrets; + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForSecrets({ + user: authData.authPayload as IUser, + secrets, + requiredPermissions, + }); + + return secrets; + case ActorType.SERVICE: + await validateServiceTokenDataClientForSecrets({ + serviceTokenData: authData.authPayload as IServiceTokenData, + secrets, + requiredPermissions, + }); + + return secrets; } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForSecrets({ - serviceTokenData: authData.authPayload, - secrets, - requiredPermissions, - }); - - return secrets; - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForSecrets({ - user: authData.authPayload, - secrets, - requiredPermissions, - }); - - return secrets; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for secrets resource", - }); } \ No newline at end of file diff --git a/backend/src/validation/serviceAccount.ts b/backend/src/validation/serviceAccount.ts index 78f2c9e69..b4cb562c4 100644 --- a/backend/src/validation/serviceAccount.ts +++ b/backend/src/validation/serviceAccount.ts @@ -4,12 +4,9 @@ import { IOrganization, ISecret, IServiceAccount, - IServiceTokenData, IUser, ServiceAccount, ServiceAccountWorkspacePermission, - ServiceTokenData, - User, } from "../models"; import { validateUserClientForServiceAccount } from "./user"; import { @@ -18,23 +15,18 @@ import { UnauthorizedRequestError, } from "../utils/errors"; import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, } from "../variables"; +import { AuthData } from "../interfaces/middleware"; +import { ActorType } from "../ee/models"; export const validateClientForServiceAccount = async ({ authData, serviceAccountId, requiredPermissions, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }, + authData: AuthData; serviceAccountId: Types.ObjectId; requiredPermissions?: string[]; }) => { @@ -46,45 +38,20 @@ export const validateClientForServiceAccount = async ({ }); } - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForServiceAccount({ - user: authData.authPayload, - serviceAccount, - requiredPermissions, - }); - - return serviceAccount; + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForServiceAccount({ + user: authData.authPayload as IUser, + serviceAccount, + requiredPermissions, + }); + + return serviceAccount; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for service account resource", + }); } - - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForServiceAccount({ - serviceAccount: authData.authPayload, - targetServiceAccount: serviceAccount, - requiredPermissions, - }); - - return serviceAccount; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for service account resource", - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForServiceAccount({ - user: authData.authPayload, - serviceAccount, - requiredPermissions, - }); - - return serviceAccount; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for service account resource", - }); } /** diff --git a/backend/src/validation/serviceTokenData.ts b/backend/src/validation/serviceTokenData.ts index 580bcbdb9..fe8e3bd0b 100644 --- a/backend/src/validation/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -1,22 +1,14 @@ import { Types } from "mongoose"; import { ISecret, - IServiceAccount, IServiceTokenData, IUser, - ServiceAccount, ServiceTokenData, - User } from "../models"; import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN -} from "../variables"; import { validateUserClientForWorkspace } from "./user"; -import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { ActorType } from "../ee/models"; +import { AuthData } from "../interfaces/middleware"; /** * Validate authenticated clients for service token with id [serviceTokenId] based @@ -31,10 +23,7 @@ export const validateClientForServiceTokenData = async ({ serviceTokenDataId, acceptedRoles }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; + authData: AuthData; serviceTokenDataId: Types.ObjectId; acceptedRoles: Array<"admin" | "member">; }) => { @@ -42,55 +31,24 @@ export const validateClientForServiceTokenData = async ({ .select("+encryptedKey +iv +tag") .populate<{ user: IUser }>("user"); - if (!serviceTokenData) - throw ServiceTokenDataNotFoundError({ - message: "Failed to find service token data" - }); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: serviceTokenData.workspace, - acceptedRoles - }); - - return serviceTokenData; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && - authData.authPayload instanceof ServiceAccount - ) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: serviceTokenData.workspace - }); - - return serviceTokenData; - } - - if ( - authData.authMode === AUTH_MODE_SERVICE_TOKEN && - authData.authPayload instanceof ServiceTokenData - ) { - throw UnauthorizedRequestError({ - message: "Failed service token authorization for service token data" - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: serviceTokenData.workspace, - acceptedRoles - }); - - return serviceTokenData; - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for service token data" + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ + message: "Failed to find service token data" }); + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: serviceTokenData.workspace, + acceptedRoles + }); + + return serviceTokenData; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for service token data" + }); + } }; /** diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index 618ccb02f..8c8f4408d 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -2,15 +2,14 @@ import net from "net"; import { Types } from "mongoose"; import { SecretBlindIndexData, - ServiceAccount, - ServiceTokenData, - User, + IServiceTokenData, + IUser, Workspace, } from "../models"; import { + ActorType, TrustedIP } from "../ee/models"; -import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { @@ -18,12 +17,6 @@ import { UnauthorizedRequestError, WorkspaceNotFoundError, } from "../utils/errors"; -import { - AUTH_MODE_API_KEY, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, -} from "../variables"; import { BotService } from "../services"; import { AuthData } from "../interfaces/middleware"; import { extractIPDetails } from "../utils/ip"; @@ -85,89 +78,59 @@ export const validateClientForWorkspace = async ({ }); } - + switch (authData.actor.type) { + case ActorType.USER: + const membership = await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId, + environment, + acceptedRoles, + requiredPermissions, + }); + + return ({ membership, workspace }); + case ActorType.SERVICE: + if (checkIPAllowlist) { + const trustedIps = await TrustedIP.find({ + workspace: workspaceId + }); + + if (trustedIps.length > 0) { + // case: check the IP address of the inbound request against trusted IPs - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - const membership = await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - }); + const blockList = new net.BlockList(); - return ({ membership, workspace }); - } + for (const trustedIp of trustedIps) { + if (trustedIp.prefix !== undefined) { + blockList.addSubnet( + trustedIp.ipAddress, + trustedIp.prefix, + trustedIp.type + ); + } else { + blockList.addAddress( + trustedIp.ipAddress, + trustedIp.type + ); + } + } + + const { type } = extractIPDetails(authData.ipAddress); + const check = blockList.check(authData.ipAddress, type); + + if (!check) throw UnauthorizedRequestError({ + message: "Failed workspace authorization" + }); + } + } - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId, - environment, - requiredPermissions, - }); - - return {}; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - if (checkIPAllowlist) { - const trustedIps = await TrustedIP.find({ - workspace: workspaceId + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId, + environment, + requiredPermissions, }); - if (trustedIps.length > 0) { - // case: check the IP address of the inbound request against trusted IPs - - const blockList = new net.BlockList(); - - for (const trustedIp of trustedIps) { - if (trustedIp.prefix !== undefined) { - blockList.addSubnet( - trustedIp.ipAddress, - trustedIp.prefix, - trustedIp.type - ); - } else { - blockList.addAddress( - trustedIp.ipAddress, - trustedIp.type - ); - } - } - - const { type } = extractIPDetails(authData.authIP); - const check = blockList.check(authData.authIP, type); - - if (!check) throw UnauthorizedRequestError({ - message: "Failed workspace authorization" - }); - } - } - - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload, - workspaceId, - environment, - requiredPermissions, - }); - - return {}; + return {}; } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - const membership = await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - }); - - return ({ membership, workspace }); - } - - throw UnauthorizedRequestError({ - message: "Failed client authorization for workspace", - }); } diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index 38bfdfc2c..38bd54dd3 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -1,4 +1,5 @@ -export const AUTH_MODE_JWT = "jwt"; -export const AUTH_MODE_SERVICE_ACCOUNT = "serviceAccount"; -export const AUTH_MODE_SERVICE_TOKEN = "serviceToken"; -export const AUTH_MODE_API_KEY = "apiKey"; // TODO: deprecate \ No newline at end of file +export enum AuthMode { + JWT = "jwt", + SERVICE_TOKEN = "serviceToken", + API_KEY = "apiKey" +} \ No newline at end of file diff --git a/frontend/src/ee/components/ActivitySideBar.tsx b/frontend/src/ee/components/ActivitySideBar.tsx index 274704f06..67d673c3d 100644 --- a/frontend/src/ee/components/ActivitySideBar.tsx +++ b/frontend/src/ee/components/ActivitySideBar.tsx @@ -1,3 +1,4 @@ +// TODO: deprecate in favor of new audit logs import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Image from "next/image"; diff --git a/frontend/src/ee/components/ActivityTable.tsx b/frontend/src/ee/components/ActivityTable.tsx index b98f2dd7e..63640e15a 100644 --- a/frontend/src/ee/components/ActivityTable.tsx +++ b/frontend/src/ee/components/ActivityTable.tsx @@ -1,3 +1,5 @@ +// TODO: deprecate in favor of new audit logs + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx new file mode 100644 index 000000000..f54b20b1a --- /dev/null +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -0,0 +1,16 @@ +import { EventType, UserAgentType } from "./enums"; + +export const eventToNameMap: { [K in EventType]: string } = { + [EventType.GET_SECRETS]: "Get Secrets", + [EventType.GET_SECRET]: "Get Secret", + [EventType.CREATE_SECRET]: "Create Secret", + [EventType.UPDATE_SECRET]: "Update Secret", + [EventType.DELETE_SECRET]: "Delete Secret", +}; + +export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { + [UserAgentType.WEB]: "Web", + [UserAgentType.CLI]: "CLI", + [UserAgentType.K8_OPERATOR]: "K8s operator", + [UserAgentType.OTHER]: "Other", +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx new file mode 100644 index 000000000..4c75f6395 --- /dev/null +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -0,0 +1,19 @@ +export enum ActorType { + USER = "user", + SERVICE = "service" +} + +export enum UserAgentType { + WEB = "web", + CLI = "cli", + K8_OPERATOR = "k8-operator", + OTHER = "other" +} + +export enum EventType { + GET_SECRETS = "get-secrets", + GET_SECRET = "get-secret", + CREATE_SECRET = "create-secret", + UPDATE_SECRET = "update-secret", + DELETE_SECRET = "delete-secret" +} \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/index.tsx b/frontend/src/hooks/api/auditLogs/index.tsx new file mode 100644 index 000000000..fb026aebb --- /dev/null +++ b/frontend/src/hooks/api/auditLogs/index.tsx @@ -0,0 +1 @@ +export * from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx new file mode 100644 index 000000000..606794876 --- /dev/null +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -0,0 +1,53 @@ +import { useQuery } from "@tanstack/react-query"; +import { apiRequest } from "@app/config/request"; +import { + AuditLog, + Actor +} from "./types"; +import { EventType, UserAgentType } from "./enums"; + +export const workspaceKeys = { + getAuditLogs: (workspaceId: string, filters: { + eventType?: EventType; + userAgentType?: UserAgentType; + actor?: string; + }) => [{ workspaceId, filters }, "audit-logs"] as const, + getAuditLogActorFilterOpts: (workspaceId: string) => [{ workspaceId }, "audit-log-actor-filters"] as const +} + +export const useGetAuditLogs = (workspaceId: string, filters: { + eventType?: EventType; + userAgentType?: UserAgentType; + actor?: string; +}) => { + return useQuery({ + queryKey: workspaceKeys.getAuditLogs(workspaceId, filters), + queryFn: async () => { + const params = new URLSearchParams(); + if (filters.eventType) { + params.append("eventType", filters.eventType); + } + + if (filters.userAgentType) { + params.append("userAgentType", filters.userAgentType); + } + + if (filters.actor) { + params.append("actor", filters.actor); + } + + const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params }); + return data.auditLogs; + } + }); +} + +export const useGetAuditLogActorFilterOpts = (workspaceId: string) => { + return useQuery({ + queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId), + queryFn: async () => { + const { data } = await apiRequest.get<{ actors: Actor[] }>(`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`); + return data.actors; + } + }); +} \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx new file mode 100644 index 000000000..e836521ec --- /dev/null +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -0,0 +1,103 @@ +import { + ActorType, + EventType, + UserAgentType +} from "./enums"; + +interface UserActorMetadata { + userId: string; + email: string; +} + +interface ServiceActorMetadata { + serviceId: string; + name: string; +} + + +interface UserActor { + type: ActorType.USER; + metadata: UserActorMetadata; +} + +export interface ServiceActor { + type: ActorType.SERVICE; + metadata: ServiceActorMetadata; +} + +export type Actor = + | UserActor + | ServiceActor; + +interface GetSecretsEvent { + type: EventType.GET_SECRETS; + metadata: { + environment: string; + secretPath: string; + numberOfSecrets: number; + }; +} + +interface GetSecretEvent { + type: EventType.GET_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + }; +} + +interface CreateSecretEvent { + type: EventType.CREATE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + } +} + +interface UpdateSecretEvent { + type: EventType.UPDATE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + } +} + +interface DeleteSecretEvent { + type: EventType.DELETE_SECRET; + metadata: { + environment: string; + secretPath: string; + secretId: string; + secretKey: string; + secretVersion: number; + } +} + +export type Event = + | GetSecretsEvent + | GetSecretEvent + | CreateSecretEvent + | UpdateSecretEvent + | DeleteSecretEvent; + +export type AuditLog = { + _id: string; + actor: Actor; + organization: string; + workspace: string; + ipAddress: string; + event: Event; + userAgent: string; + userAgentType: UserAgentType; + createdAt: string; + updatedAt: string; +} diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 672e51052..c5839c9bf 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -15,6 +15,7 @@ export * from "./ssoConfig"; export * from "./subscriptions"; export * from "./tags"; export * from "./trustedIps"; +export * from "./auditLogs"; export * from "./users"; export * from "./webhooks"; export * from "./workspace"; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 433092db9..3fcdbb339 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -3,6 +3,7 @@ export type SubscriptionPlan = { membersUsed: number; memberLimit: number; auditLogs: boolean; + auditLogsRetentionDays: number; customAlerts: boolean; customRateLimits: boolean; pitRecovery: boolean; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 0e3233452..9833c06fd 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -28,7 +28,8 @@ export const workspaceKeys = { getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], getAllUserWorkspace: ["workspaces"] as const, - getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const + getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, + getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -259,3 +260,4 @@ export const useDeleteWsEnvironment = () => { } }); }; + diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 3f58f39b9..82a89a9ba 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -53,4 +53,4 @@ export type UpdateEnvironmentDTO = { environmentName: string; }; -export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string }; +export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string }; \ No newline at end of file diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index fea50dd8a..1df7517d4 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -483,6 +483,18 @@ export const AppLayout = ({ children }: LayoutProps) => { + + + + Audit Logs V2 + + + { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: t("billing.title") })} + + + + +
+ ); +} + +export default Logs; + +Logs.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/LogsPage.tsx b/frontend/src/views/Project/LogsPage/LogsPage.tsx new file mode 100644 index 000000000..c17fd7092 --- /dev/null +++ b/frontend/src/views/Project/LogsPage/LogsPage.tsx @@ -0,0 +1,17 @@ +import { + LogsSection +} from "./components"; + +export const LogsPage = () => { + return ( +
+
+
+

Audit Logs

+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx b/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx new file mode 100644 index 000000000..e86c1ae15 --- /dev/null +++ b/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx @@ -0,0 +1,147 @@ +import { Control, Controller, UseFormReset } from "react-hook-form"; +import { + FormControl, + Select, + SelectItem, + Button +} from "@app/components/v2"; +import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; +import { useWorkspace } from "@app/context"; +import { useGetAuditLogActorFilterOpts } from "@app/hooks/api"; +import { Actor } from "~/hooks/api/auditLogs/types"; +import { ActorType } from "~/hooks/api/auditLogs/enums"; +import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AuditLogFilterFormData } from "./LogsSection"; + +const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); +const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ label, value })); + +type Props = { + control: Control; + reset: UseFormReset; +} + +export const LogsFilter = ({ + control, + reset +}: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? ""); + + const renderActorSelectItem = (actor: Actor) => { + switch (actor.type) { + case ActorType.USER: + return ( + + {actor.metadata.email} + + ); + case ActorType.SERVICE: + return ( + + {actor.metadata.name} + + ); + } + } + + return ( +
+
+
+ ( + + + + )} + /> +
+ {!isLoading && data && data.length > 0 && ( +
+ ( + + + + )} + /> +
+ )} +
+ ( + + + + )} + /> +
+
+
+ +
+
+ ); +} diff --git a/frontend/src/views/Project/LogsPage/components/LogsSection.tsx b/frontend/src/views/Project/LogsPage/components/LogsSection.tsx new file mode 100644 index 000000000..e9db22c53 --- /dev/null +++ b/frontend/src/views/Project/LogsPage/components/LogsSection.tsx @@ -0,0 +1,49 @@ +import { useForm } from "react-hook-form"; +import { LogsFilter } from "./LogsFilter"; +import { LogsTable } from "./LogsTable"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; + +const schema = yup.object({ + eventType: yup.string() + .oneOf(Object.values(EventType), 'Invalid event type'), + actor: yup.string(), + userAgentType: yup.string() + .oneOf(Object.values(UserAgentType), 'Invalid user agent type'), +}).required(); + +export type AuditLogFilterFormData = yup.InferType; + +export const LogsSection = () => { + const { + control, + reset, + watch, + } = useForm({ + resolver: yupResolver(schema) + }); + + const eventType = watch("eventType") as EventType | undefined; + const userAgentType = watch("userAgentType") as UserAgentType | undefined; + const actor = watch("actor") as string | undefined; + + return ( +
+
+

+ Audit Logs +

+
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx new file mode 100644 index 000000000..8b0f769c3 --- /dev/null +++ b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx @@ -0,0 +1,70 @@ +import { faFile } from "@fortawesome/free-solid-svg-icons"; +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetAuditLogs } from "@app/hooks/api"; +import { LogsTableRow } from "./LogsTableRow"; +import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; + +type Props = { + eventType: EventType | undefined; + userAgentType: UserAgentType | undefined; + actor: string | undefined; +} + +export const LogsTable = ({ + eventType, + userAgentType, + actor +}: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", { + eventType, + userAgentType, + actor + }); + + return ( + + + + + + + + + + + + + {!isLoading && data && data.map((auditLog) => ( + + ))} + {isLoading && } + {!isLoading && data && data.length === 0 && ( + + + + )} + +
TimestampEventActorSourceMetadata
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx new file mode 100644 index 000000000..152fbd12f --- /dev/null +++ b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx @@ -0,0 +1,114 @@ +import { AuditLog, Actor, Event } from "~/hooks/api/auditLogs/types"; +import { ActorType, EventType } from "~/hooks/api/auditLogs/enums"; +import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; +import { + Td, + Tr +} from "@app/components/v2"; + +type Props = { + auditLog: AuditLog +} + +export const LogsTableRow = ({ + auditLog +}: Props) => { + const renderActor = (actor: Actor) => { + switch (actor.type) { + case ActorType.USER: + return ( + +

{actor.metadata.email}

+

User

+ + ); + case ActorType.SERVICE: + return ( + +

{`${actor.metadata.name}`}

+

Service token

+ + ); + } + } + + const renderMetadata = (event: Event) => { + switch (event.type) { + case EventType.GET_SECRETS: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+

{`# Secrets: ${event.metadata.numberOfSecrets}`}

+ + ); + case EventType.GET_SECRET: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+

{`Secret: ${event.metadata.secretKey}`}

+ + ); + case EventType.CREATE_SECRET: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+

{`Secret: ${event.metadata.secretKey}`}

+ + ); + case EventType.UPDATE_SECRET: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+

{`Secret: ${event.metadata.secretKey}`}

+ + ); + case EventType.DELETE_SECRET: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+

{`Secret: ${event.metadata.secretKey}`}

+ + ); + default: + return ( + Test + ); + } + } + + const formatDate = (dateToFormat: string) => { + const date = new Date(dateToFormat); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + let hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + // convert from 24h to 12h format + const period = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + + const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`; + return formattedDate; + } + + return ( + + {formatDate(auditLog.createdAt)} + {`${eventToNameMap[auditLog.event.type]}`} + {renderActor(auditLog.actor)} + +

{userAgentTTypeoNameMap[auditLog.userAgentType]}

+

{auditLog.ipAddress}

+ + {renderMetadata(auditLog.event)} + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/index.tsx b/frontend/src/views/Project/LogsPage/components/index.tsx new file mode 100644 index 000000000..009b8fecd --- /dev/null +++ b/frontend/src/views/Project/LogsPage/components/index.tsx @@ -0,0 +1 @@ +export { LogsSection } from "./LogsSection"; \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/index.tsx b/frontend/src/views/Project/LogsPage/index.tsx new file mode 100644 index 000000000..9e25d1a16 --- /dev/null +++ b/frontend/src/views/Project/LogsPage/index.tsx @@ -0,0 +1 @@ +export { LogsPage } from "./LogsPage"; \ No newline at end of file From 373dfff8e062fcbd182f93f79d8d3625f1a3230e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 5 Aug 2023 17:06:49 +0700 Subject: [PATCH 02/64] Remove print statement --- backend/src/middleware/requireWorkspaceAuth.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index e5e1e447d..f6f7405a9 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -32,8 +32,6 @@ const requireWorkspaceAuth = ({ const workspaceId = req[locationWorkspaceId]?.workspaceId; const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; - console.log("workspaceId: ", workspaceId); - // validate clients const { membership, workspace } = await validateClientForWorkspace({ authData: req.authData, From 5604232aeaef62f0b65cb0824f11219e447785c4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 19:09:38 +0800 Subject: [PATCH 03/64] added user controller and modified auth method page --- backend/src/controllers/v2/usersController.ts | 37 ++++++++ backend/src/models/user.ts | 1 + backend/src/routes/v2/users.ts | 18 ++++ frontend/src/components/v2/Select/Select.tsx | 1 + frontend/src/hooks/api/users/index.tsx | 3 +- frontend/src/hooks/api/users/queries.tsx | 22 +++++ frontend/src/hooks/api/users/types.ts | 1 + .../AuthMethodSection/AuthMethodSection.tsx | 87 +++++++++---------- 8 files changed, 123 insertions(+), 47 deletions(-) diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 66a48b550..48acbf490 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -148,6 +148,43 @@ export const updateAuthProvider = async (req: Request, res: Response) => { }); } +/** + * Update auth provider of the current user to [authProvider] + * @param req + * @param res + * @returns + */ + export const updateAuthProviders = async (req: Request, res: Response) => { + const { + authProviders + } = req.body; + + if ( + req.user?.authProvider === AuthProvider.OKTA_SAML + || req.user?.authProvider === AuthProvider.AZURE_SAML + || req.user?.authProvider === AuthProvider.JUMPCLOUD_SAML + ) { + return res.status(400).send({ + message: "Failed to update user authentication method because SAML SSO is enforced" + }); + } + + const user = await User.findByIdAndUpdate( + req.user._id.toString(), + { + authProviders + }, + { + new: true + } + ); + + return res.status(200).send({ + user + }); +} + + /** * Return organizations that the current user is part of. * @param req diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index fb14e8b8a..77b8f522f 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -13,6 +13,7 @@ export interface IUser extends Document { _id: Types.ObjectId; authId?: string; authProvider?: AuthProvider; + authProviders?: AuthProvider[]; email: string; firstName?: string; lastName?: string; diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index 334ef523b..6042e6e3f 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -57,6 +57,24 @@ router.patch( usersController.updateAuthProvider ); +router.put( + "/me/auth-providers", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + }), + body("authProviders").exists().isArray({ + min: 1, + }).custom((authProviders: AuthProvider[]) => { + return authProviders.every(provider => [ + AuthProvider.EMAIL, + AuthProvider.GOOGLE, + AuthProvider.GITHUB + ].includes(provider)) + }), + validateRequest, + usersController.updateAuthProviders, +); + router.get( "/me/organizations", requireAuth({ diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index cdf790634..ccac29a2a 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -16,6 +16,7 @@ type Props = { position?: "item-aligned" | "popper"; isDisabled?: boolean; icon?: IconProp; + isMulti?: boolean; }; export type SelectProps = Omit & Props; diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index a209367e2..8aec39c4e 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -15,5 +15,6 @@ export { useRegisterUserAction, useRevokeMySessions, useUpdateOrgUserRole, - useUpdateUserAuthProvider + useUpdateUserAuthProvider, + useUpdateUserAuthProviders, } from "./queries"; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 93d8d41de..0def469e9 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -80,6 +80,28 @@ export const useUpdateUserAuthProvider = () => { }); }; + +export const useUpdateUserAuthProviders = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + authProviders + }: { + authProviders: string[]; + }) => { + const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-providers", { + authProviders + }); + + return user; + }, + onSuccess: () => { + queryClient.invalidateQueries(userKeys.getUser); + } + }); +}; + export const useGetUserAction = (action: string) => useQuery({ queryKey: userKeys.userAction, diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 0c312b2b6..51bfa3452 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -13,6 +13,7 @@ export type User = { firstName?: string; lastName?: string; authProvider?: AuthProvider; + authProviders?: AuthProvider[]; encryptionVersion?: number; protectedKey?: string; protectedKeyIV?: string; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx index b8246180c..6d874de0e 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx @@ -1,17 +1,16 @@ import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, - FormControl, - Select, - SelectItem} from "@app/components/v2"; + Checkbox +} from "@app/components/v2"; import { useUser } from "@app/context"; import { - useUpdateUserAuthProvider + useUpdateUserAuthProviders } from "@app/hooks/api"; const authMethods = [ @@ -24,7 +23,7 @@ const authMethods = [ ]; const schema = yup.object({ - authMethod: yup.string().required("Auth method is required") + authMethods: yup.array().required("Auth method is required") }); export type FormData = yup.InferType; @@ -32,35 +31,38 @@ export type FormData = yup.InferType; export const AuthMethodSection = () => { const { createNotification } = useNotificationContext(); const { user } = useUser(); - const { mutateAsync, isLoading } = useUpdateUserAuthProvider(); + const { mutateAsync, isLoading } = useUpdateUserAuthProviders(); const { reset, - control, - handleSubmit + handleSubmit, + setValue, + watch, } = useForm({ defaultValues: { - authMethod: user?.authProvider ?? "email" + authMethods: [user?.authProvider ?? "email"] }, resolver: yupResolver(schema) }); + const selectedAuthMethods = watch("authMethods"); + useEffect(() => { if (user) { reset({ - authMethod: user?.authProvider ?? "email" + authMethods: [user?.authProvider ?? "email"] }); } }, [user]); const onFormSubmit = async ({ - authMethod + authMethods }: FormData) => { try { if ( - authMethod === "okta-saml" - || authMethod === "azure-saml" - || authMethod === "jumpcloud-saml" + authMethods.includes("okta-saml") + || authMethods.includes("azure-saml") + || authMethods.includes("jumpcloud-saml") ) { createNotification({ text: "SAML authentication can only be configured in your organization settings", @@ -71,7 +73,7 @@ export const AuthMethodSection = () => { } await mutateAsync({ - authProvider: authMethod + authProviders: authMethods }); createNotification({ @@ -96,36 +98,29 @@ export const AuthMethodSection = () => { Authentication Method
- ( - - - - )} - /> + { + authMethods.map(authMethod => ( + { + if (checked) { + setValue("authMethods", [ + ...selectedAuthMethods, + authMethod.value + ]) + } else { + setValue("authMethods", selectedAuthMethods.filter(auth => auth !== authMethod.value)) + } + }}> + {authMethod.label} + + )) + }
+ ); -} \ No newline at end of file +} From 04fdccc45d7c8a49dfc6f82a7de1780c678ff538 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 19:45:27 +0800 Subject: [PATCH 04/64] modified backend controllers to support new auth providers --- backend/src/controllers/v3/authController.ts | 10 ++++++++-- backend/src/helpers/auth.ts | 7 +++++-- backend/src/models/user.ts | 3 +++ backend/src/utils/auth.ts | 3 +++ .../AuthMethodSection/AuthMethodSection.tsx | 8 ++++++-- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 08ef7fa8d..69aa8874e 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -56,7 +56,10 @@ export const login1 = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to find user"); - if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) { + const shouldValidateProviderAuth = (user.authProvider && user.authProvider !== AuthProvider.EMAIL) + || (user.authProviders && !user.authProviders?.includes(AuthProvider.EMAIL)) + + if (shouldValidateProviderAuth) { await validateProviderAuthToken({ email, user, @@ -116,7 +119,10 @@ export const login2 = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to find user"); - if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) { + const shouldValidateProviderAuth = (user.authProvider && user.authProvider !== AuthProvider.EMAIL) + || (user.authProviders && !user.authProviders?.includes(AuthProvider.EMAIL)) + + if (shouldValidateProviderAuth) { await validateProviderAuthToken({ email, user, diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index eb6eeb4d2..f6b676a36 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -390,10 +390,13 @@ export const validateProviderAuthToken = async ({ jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) ); + const doesProviderMatch = (user.authProvider && user.authProvider === decodedToken.authProvider) + || (user.authProviders && user.authProviders.includes(decodedToken.authProvider)); + if ( - decodedToken.authProvider !== user.authProvider || + !doesProviderMatch || decodedToken.email !== email ) { throw new Error("Invalid authentication credentials.") } -} \ No newline at end of file +} diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 77b8f522f..9251c487d 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -44,6 +44,9 @@ const userSchema = new Schema( type: String, enum: AuthProvider, }, + authProviders: [{ + type: String, + }], email: { type: String, required: true, diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 8faba80bf..3fdd5915f 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -119,6 +119,7 @@ const initializePassport = async () => { firstName: user.firstName, lastName: user.lastName, authProvider: user.authProvider, + authProviders: user.authProviders, isUserCompleted, ...(req.query.state ? { callbackPort: req.query.state as string @@ -173,6 +174,7 @@ const initializePassport = async () => { firstName: user.firstName, lastName: user.lastName, authProvider: user.authProvider, + authProviders: user.authProviders, isUserCompleted, ...(req.query.state ? { callbackPort: req.query.state as string @@ -302,6 +304,7 @@ const initializePassport = async () => { lastName, organizationName: organization?.name, authProvider: user.authProvider, + authProviders: user.authProviders, isUserCompleted, ...(req.body.RelayState ? { callbackPort: req.body.RelayState as string diff --git a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx index 6d874de0e..6e3b006e3 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx @@ -33,6 +33,10 @@ export const AuthMethodSection = () => { const { user } = useUser(); const { mutateAsync, isLoading } = useUpdateUserAuthProviders(); + const defaultAuthMethods = user.authProviders?.length ? + user.authProviders : + [user?.authProvider ?? "email"]; + const { reset, handleSubmit, @@ -40,7 +44,7 @@ export const AuthMethodSection = () => { watch, } = useForm({ defaultValues: { - authMethods: [user?.authProvider ?? "email"] + authMethods: defaultAuthMethods, }, resolver: yupResolver(schema) }); @@ -50,7 +54,7 @@ export const AuthMethodSection = () => { useEffect(() => { if (user) { reset({ - authMethods: [user?.authProvider ?? "email"] + authMethods: defaultAuthMethods, }); } }, [user]); From 3a9bf5409be87fd81c0e8305909537a97de89bf2 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 21:46:20 +0800 Subject: [PATCH 05/64] finalization of create token logic --- backend/src/controllers/v3/authController.ts | 14 +++---- backend/src/helpers/auth.ts | 5 +-- backend/src/models/user.ts | 1 + backend/src/utils/auth.ts | 39 ++++++++++---------- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 69aa8874e..831cb81b7 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -56,10 +56,9 @@ export const login1 = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to find user"); - const shouldValidateProviderAuth = (user.authProvider && user.authProvider !== AuthProvider.EMAIL) - || (user.authProviders && !user.authProviders?.includes(AuthProvider.EMAIL)) - - if (shouldValidateProviderAuth) { + let authProviders = [...(user.authProviders || []), user.authProvider]; + + if (!authProviders.includes(AuthProvider.EMAIL)) { await validateProviderAuthToken({ email, user, @@ -119,10 +118,9 @@ export const login2 = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to find user"); - const shouldValidateProviderAuth = (user.authProvider && user.authProvider !== AuthProvider.EMAIL) - || (user.authProviders && !user.authProviders?.includes(AuthProvider.EMAIL)) - - if (shouldValidateProviderAuth) { + let authProviders = [...(user.authProviders || []), user.authProvider]; + + if (!authProviders.includes(AuthProvider.EMAIL)) { await validateProviderAuthToken({ email, user, diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index f6b676a36..5e5101ce7 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -390,11 +390,10 @@ export const validateProviderAuthToken = async ({ jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) ); - const doesProviderMatch = (user.authProvider && user.authProvider === decodedToken.authProvider) - || (user.authProviders && user.authProviders.includes(decodedToken.authProvider)); + let authProviders = [...(user.authProviders || []), user.authProvider]; if ( - !doesProviderMatch || + !authProviders.includes(decodedToken.authProvider) || decodedToken.email !== email ) { throw new Error("Invalid authentication credentials.") diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 9251c487d..852214d52 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -46,6 +46,7 @@ const userSchema = new Schema( }, authProviders: [{ type: String, + enum: AuthProvider, }], email: { type: String, diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 3fdd5915f..b58a135c9 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -97,20 +97,22 @@ const initializePassport = async () => { email }).select("+publicKey"); - if (user && user.authProvider !== AuthProvider.GOOGLE) { - done(InternalServerError()); - } - if (!user) { user = await new User({ email, - authProvider: AuthProvider.GOOGLE, + authProviders: [AuthProvider.GOOGLE], authId: profile.id, firstName: profile.name.givenName, lastName: profile.name.familyName }).save(); } + let authProviders = [...(user.authProviders || []), user.authProvider]; + + if (!authProviders.includes(AuthProvider.GOOGLE)) { + done(InternalServerError()); + } + const isUserCompleted = !!user.publicKey; const providerAuthToken = createToken({ payload: { @@ -118,8 +120,7 @@ const initializePassport = async () => { email: user.email, firstName: user.firstName, lastName: user.lastName, - authProvider: user.authProvider, - authProviders: user.authProviders, + authProvider: AuthProvider.GOOGLE, isUserCompleted, ...(req.query.state ? { callbackPort: req.query.state as string @@ -151,21 +152,23 @@ const initializePassport = async () => { let user = await User.findOne({ email }).select("+publicKey"); - - if (user && user.authProvider !== AuthProvider.GITHUB) { - done(InternalServerError()); - } - + if (!user) { user = await new User({ email: email, - authProvider: AuthProvider.GITHUB, + authProviders: [AuthProvider.GITHUB], authId: profile.id, firstName: profile.displayName, lastName: "" }).save(); } + let authProviders = [...(user.authProviders || []), user.authProvider]; + + if (!authProviders.includes(AuthProvider.GITHUB)) { + done(InternalServerError()); + } + const isUserCompleted = !!user.publicKey; const providerAuthToken = createToken({ payload: { @@ -173,8 +176,7 @@ const initializePassport = async () => { email: user.email, firstName: user.firstName, lastName: user.lastName, - authProvider: user.authProvider, - authProviders: user.authProviders, + authProvider: AuthProvider.GITHUB, isUserCompleted, ...(req.query.state ? { callbackPort: req.query.state as string @@ -249,7 +251,7 @@ const initializePassport = async () => { await User.findByIdAndUpdate( user._id, { - authProvider: req.ssoConfig.authProvider + authProviders: [req.ssoConfig.authProvider] }, { new: true @@ -281,7 +283,7 @@ const initializePassport = async () => { } else { user = await new User({ email, - authProvider: req.ssoConfig.authProvider, + authProviders: [req.ssoConfig.authProvider], firstName, lastName }).save(); @@ -303,8 +305,7 @@ const initializePassport = async () => { firstName, lastName, organizationName: organization?.name, - authProvider: user.authProvider, - authProviders: user.authProviders, + authProvider: req.ssoConfig.authProvider, isUserCompleted, ...(req.body.RelayState ? { callbackPort: req.body.RelayState as string From 0a140f5333042c278e60b5ac292e76746400fe33 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 22:04:39 +0800 Subject: [PATCH 06/64] updated implementation of user update after sso change --- backend/src/ee/controllers/v1/ssoController.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/controllers/v1/ssoController.ts b/backend/src/ee/controllers/v1/ssoController.ts index 42b4d12be..a310927b9 100644 --- a/backend/src/ee/controllers/v1/ssoController.ts +++ b/backend/src/ee/controllers/v1/ssoController.ts @@ -3,6 +3,7 @@ import { Types } from "mongoose"; import { BotOrgService } from "../../../services"; import { SSOConfig } from "../../models"; import { + AuthProvider, MembershipOrg, User } from "../../../models"; @@ -156,7 +157,10 @@ export const updateSSOConfig = async (req: Request, res: Response) => { } }, { - authProvider: ssoConfig.authProvider + authProviders: [ssoConfig.authProvider], + $unset: { + authProvider: 1 + } } ); } else { @@ -167,8 +171,9 @@ export const updateSSOConfig = async (req: Request, res: Response) => { } }, { + authProviders: [AuthProvider.EMAIL], $unset: { - authProvider: 1 + authProvider: 1, } } ); From bde30049bc599c1807f3d038ab3051c2e85815ba Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 22:29:31 +0800 Subject: [PATCH 07/64] ensured backwards compatibility --- backend/src/controllers/v3/authController.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 831cb81b7..0317cb09d 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -56,9 +56,10 @@ export const login1 = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to find user"); - let authProviders = [...(user.authProviders || []), user.authProvider]; + const authProviders = [...(user.authProviders || [])]; + user.authProvider && authProviders.push(user.authProvider); - if (!authProviders.includes(AuthProvider.EMAIL)) { + if (authProviders.length && !authProviders.includes(AuthProvider.EMAIL)) { await validateProviderAuthToken({ email, user, @@ -118,9 +119,10 @@ export const login2 = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to find user"); - let authProviders = [...(user.authProviders || []), user.authProvider]; + const authProviders = [...(user.authProviders || [])]; + user.authProvider && authProviders.push(user.authProvider); - if (!authProviders.includes(AuthProvider.EMAIL)) { + if (authProviders.length && !authProviders.includes(AuthProvider.EMAIL)) { await validateProviderAuthToken({ email, user, From 681255187f76424835cc8c35b3c3cda4882805ba Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 22:30:02 +0800 Subject: [PATCH 08/64] modified initialize org to check for auth providers --- backend/src/controllers/v3/signupController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index b33a7fe0c..06fac4a85 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -117,7 +117,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null - if (user.authProvider !== AuthProvider.OKTA_SAML) { + if (!user.authProviders?.includes(AuthProvider.OKTA_SAML)) { // initialize default organization and workspace await initializeDefaultOrg({ organizationName, From b4dbdbabac8e6782c0bc4023413d8059ac3a4a9f Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 22:33:53 +0800 Subject: [PATCH 09/64] used const --- backend/src/utils/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index b58a135c9..7942de628 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -107,7 +107,7 @@ const initializePassport = async () => { }).save(); } - let authProviders = [...(user.authProviders || []), user.authProvider]; + const authProviders = [...(user.authProviders || []), user.authProvider]; if (!authProviders.includes(AuthProvider.GOOGLE)) { done(InternalServerError()); @@ -163,7 +163,7 @@ const initializePassport = async () => { }).save(); } - let authProviders = [...(user.authProviders || []), user.authProvider]; + const authProviders = [...(user.authProviders || []), user.authProvider]; if (!authProviders.includes(AuthProvider.GITHUB)) { done(InternalServerError()); From dc3f2c78c1be86871f80862677e7fab64b9979e3 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 6 Aug 2023 22:38:24 +0800 Subject: [PATCH 10/64] resolved lint issue --- .../AuthMethodSection/AuthMethodSection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx index 6e3b006e3..5458525ce 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx @@ -13,7 +13,7 @@ import { useUpdateUserAuthProviders } from "@app/hooks/api"; -const authMethods = [ +const authMethodList = [ { label: "Email", value: "email" }, { label: "Google SSO", value: "google" }, { label: "GitHub SSO", value: "github" }, @@ -103,7 +103,7 @@ export const AuthMethodSection = () => {
{ - authMethods.map(authMethod => ( + authMethodList.map(authMethod => ( Date: Mon, 7 Aug 2023 11:25:06 +0700 Subject: [PATCH 11/64] Run linter --- .../ee/controllers/v1/workspaceController.ts | 21 +++++++----- backend/src/ee/routes/v1/organizations.ts | 2 +- backend/src/ee/routes/v1/secret.ts | 4 +-- backend/src/ee/routes/v1/secretSnapshot.ts | 2 +- backend/src/ee/routes/v1/sso.ts | 4 +-- backend/src/ee/routes/v1/workspace.ts | 6 ++-- backend/src/ee/services/EEAuditLogService.ts | 4 +-- backend/src/helpers/auth.ts | 4 +-- backend/src/helpers/secrets.ts | 4 +-- backend/src/interfaces/middleware/index.ts | 2 +- backend/src/routes/v1/bot.ts | 2 +- backend/src/routes/v1/integration.ts | 4 +-- backend/src/routes/v1/integrationAuth.ts | 4 +-- backend/src/routes/v1/key.ts | 2 +- backend/src/routes/v1/organization.ts | 4 +-- backend/src/routes/v1/secret.ts | 4 +-- backend/src/routes/v1/secretImport.ts | 2 +- backend/src/routes/v1/secretsFolder.ts | 2 +- backend/src/routes/v1/serviceToken.ts | 4 +-- backend/src/routes/v1/webhook.ts | 2 +- backend/src/routes/v1/workspace.ts | 4 +-- backend/src/routes/v2/environment.ts | 4 +-- backend/src/routes/v2/organizations.ts | 4 +-- backend/src/routes/v2/secret.ts | 2 +- backend/src/routes/v2/secrets.ts | 2 +- backend/src/routes/v2/serviceTokenData.ts | 2 +- backend/src/routes/v2/tags.ts | 4 +-- backend/src/routes/v2/workspace.ts | 4 +-- backend/src/validation/membership.ts | 7 ---- backend/src/validation/organization.ts | 3 +- backend/src/validation/secrets.ts | 4 +-- backend/src/validation/workspace.ts | 7 ++-- frontend/src/hooks/api/auditLogs/queries.tsx | 9 ++--- frontend/src/hooks/api/index.tsx | 2 +- .../src/pages/integrations/checkly/create.tsx | 1 + .../LogsPage/components/LogsFilter.tsx | 33 ++++++++++++------- .../LogsPage/components/LogsSection.tsx | 20 ++++------- .../Project/LogsPage/components/LogsTable.tsx | 5 ++- .../LogsPage/components/LogsTableRow.tsx | 23 ++++++++----- .../Project/LogsPage/components/types.tsx | 13 ++++++++ 40 files changed, 130 insertions(+), 106 deletions(-) create mode 100644 frontend/src/views/Project/LogsPage/components/types.tsx diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index c865dc8eb..a77cdca71 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,20 +1,19 @@ import { Request, Response } from "express"; import { PipelineStage, Types } from "mongoose"; -import { Secret, Membership, User, ServiceTokenData } from "../../../models"; +import { Membership, Secret, ServiceTokenData, User } from "../../../models"; import { + ActorType, + AuditLog, FolderVersion, IPType, ISecretVersion, Log, SecretSnapshot, SecretVersion, + ServiceActor, TFolderRootVersionSchema, TrustedIP, - AuditLog, - Actor, - ActorType, - UserActor, - ServiceActor + UserActor } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; @@ -599,7 +598,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { }; /** - * Return trusted ips for workspace with id [workspaceId] + * Return audit logs for workspace with id [workspaceId] * @param req * @param res */ @@ -608,6 +607,8 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { const eventType = req.query.eventType; const userAgentType = req.query.userAgentType; const actor = req.query.actor as string | undefined; + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); const auditLogs = await AuditLog.find({ workspace: new Types.ObjectId(workspaceId), @@ -626,7 +627,9 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { }) } : {}) }) - .sort({ createdAt: -1 }); + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); return res.status(200).send({ auditLogs @@ -634,7 +637,7 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { } /** - * Return trusted ips for workspace with id [workspaceId] + * Return audit log actor filter options for workspace with id [workspaceId] * @param req * @param res */ diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index d41d232da..6506d3afa 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -8,7 +8,7 @@ import { import { body, param, query } from "express-validator"; import { organizationsController } from "../../controllers/v1"; import { - ACCEPTED, ADMIN, MEMBER, OWNER, AuthMode + ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables"; router.get( diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 376922e74..0eb23ee80 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -9,10 +9,10 @@ import { body, param, query } from "express-validator"; import { secretController } from "../../controllers/v1"; import { ADMIN, + AuthMode, MEMBER, PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, - AuthMode + PERMISSION_WRITE_SECRETS } from "../../../variables"; router.get( diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index c6201cc12..ecfe47ca5 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -8,7 +8,7 @@ import { validateRequest, } from "../../../middleware"; import { param } from "express-validator"; -import { ADMIN, MEMBER, AuthMode } from "../../../variables"; +import { ADMIN, AuthMode, MEMBER } from "../../../variables"; import { secretSnapshotController } from "../../controllers/v1"; router.get( diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index 22ecee9f0..005f84a0e 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -15,8 +15,8 @@ import { authLimiter } from "../../../helpers/rateLimiter"; import { ACCEPTED, ADMIN, - OWNER, - AuthMode + AuthMode, + OWNER } from "../../../variables"; router.get( diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 407f5d3f8..90eea76e6 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -8,8 +8,8 @@ import { import { body, param, query } from "express-validator"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../../variables"; import { workspaceController } from "../../controllers/v1"; import { EventType, UserAgentType } from "../../models"; @@ -97,6 +97,8 @@ router.get( query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), query("actor").isString().optional({ nullable: true }), + query("offset").isString().default("0"), + query("limit").isString().default("20"), validateRequest, workspaceController.getWorkspaceAuditLogs ); diff --git a/backend/src/ee/services/EEAuditLogService.ts b/backend/src/ee/services/EEAuditLogService.ts index 02ea99225..035742077 100644 --- a/backend/src/ee/services/EEAuditLogService.ts +++ b/backend/src/ee/services/EEAuditLogService.ts @@ -11,8 +11,8 @@ interface EventScope { } type ValidEventScope = - | Required> - | Required> + | Required> + | Required> | Required export default class EEAuditLogService { diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 50b68e62c..8d034dfbf 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -28,8 +28,8 @@ import { AuthMode } from "../variables"; import { - UserAuthData, - ServiceTokenAuthData + ServiceTokenAuthData, + UserAuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 0d5562b4e..880e7cee3 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -13,7 +13,7 @@ import { SecretBlindIndexData, ServiceTokenData, } from "../models"; -import { SecretVersion, EventType } from "../ee/models"; +import { EventType, SecretVersion } from "../ee/models"; import { BadRequestError, InternalServerError, @@ -40,7 +40,7 @@ import { } from "../utils/crypto"; import { TelemetryService } from "../services"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; -import { EELogService, EESecretService, EEAuditLogService } from "../ee/services"; +import { EEAuditLogService, EELogService, EESecretService } from "../ee/services"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index bb2435ecf..e8dd1b91b 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -4,8 +4,8 @@ import { IUser, } from "../../models"; import { - UserActor, ServiceActor, + UserActor, UserAgentType } from "../../ee/models"; diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index 536e0a3bd..e50d35625 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -8,7 +8,7 @@ import { validateRequest, } from "../../middleware"; import { botController } from "../../controllers/v1"; -import { ADMIN, MEMBER, AuthMode } from "../../variables"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; router.get( "/:workspaceId", diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 0bd1dfbc6..95ec4ec2d 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -8,8 +8,8 @@ import { } from "../../middleware"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; import { body, param } from "express-validator"; import { integrationController } from "../../controllers/v1"; diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index daf880c60..b8b7f348d 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -9,8 +9,8 @@ import { } from "../../middleware"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; import { integrationAuthController } from "../../controllers/v1"; diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index daa840ee7..a12c8c0bb 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -6,7 +6,7 @@ import { validateRequest, } from "../../middleware"; import { body, param } from "express-validator"; -import { ADMIN, MEMBER, AuthMode } from "../../variables"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; import { keyController } from "../../controllers/v1"; router.post( diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index fdad1f9fa..8f4a924e9 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -9,9 +9,9 @@ import { import { ACCEPTED, ADMIN, + AuthMode, MEMBER, - OWNER, - AuthMode + OWNER } from "../../variables"; import { organizationController } from "../../controllers/v1"; diff --git a/backend/src/routes/v1/secret.ts b/backend/src/routes/v1/secret.ts index 6cebd4db8..10668de34 100644 --- a/backend/src/routes/v1/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -10,8 +10,8 @@ import { body, param, query } from "express-validator"; import { secretController } from "../../controllers/v1"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; // note to devs: these endpoints will be deprecated in favor of v2 diff --git a/backend/src/routes/v1/secretImport.ts b/backend/src/routes/v1/secretImport.ts index 2e7e22bc3..79ee8e238 100644 --- a/backend/src/routes/v1/secretImport.ts +++ b/backend/src/routes/v1/secretImport.ts @@ -3,7 +3,7 @@ const router = express.Router(); import { body, param, query } from "express-validator"; import { secretImportController } from "../../controllers/v1"; import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { ADMIN, MEMBER, AuthMode } from "../../variables"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; router.post( "/", diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 6a89e4b97..f7773367b 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -12,7 +12,7 @@ import { getFolders, updateFolderById, } from "../../controllers/v1/secretsFolderController"; -import { ADMIN, MEMBER, AuthMode } from "../../variables"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; router.post( "/", diff --git a/backend/src/routes/v1/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts index a3974e217..aaf35e85f 100644 --- a/backend/src/routes/v1/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -9,8 +9,8 @@ import { import { body } from "express-validator"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; import { serviceTokenController } from "../../controllers/v1"; diff --git a/backend/src/routes/v1/webhook.ts b/backend/src/routes/v1/webhook.ts index 55c471ea3..16264d4ed 100644 --- a/backend/src/routes/v1/webhook.ts +++ b/backend/src/routes/v1/webhook.ts @@ -2,7 +2,7 @@ import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { body, param, query } from "express-validator"; -import { ADMIN, MEMBER, AuthMode } from "../../variables"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; import { webhookController } from "../../controllers/v1"; router.post( diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 2fd178f52..9a4554be6 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -8,8 +8,8 @@ import { } from "../../middleware"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; import { membershipController, workspaceController } from "../../controllers/v1"; diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index f383aa2fd..e9e7fcad3 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -9,8 +9,8 @@ import { } from "../../middleware"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; router.post( diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index 6196796cd..b305c1c0a 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -10,9 +10,9 @@ import { body, param } from "express-validator"; import { ACCEPTED, ADMIN, + AuthMode, MEMBER, - OWNER, - AuthMode + OWNER } from "../../variables"; import { organizationsController } from "../../controllers/v2"; diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 9b7526a99..d9fedda20 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -8,8 +8,8 @@ import { import { body, param, query } from "express-validator"; import { ADMIN, - MEMBER, AuthMode, + MEMBER, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, } from "../../variables"; diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index e347e11c3..52196e983 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -12,8 +12,8 @@ import { body, query } from "express-validator"; import { secretsController } from "../../controllers/v2"; import { ADMIN, - MEMBER, AuthMode, + MEMBER, PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, SECRET_PERSONAL, diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index aafd1cd47..efdc905fe 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -9,8 +9,8 @@ import { import { body, param } from "express-validator"; import { ADMIN, - MEMBER, AuthMode, + MEMBER, PERMISSION_WRITE_SECRETS } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index af82db6ac..7ccfd17cd 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -9,8 +9,8 @@ import { } from "../../middleware"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; router.get( diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index a93bad48e..77ed75eb1 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -9,8 +9,8 @@ import { } from "../../middleware"; import { ADMIN, - MEMBER, - AuthMode + AuthMode, + MEMBER } from "../../variables"; import { workspaceController } from "../../controllers/v2"; diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index d788f4461..6c40aa0aa 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -1,23 +1,16 @@ import { Types } from "mongoose"; import { - IServiceAccount, IServiceTokenData, IUser, Membership, - ServiceAccount, - ServiceTokenData, - User, } from "../models"; -import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { MembershipNotFoundError, - UnauthorizedRequestError, } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; -import { auth } from "../routes/v1"; /** * Validate authenticated clients for membership with id [membershipId] based diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index c4838d154..4ca9811c5 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -36,9 +36,10 @@ export const validateClientForOrganization = async ({ }); } + let membershipOrg; switch (authData.actor.type) { case ActorType.USER: - const membershipOrg = await validateUserClientForOrganization({ + membershipOrg = await validateUserClientForOrganization({ user: authData.authPayload as IUser, organization, acceptedRoles, diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index c51e83e23..6c0ac3084 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -1,9 +1,9 @@ import { Types } from "mongoose"; import { ISecret, - Secret, - IUser, IServiceTokenData, + IUser, + Secret, } from "../models"; import { validateUserClientForSecret, validateUserClientForSecrets } from "./user"; import { validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index 8c8f4408d..3be8d0dad 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -1,9 +1,9 @@ import net from "net"; import { Types } from "mongoose"; import { - SecretBlindIndexData, - IServiceTokenData, + IServiceTokenData, IUser, + SecretBlindIndexData, Workspace, } from "../models"; import { @@ -78,9 +78,10 @@ export const validateClientForWorkspace = async ({ }); } + let membership; switch (authData.actor.type) { case ActorType.USER: - const membership = await validateUserClientForWorkspace({ + membership = await validateUserClientForWorkspace({ user: authData.authPayload as IUser, workspaceId, environment, diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 606794876..05148c941 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -1,10 +1,11 @@ import { useQuery } from "@tanstack/react-query"; + import { apiRequest } from "@app/config/request"; -import { - AuditLog, - Actor -} from "./types"; + import { EventType, UserAgentType } from "./enums"; +import { + Actor, + AuditLog} from "./types"; export const workspaceKeys = { getAuditLogs: (workspaceId: string, filters: { diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index c5839c9bf..4e5cb66f4 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,3 +1,4 @@ +export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; export * from "./incidentContacts"; @@ -15,7 +16,6 @@ export * from "./ssoConfig"; export * from "./subscriptions"; export * from "./tags"; export * from "./trustedIps"; -export * from "./auditLogs"; export * from "./users"; export * from "./webhooks"; export * from "./workspace"; diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index de7dd9347..2ae0435cf 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -11,6 +11,7 @@ import { Select, SelectItem } from "@app/components/v2"; + import { useGetIntegrationAuthApps, useGetIntegrationAuthById diff --git a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx b/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx index e86c1ae15..d1273f3aa 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx @@ -1,18 +1,20 @@ import { Control, Controller, UseFormReset } from "react-hook-form"; -import { - FormControl, - Select, - SelectItem, - Button -} from "@app/components/v2"; -import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; -import { useWorkspace } from "@app/context"; -import { useGetAuditLogActorFilterOpts } from "@app/hooks/api"; -import { Actor } from "~/hooks/api/auditLogs/types"; -import { ActorType } from "~/hooks/api/auditLogs/enums"; import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AuditLogFilterFormData } from "./LogsSection"; + +import { + Button, + FormControl, + Select, + SelectItem} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetAuditLogActorFilterOpts } from "@app/hooks/api"; + +import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; +import { ActorType } from "~/hooks/api/auditLogs/enums"; +import { Actor } from "~/hooks/api/auditLogs/types"; + +import { AuditLogFilterFormData } from "./types"; const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ label, value })); @@ -43,7 +45,14 @@ export const LogsFilter = ({ {actor.metadata.name} ); + default: + return ( + + N/A + + ); } + } return ( diff --git a/frontend/src/views/Project/LogsPage/components/LogsSection.tsx b/frontend/src/views/Project/LogsPage/components/LogsSection.tsx index e9db22c53..5651d0ef0 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsSection.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsSection.tsx @@ -1,19 +1,11 @@ import { useForm } from "react-hook-form"; -import { LogsFilter } from "./LogsFilter"; -import { LogsTable } from "./LogsTable"; import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; + import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; -const schema = yup.object({ - eventType: yup.string() - .oneOf(Object.values(EventType), 'Invalid event type'), - actor: yup.string(), - userAgentType: yup.string() - .oneOf(Object.values(UserAgentType), 'Invalid user agent type'), -}).required(); - -export type AuditLogFilterFormData = yup.InferType; +import { LogsFilter } from "./LogsFilter"; +import { LogsTable } from "./LogsTable"; +import { AuditLogFilterFormData,auditLogFilterFormSchema } from "./types"; export const LogsSection = () => { const { @@ -21,7 +13,7 @@ export const LogsSection = () => { reset, watch, } = useForm({ - resolver: yupResolver(schema) + resolver: yupResolver(auditLogFilterFormSchema) }); const eventType = watch("eventType") as EventType | undefined; @@ -46,4 +38,4 @@ export const LogsSection = () => { />
); -} \ No newline at end of file + } \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx index 8b0f769c3..823e3f56d 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx @@ -1,4 +1,5 @@ import { faFile } from "@fortawesome/free-solid-svg-icons"; + import { EmptyState, Table, @@ -12,9 +13,11 @@ import { } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetAuditLogs } from "@app/hooks/api"; -import { LogsTableRow } from "./LogsTableRow"; + import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; +import { LogsTableRow } from "./LogsTableRow"; + type Props = { eventType: EventType | undefined; userAgentType: UserAgentType | undefined; diff --git a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx index 152fbd12f..05dfec9b0 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx @@ -1,11 +1,12 @@ -import { AuditLog, Actor, Event } from "~/hooks/api/auditLogs/types"; -import { ActorType, EventType } from "~/hooks/api/auditLogs/enums"; -import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; import { Td, Tr } from "@app/components/v2"; +import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; +import { ActorType, EventType } from "~/hooks/api/auditLogs/enums"; +import { Actor, AuditLog, Event } from "~/hooks/api/auditLogs/types"; + type Props = { auditLog: AuditLog } @@ -29,6 +30,10 @@ export const LogsTableRow = ({

Service token

); + default: + return ( + + ); } } @@ -84,16 +89,16 @@ export const LogsTableRow = ({ const formatDate = (dateToFormat: string) => { const date = new Date(dateToFormat); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); let hours = date.getHours(); - const minutes = String(date.getMinutes()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, "0"); // convert from 24h to 12h format - const period = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' + const period = hours >= 12 ? "PM" : "AM"; + hours %= 12; + hours = hours || 12; // the hour '0' should be '12' const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`; return formattedDate; diff --git a/frontend/src/views/Project/LogsPage/components/types.tsx b/frontend/src/views/Project/LogsPage/components/types.tsx new file mode 100644 index 000000000..025818578 --- /dev/null +++ b/frontend/src/views/Project/LogsPage/components/types.tsx @@ -0,0 +1,13 @@ +import * as yup from "yup"; + +import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums"; + +export const auditLogFilterFormSchema = yup.object({ + eventType: yup.string() + .oneOf(Object.values(EventType), "Invalid event type"), + actor: yup.string(), + userAgentType: yup.string() + .oneOf(Object.values(UserAgentType), "Invalid user agent type"), +}).required(); + +export type AuditLogFilterFormData = yup.InferType; \ No newline at end of file From 2067c021ed8bd1817c4688869e60e8f64994b271 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 7 Aug 2023 16:21:51 +0530 Subject: [PATCH 12/64] feat(ui): added pagination component --- frontend/.storybook/preview.js | 22 ++++- .../v2/Pagination/Pagination.stories.tsx | 29 ++++++ .../components/v2/Pagination/Pagination.tsx | 95 +++++++++++++++++++ .../src/components/v2/Pagination/index.tsx | 2 + 4 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/v2/Pagination/Pagination.stories.tsx create mode 100644 frontend/src/components/v2/Pagination/Pagination.tsx create mode 100644 frontend/src/components/v2/Pagination/index.tsx diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index 1fef7cf80..2a3a61d5a 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -1,8 +1,22 @@ -import { themes } from '@storybook/theming'; -import '../src/styles/globals.css'; +import { themes } from "@storybook/theming"; +import "react-day-picker/dist/style.css"; +import "../src/styles/globals.css"; export const parameters = { - actions: { argTypesRegex: '^on[A-Z].*' }, + actions: { argTypesRegex: "^on[A-Z].*" }, + backgrounds: { + default: "dark", + values: [ + { + name: "dark", + value: "rgb(14, 16, 20)" + }, + { + name: "paper", + value: "rgb(30, 31, 34)" + } + ] + }, controls: { matchers: { color: /(background|color)$/i, @@ -10,6 +24,6 @@ export const parameters = { } }, darkMode: { - dark: { ...themes.dark, appContentBg: 'rgb(14,16,20)', appBg: 'rgb(14,16,20)' } + dark: { ...themes.dark, appContentBg: "rgb(14,16,20)", appBg: "rgb(14,16,20)" } } }; diff --git a/frontend/src/components/v2/Pagination/Pagination.stories.tsx b/frontend/src/components/v2/Pagination/Pagination.stories.tsx new file mode 100644 index 000000000..fd10ef8c6 --- /dev/null +++ b/frontend/src/components/v2/Pagination/Pagination.stories.tsx @@ -0,0 +1,29 @@ +// eslint-disable-next-line +import { useArgs } from "@storybook/client-api"; +import type { Meta } from "@storybook/react"; + +import { Pagination, PaginationProps } from "./Pagination"; + +const meta: Meta = { + title: "Components/Pagination", + component: Pagination, + tags: ["v2"], + args: { + count: 50 + } +}; + +export default meta; +// type Story = StoryObj; + +export const Primary = (args: PaginationProps) => { + const [, updateArgs] = useArgs(); + + return ( + updateArgs({ page })} + onChangePerPage={(perPage) => updateArgs({ perPage, page: 1 })} + /> + ); +}; diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx new file mode 100644 index 000000000..99d50a07f --- /dev/null +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -0,0 +1,95 @@ +import { + faCaretDown, + faCheck, + faChevronLeft, + faChevronRight +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "../Dropdown"; +import { IconButton } from "../IconButton"; + +export type PaginationProps = { + count: number; + page: number; + perPage?: number; + onChangePage: (pageNumber: number) => void; + onChangePerPage: (newRows: number) => void; + className: string; + perPageList?: number[]; +}; + +export const Pagination = ({ + count, + page = 1, + perPage = 20, + onChangePage, + onChangePerPage, + perPageList = [10, 20, 50, 100], + className +}: PaginationProps) => { + const prevPageNumber = Math.max(1, page - 1); + const canGoPrev = page > 1; + + const upperLimit = Math.ceil(count / perPage); + const nextPageNumber = Math.min(upperLimit, page + 1); + const canGoNext = page + 1 <= upperLimit; + + return ( +
+
+
+ {(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count} +
+ + + + + + + + {perPageList.map((perPageOption) => ( + } + iconPos="right" + onClick={() => onChangePerPage(perPageOption)} + > + {perPageOption} rows per page + + ))} + + +
+
+ onChangePage(prevPageNumber)} + isDisabled={!canGoPrev} + > + + + onChangePage(nextPageNumber)} + isDisabled={!canGoNext} + > + + +
+
+ ); +}; diff --git a/frontend/src/components/v2/Pagination/index.tsx b/frontend/src/components/v2/Pagination/index.tsx new file mode 100644 index 000000000..583a0b550 --- /dev/null +++ b/frontend/src/components/v2/Pagination/index.tsx @@ -0,0 +1,2 @@ +export type { PaginationProps } from "./Pagination"; +export { Pagination } from "./Pagination"; From be86e4176cdf7914edc48767fe9dab3fe71057ce Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 7 Aug 2023 16:22:15 +0530 Subject: [PATCH 13/64] feat(ui): added datepicker component --- frontend/package-lock.json | 244 ++++++++++++++++-- frontend/package.json | 4 +- .../v2/DatePicker/DatePicker.stories.tsx | 18 ++ .../components/v2/DatePicker/DatePicker.tsx | 37 +++ .../src/components/v2/DatePicker/index.tsx | 2 + .../src/components/v2/Dropdown/Dropdown.tsx | 5 +- frontend/src/components/v2/index.tsx | 2 + frontend/src/pages/_app.tsx | 1 + frontend/src/styles/globals.css | 15 ++ .../components/WebhooksTab/WebhooksTab.tsx | 5 +- 10 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/v2/DatePicker/DatePicker.stories.tsx create mode 100644 frontend/src/components/v2/DatePicker/DatePicker.tsx create mode 100644 frontend/src/components/v2/DatePicker/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 987ebd5c8..568410bb2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,7 +47,7 @@ "classnames": "^2.3.1", "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", - "dayjs": "^1.11.9", + "date-fns": "^2.30.0", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", @@ -69,6 +69,7 @@ "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", "react-contenteditable": "^3.3.7", + "react-day-picker": "^8.8.0", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.4", "react-hook-form": "^7.43.0", @@ -95,6 +96,7 @@ "@storybook/addon-links": "^7.0.23", "@storybook/addon-styling": "^1.3.0", "@storybook/blocks": "^7.0.23", + "@storybook/client-api": "^7.2.1", "@storybook/nextjs": "^7.0.23", "@storybook/react": "^7.0.23", "@storybook/testing-library": "^0.2.0", @@ -5781,6 +5783,20 @@ } } }, + "node_modules/@storybook/builder-webpack5/node_modules/@storybook/client-api": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.0.23.tgz", + "integrity": "sha512-Z2ubbWJXORuhb1Faur3DL5zV8OObanQY7ow506CmEwNKRgy2LVzkBwpm4woEJHgw95sOyGuz3f3xfDP2DC6M7A==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.0.23", + "@storybook/preview-api": "7.0.23" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { "version": "16.18.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.36.tgz", @@ -6126,13 +6142,96 @@ "dev": true }, "node_modules/@storybook/client-api": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.0.23.tgz", - "integrity": "sha512-Z2ubbWJXORuhb1Faur3DL5zV8OObanQY7ow506CmEwNKRgy2LVzkBwpm4woEJHgw95sOyGuz3f3xfDP2DC6M7A==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.2.1.tgz", + "integrity": "sha512-VeRUxc4ufSaGAQPe/LM4aucpEx2UHHw9c+tzolV3hzGIp6pmIAS8XI6thL2IccYmsNMS2zz9oDESYP9cNlTsyA==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.0.23", - "@storybook/preview-api": "7.0.23" + "@storybook/client-logger": "7.2.1", + "@storybook/preview-api": "7.2.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/client-api/node_modules/@storybook/channels": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.2.1.tgz", + "integrity": "sha512-3ZogzjwlFG+oarwnI7TTvWvHVOUtJbjrgZkM5QuLMlxNzIR1XuBY8f01yf4K8+VpdNy9DY+7Q/j6tBThfwYvpA==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.2.1", + "@storybook/core-events": "7.2.1", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.0.3", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/client-api/node_modules/@storybook/client-logger": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.2.1.tgz", + "integrity": "sha512-Lyht/lJg2S65CXRy9rXAZXP/Mgye7jbi/aqQL8z9VRMGChbL+k/3pSZnXTTrD1OVSpCEr4UWA+9bStzT4VjtYA==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/client-api/node_modules/@storybook/core-events": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.2.1.tgz", + "integrity": "sha512-EUXYb3gyQ2EzpDAWkgfoDl1EPabj3OE6+zntsD/gwvzQU85BTocs10ksnRyS55bfrQpYbf+Z+gw2CZboyagLgg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/client-api/node_modules/@storybook/preview-api": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.2.1.tgz", + "integrity": "sha512-WKecuOdeh9+og6bPR9KoQf/JCeSRPCcfZv9uNfJzAp3IiTnS3UpfCz+HBZzZJQrisgbd7OulNY400HQUmxY2Ag==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.2.1", + "@storybook/client-logger": "7.2.1", + "@storybook/core-events": "7.2.1", + "@storybook/csf": "^0.1.0", + "@storybook/global": "^5.0.0", + "@storybook/types": "7.2.1", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "synchronous-promise": "^2.0.15", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/client-api/node_modules/@storybook/types": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.2.1.tgz", + "integrity": "sha512-YwlIY1uyxfJjijbB5x1d1QOKaUUDJnMX8BSb8oGqU4cyT76X/Is4CbGs+vccFsJo0tZu1GfuahYXl0EDT0nnSQ==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.2.1", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" }, "funding": { "type": "opencollective", @@ -10724,10 +10823,20 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "node_modules/dayjs": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", - "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } }, "node_modules/debug": { "version": "4.3.4", @@ -19041,6 +19150,19 @@ "react": ">=16.3" } }, + "node_modules/react-day-picker": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz", + "integrity": "sha512-QIC3uOuyGGbtypbd5QEggsCSqVaPNu8kzUWquZ7JjW9fuWB9yv7WyixKmnaFelTLXFdq7h7zU6n/aBleBqe/dA==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", @@ -26972,6 +27094,16 @@ "webpack-virtual-modules": "^0.4.3" }, "dependencies": { + "@storybook/client-api": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.0.23.tgz", + "integrity": "sha512-Z2ubbWJXORuhb1Faur3DL5zV8OObanQY7ow506CmEwNKRgy2LVzkBwpm4woEJHgw95sOyGuz3f3xfDP2DC6M7A==", + "dev": true, + "requires": { + "@storybook/client-logger": "7.0.23", + "@storybook/preview-api": "7.0.23" + } + }, "@types/node": { "version": "16.18.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.36.tgz", @@ -27215,13 +27347,78 @@ } }, "@storybook/client-api": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.0.23.tgz", - "integrity": "sha512-Z2ubbWJXORuhb1Faur3DL5zV8OObanQY7ow506CmEwNKRgy2LVzkBwpm4woEJHgw95sOyGuz3f3xfDP2DC6M7A==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.2.1.tgz", + "integrity": "sha512-VeRUxc4ufSaGAQPe/LM4aucpEx2UHHw9c+tzolV3hzGIp6pmIAS8XI6thL2IccYmsNMS2zz9oDESYP9cNlTsyA==", "dev": true, "requires": { - "@storybook/client-logger": "7.0.23", - "@storybook/preview-api": "7.0.23" + "@storybook/client-logger": "7.2.1", + "@storybook/preview-api": "7.2.1" + }, + "dependencies": { + "@storybook/channels": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.2.1.tgz", + "integrity": "sha512-3ZogzjwlFG+oarwnI7TTvWvHVOUtJbjrgZkM5QuLMlxNzIR1XuBY8f01yf4K8+VpdNy9DY+7Q/j6tBThfwYvpA==", + "dev": true, + "requires": { + "@storybook/client-logger": "7.2.1", + "@storybook/core-events": "7.2.1", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.0.3", + "tiny-invariant": "^1.3.1" + } + }, + "@storybook/client-logger": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.2.1.tgz", + "integrity": "sha512-Lyht/lJg2S65CXRy9rXAZXP/Mgye7jbi/aqQL8z9VRMGChbL+k/3pSZnXTTrD1OVSpCEr4UWA+9bStzT4VjtYA==", + "dev": true, + "requires": { + "@storybook/global": "^5.0.0" + } + }, + "@storybook/core-events": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.2.1.tgz", + "integrity": "sha512-EUXYb3gyQ2EzpDAWkgfoDl1EPabj3OE6+zntsD/gwvzQU85BTocs10ksnRyS55bfrQpYbf+Z+gw2CZboyagLgg==", + "dev": true + }, + "@storybook/preview-api": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.2.1.tgz", + "integrity": "sha512-WKecuOdeh9+og6bPR9KoQf/JCeSRPCcfZv9uNfJzAp3IiTnS3UpfCz+HBZzZJQrisgbd7OulNY400HQUmxY2Ag==", + "dev": true, + "requires": { + "@storybook/channels": "7.2.1", + "@storybook/client-logger": "7.2.1", + "@storybook/core-events": "7.2.1", + "@storybook/csf": "^0.1.0", + "@storybook/global": "^5.0.0", + "@storybook/types": "7.2.1", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "synchronous-promise": "^2.0.15", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/types": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.2.1.tgz", + "integrity": "sha512-YwlIY1uyxfJjijbB5x1d1QOKaUUDJnMX8BSb8oGqU4cyT76X/Is4CbGs+vccFsJo0tZu1GfuahYXl0EDT0nnSQ==", + "dev": true, + "requires": { + "@storybook/channels": "7.2.1", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + } + } } }, "@storybook/client-logger": { @@ -30800,10 +30997,13 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "dayjs": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", - "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } }, "debug": { "version": "4.3.4", @@ -36915,6 +37115,12 @@ "prop-types": "^15.7.1" } }, + "react-day-picker": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz", + "integrity": "sha512-QIC3uOuyGGbtypbd5QEggsCSqVaPNu8kzUWquZ7JjW9fuWB9yv7WyixKmnaFelTLXFdq7h7zU6n/aBleBqe/dA==", + "requires": {} + }, "react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index e779e95b1..9aa355d4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,7 @@ "classnames": "^2.3.1", "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", - "dayjs": "^1.11.9", + "date-fns": "^2.30.0", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", @@ -77,6 +77,7 @@ "react-beautiful-dnd": "^13.1.1", "react-code-input": "^3.10.1", "react-contenteditable": "^3.3.7", + "react-day-picker": "^8.8.0", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.4", "react-hook-form": "^7.43.0", @@ -103,6 +104,7 @@ "@storybook/addon-links": "^7.0.23", "@storybook/addon-styling": "^1.3.0", "@storybook/blocks": "^7.0.23", + "@storybook/client-api": "^7.2.1", "@storybook/nextjs": "^7.0.23", "@storybook/react": "^7.0.23", "@storybook/testing-library": "^0.2.0", diff --git a/frontend/src/components/v2/DatePicker/DatePicker.stories.tsx b/frontend/src/components/v2/DatePicker/DatePicker.stories.tsx new file mode 100644 index 000000000..4c6e95b5e --- /dev/null +++ b/frontend/src/components/v2/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { DatePicker } from "./DatePicker"; + +const meta: Meta = { + title: "Components/DatePicker", + component: DatePicker, + tags: ["v2"], + argTypes: {} +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args +export const Primary: Story = { + args: {} +}; diff --git a/frontend/src/components/v2/DatePicker/DatePicker.tsx b/frontend/src/components/v2/DatePicker/DatePicker.tsx new file mode 100644 index 000000000..4c59c00b1 --- /dev/null +++ b/frontend/src/components/v2/DatePicker/DatePicker.tsx @@ -0,0 +1,37 @@ +import { DayPicker, DayPickerProps } from "react-day-picker"; +import { faCalendar } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover"; +import { format } from "date-fns"; + +import { Button } from "../Button"; +import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2"; + +export type DatePickerProps = Omit & { + value?: Date; + onChange: (date?: Date) => void; + popUpProps: PopoverProps; + popUpContentProps: PopoverContentProps; +}; + +// Doc: https://react-day-picker.js.org/ +export const DatePicker = ({ + value, + onChange, + popUpProps, + popUpContentProps, + ...props +}: DatePickerProps) => { + return ( + + + + + + + + + ); +}; diff --git a/frontend/src/components/v2/DatePicker/index.tsx b/frontend/src/components/v2/DatePicker/index.tsx new file mode 100644 index 000000000..86fa9e298 --- /dev/null +++ b/frontend/src/components/v2/DatePicker/index.tsx @@ -0,0 +1,2 @@ +export type { DatePickerProps } from "./DatePicker"; +export { DatePicker } from "./DatePicker"; diff --git a/frontend/src/components/v2/Dropdown/Dropdown.tsx b/frontend/src/components/v2/Dropdown/Dropdown.tsx index def928c74..8d2427c1c 100644 --- a/frontend/src/components/v2/Dropdown/Dropdown.tsx +++ b/frontend/src/components/v2/Dropdown/Dropdown.tsx @@ -49,6 +49,7 @@ export type DropdownMenuItemProps = icon?: ReactNode; as?: T; inputRef?: Ref; + iconPos?: "left" | "right"; }; export const DropdownMenuItem = ({ @@ -57,6 +58,7 @@ export const DropdownMenuItem = ({ className, icon, as: Item = "button", + iconPos = "left", ...props }: DropdownMenuItemProps & ComponentPropsWithRef) => ( ({ )} > - {icon && {icon}} + {icon && iconPos === "left" && {icon}} {children} + {icon && iconPos === "right" && {icon}} ); diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index b4412b47b..8d23594dc 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -1,6 +1,7 @@ export * from "./Button"; export * from "./Card"; export * from "./Checkbox"; +export * from "./DatePicker"; export * from "./DeleteActionModal"; export * from "./Drawer"; export * from "./Dropdown"; @@ -12,6 +13,7 @@ export * from "./IconButton"; export * from "./Input"; export * from "./Menu"; export * from "./Modal"; +export * from "./Pagination"; export * from "./Popoverv2"; export * from "./SecretInput"; export * from "./Select"; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 82e642245..332557732 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -28,6 +28,7 @@ import { queryClient } from "@app/reactQuery"; import "nprogress/nprogress.css"; import "@fortawesome/fontawesome-svg-core/styles.css"; +import "react-day-picker/dist/style.css"; import "../styles/globals.css"; import "@app/i18n"; diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index ed191a319..ae8d17427 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -1,5 +1,20 @@ @tailwind base; @tailwind components; + +.rdp-day, +.rdp-nav_button { + @apply rounded-md hover:text-mineshaft-500; +} + +.rdp-button:hover:not([disabled]):not(.rdp-day_selected), +.rdp-nav_button:hover { + @apply bg-primary; +} + +.rdp-day_today { + @apply bg-primary/50 hover:bg-primary-600 text-mineshaft-500; +} + @tailwind utilities; @layer utilities { diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx index 598af0360..d5cb16b7b 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { faInfoCircle, faPlug, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import dayjs from "dayjs"; +import { format } from "date-fns"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { @@ -196,7 +196,8 @@ export const WebhooksTab = () => { content={
- Updated At: {dayjs(updatedAt).format("YYYY-MM-DD, hh:mm A")} + Updated At:{" "} + {format(new Date(updatedAt), "YYYY-MM-DD, hh:mm aaa")}
{lastRunErrorMessage && (
From 2591161272baa3e93a34c73dddcc201ddc41d16e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 8 Aug 2023 09:50:49 +0700 Subject: [PATCH 14/64] Add more audit log events --- .../v1/integrationAuthController.ts | 45 ++++ .../controllers/v1/integrationController.ts | 43 +++- .../controllers/v1/membershipController.ts | 39 ++- .../controllers/v1/secretsFolderController.ts | 86 ++++++- .../src/controllers/v1/webhookController.ts | 66 ++++- .../controllers/v2/environmentController.ts | 55 ++++- .../v2/serviceTokenDataController.ts | 35 ++- .../ee/controllers/v1/workspaceController.ts | 58 ++++- backend/src/ee/models/auditLog/enums.ts | 22 +- backend/src/ee/models/auditLog/types.ts | 217 ++++++++++++++++- backend/src/ee/routes/v1/workspace.ts | 4 +- backend/src/ee/services/EELicenseService.ts | 6 +- backend/src/services/FolderService.ts | 17 +- .../src/hooks/api/auditLogs/constants.tsx | 30 ++- frontend/src/hooks/api/auditLogs/enums.tsx | 30 ++- frontend/src/hooks/api/auditLogs/queries.tsx | 25 +- frontend/src/hooks/api/auditLogs/types.tsx | 225 +++++++++++++++++- .../Project/LogsPage/components/LogsTable.tsx | 4 +- .../LogsPage/components/LogsTableRow.tsx | 163 +++++++++++++ 19 files changed, 1117 insertions(+), 53 deletions(-) diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 43d223ad6..f4c25d54b 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -3,7 +3,9 @@ import { Types } from "mongoose"; import { standardRequest } from "../../config/request"; import { getApps, getTeams, revokeAccess } from "../../integrations"; import { Bot, IntegrationAuth } from "../../models"; +import { EventType } from "../../ee/models"; import { IntegrationService } from "../../services"; +import { EEAuditLogService } from "../../ee/services"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, @@ -62,6 +64,19 @@ export const oAuthExchange = async (req: Request, res: Response) => { environment: environments[0].slug }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.AUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + }, + { + workspaceId: integrationAuth.workspace + } + ); + return res.status(200).send({ integrationAuth }); @@ -129,6 +144,19 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) => }); if (!integrationAuth) throw new Error("Failed to save integration access token"); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.AUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + }, + { + workspaceId: integrationAuth.workspace + } + ); return res.status(200).send({ integrationAuth @@ -530,6 +558,23 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => { integrationAuth: req.integrationAuth, accessToken: req.accessToken }); + + if (!integrationAuth) return res.status(400).send({ + message: "Failed to find integration authorization" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UNAUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + }, + { + workspaceId: integrationAuth.workspace + } + ); return res.status(200).send({ integrationAuth diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 91b32c805..e647cfb00 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -6,6 +6,8 @@ import { eventStartIntegration } from "../../events"; import Folder from "../../models/folder"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; +import { EEAuditLogService } from "../../ee/services"; +import { EventType } from "../../ee/models"; /** * Create/initialize an (empty) integration for integration authorization @@ -74,6 +76,25 @@ export const createIntegration = async (req: Request, res: Response) => { }) }); } + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_INTEGRATION, + metadata: { + integrationId: integration._id.toString(), + integration: integration.integration, + environment: integration.environment, + secretPath, + app: integration.app, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId + } + }, + { + workspaceId: integration.workspace + } + ); return res.status(200).send({ integration @@ -148,8 +169,7 @@ export const updateIntegration = async (req: Request, res: Response) => { }; /** - * Delete integration with id [integrationId] and deactivate bot if there are - * no integrations left + * Delete integration with id [integrationId] * @param req * @param res * @returns @@ -163,6 +183,25 @@ export const deleteIntegration = async (req: Request, res: Response) => { if (!integration) throw new Error("Failed to find integration"); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_INTEGRATION, + metadata: { + integrationId: integration._id.toString(), + integration: integration.integration, + environment: integration.environment, + secretPath: integration.secretPath, + app: integration.app, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId + } + }, + { + workspaceId: integration.workspace + } + ); + return res.status(200).send({ integration }); diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index d795bcb60..a473f0c4a 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,9 +1,12 @@ import { Request, Response } from "express"; -import { Key, Membership, MembershipOrg, User } from "../../models"; +import { Types } from "mongoose"; +import { Key, Membership, MembershipOrg, User, IUser } from "../../models"; +import { EventType } from "../../ee/models"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { sendMail } from "../../helpers/nodemailer"; import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; import { getSiteURL } from "../../config"; +import { EEAuditLogService } from "../../ee/services"; /** * Check that user is a member of workspace with id [workspaceId] @@ -36,11 +39,11 @@ export const validateMembership = async (req: Request, res: Response) => { */ export const deleteMembership = async (req: Request, res: Response) => { const { membershipId } = req.params; - + // check if membership to delete exists const membershipToDelete = await Membership.findOne({ _id: membershipId - }).populate("user"); + }).populate<{ user: IUser }>("user"); if (!membershipToDelete) { throw new Error("Failed to delete workspace membership that doesn't exist"); @@ -66,6 +69,20 @@ export const deleteMembership = async (req: Request, res: Response) => { const deletedMembership = await deleteMember({ membershipId: membershipToDelete._id.toString() }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.REMOVE_WORKSPACE_MEMBER, + metadata: { + userId: membershipToDelete.user._id.toString(), + email: membershipToDelete.user.email + } + }, + { + workspaceId: membership.workspace + } + ); return res.status(200).send({ deletedMembership @@ -140,7 +157,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { const inviteeMembership = await Membership.findOne({ user: invitee._id, workspace: workspaceId - }); + }).populate<{ user: IUser }>("user"); if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); @@ -181,6 +198,20 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { } }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_WORKSPACE_MEMBER, + metadata: { + userId: invitee._id.toString(), + email: invitee.email + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.status(200).send({ invitee, latestKey diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 6b31fd111..742852343 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; import { Secret } from "../../models"; +import { Types } from "mongoose"; import Folder from "../../models/folder"; import { BadRequestError } from "../../utils/errors"; import { @@ -8,6 +9,7 @@ import { generateFolderId, getAllFolderIds, getFolderByPath, + getFolderPath, getParentFromFolderId, searchByFolderId, searchByFolderIdWithDir, @@ -15,10 +17,9 @@ import { } from "../../services/FolderService"; import { ADMIN, MEMBER } from "../../variables"; import { validateMembership } from "../../helpers/membership"; -import { FolderVersion } from "../../ee/models"; -import { EESecretService } from "../../ee/services"; +import { FolderVersion, EventType } from "../../ee/models"; +import { EESecretService, EEAuditLogService } from "../../ee/services"; -// TODO // verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { const { workspaceId, environment, folderName, parentFolderId } = req.body; @@ -33,6 +34,7 @@ export const createFolder = async (req: Request, res: Response) => { environment, }).lean(); // space has no folders initialized + if (!folders) { const id = generateFolderId(); const folder = new Folder({ @@ -56,13 +58,32 @@ export const createFolder = async (req: Request, res: Response) => { workspaceId, environment, }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_FOLDER, + metadata: { + environment, + folderId: id, + folderName, + folderPath: `root/${folderName}` + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.json({ folder: { id, name: folderName } }); } const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); + await Folder.findByIdAndUpdate(folders._id, folders); const parentFolder = searchByFolderId(folders.nodes, parentFolderId); + const folderVersion = new FolderVersion({ workspace: workspaceId, environment, @@ -75,6 +96,24 @@ export const createFolder = async (req: Request, res: Response) => { environment, folderId: parentFolderId, }); + + const folderPath = await getFolderPath(folders, folder.id); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_FOLDER, + metadata: { + environment, + folderId: folder.id, + folderName, + folderPath + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); return res.json({ folder }); }; @@ -87,7 +126,7 @@ export const updateFolderById = async (req: Request, res: Response) => { if (!folders) { throw BadRequestError({ message: "The folder doesn't exist" }); } - + // check that user is a member of the workspace await validateMembership({ userId: req.user._id.toString(), @@ -100,10 +139,12 @@ export const updateFolderById = async (req: Request, res: Response) => { throw BadRequestError({ message: "The folder doesn't exist" }); } const folder = parentFolder.children.find(({ id }) => id === folderId); + if (!folder) { throw BadRequestError({ message: "The folder doesn't exist" }); } + const oldFolderName = folder.name; parentFolder.version += 1; folder.name = name; @@ -121,6 +162,25 @@ export const updateFolderById = async (req: Request, res: Response) => { folderId: parentFolder.id, }); + const folderPath = await getFolderPath(folders, folder.id); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_FOLDER, + metadata: { + environment, + folderId: folder.id, + oldFolderName, + newFolderName: name, + folderPath + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.json({ message: "Successfully updated folder", folder: { name: folder.name, id: folder.id }, @@ -143,6 +203,8 @@ export const deleteFolder = async (req: Request, res: Response) => { acceptedRoles: [ADMIN, MEMBER], }); + const folderPath = await getFolderPath(folders, folderId); + const delOp = deleteFolderById(folders.nodes, folderId); if (!delOp) { throw BadRequestError({ message: "The folder doesn't exist" }); @@ -173,6 +235,22 @@ export const deleteFolder = async (req: Request, res: Response) => { folderId: parentFolder.id, }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_FOLDER , + metadata: { + environment, + folderId, + folderName: delFolder.name, + folderPath + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + res.send({ message: "successfully deleted folders", folders: delFolderIds }); }; diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts index afaf82e81..a79b794cf 100644 --- a/backend/src/controllers/v1/webhookController.ts +++ b/backend/src/controllers/v1/webhookController.ts @@ -4,7 +4,9 @@ import { client, getRootEncryptionKey } from "../../config"; import { validateMembership } from "../../helpers"; import Webhook from "../../models/webhooks"; import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; -import { BadRequestError } from "../../utils/errors"; +import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; +import { EEAuditLogService } from "../../ee/services"; +import { EventType } from "../../ee/models"; import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables"; export const createWebhook = async (req: Request, res: Response) => { @@ -27,6 +29,23 @@ export const createWebhook = async (req: Request, res: Response) => { } await webhook.save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_WEBHOOK, + metadata: { + webhookId: webhook._id.toString(), + environment, + secretPath, + webhookUrl, + isDisabled: false + } + }, + { + workspaceId + } + ); return res.status(200).send({ webhook, @@ -54,6 +73,23 @@ export const updateWebhook = async (req: Request, res: Response) => { } await webhook.save(); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + webhookId: webhook._id.toString(), + environment: webhook.environment, + secretPath: webhook.secretPath, + webhookUrl: webhook.url, + isDisabled + } + }, + { + workspaceId: webhook.workspace + } + ); + return res.status(200).send({ webhook, message: "successfully updated webhook" @@ -62,9 +98,10 @@ export const updateWebhook = async (req: Request, res: Response) => { export const deleteWebhook = async (req: Request, res: Response) => { const { webhookId } = req.params; - const webhook = await Webhook.findById(webhookId); + let webhook = await Webhook.findById(webhookId); + if (!webhook) { - throw BadRequestError({ message: "Webhook not found!!" }); + throw ResourceNotFoundError({ message: "Webhook not found!!" }); } await validateMembership({ @@ -72,8 +109,29 @@ export const deleteWebhook = async (req: Request, res: Response) => { workspaceId: webhook.workspace, acceptedRoles: [ADMIN, MEMBER] }); + + webhook = await Webhook.findByIdAndDelete(webhookId); - await webhook.deleteOne(); + if (!webhook) { + throw ResourceNotFoundError({ message: "Webhook not found!!" }); + } + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_WEBHOOK, + metadata: { + webhookId: webhook._id.toString(), + environment: webhook.environment, + secretPath: webhook.secretPath, + webhookUrl: webhook.url, + isDisabled: webhook.isDisabled + } + }, + { + workspaceId: webhook.workspace + } + ); return res.status(200).send({ message: "successfully removed webhook" diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index e7de3a8cf..20680f1c6 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -8,8 +8,8 @@ import { ServiceTokenData, Workspace, } from "../../models"; -import { SecretVersion } from "../../ee/models"; -import { EELicenseService } from "../../ee/services"; +import { SecretVersion, EventType } from "../../ee/models"; +import { EELicenseService, EEAuditLogService } from "../../ee/services"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import _ from "lodash"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; @@ -61,6 +61,20 @@ export const createWorkspaceEnvironment = async ( await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId)); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_ENVIRONMENT, + metadata: { + name: environmentName, + slug: environmentSlug + } + }, + { + workspaceId: workspace._id + } + ); + return res.status(200).send({ message: "Successfully created new environment", workspace: workspaceId, @@ -110,6 +124,8 @@ export const renameWorkspaceEnvironment = async ( if (envIndex === -1) { throw new Error("Invalid environment given"); } + + const oldEnvironment = workspace.environments[envIndex]; workspace.environments[envIndex].name = environmentName; workspace.environments[envIndex].slug = environmentSlug.toLowerCase(); @@ -142,8 +158,23 @@ export const renameWorkspaceEnvironment = async ( }, { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } - ) - + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_ENVIRONMENT, + metadata: { + oldName: oldEnvironment.name, + newName: environmentName, + oldSlug: oldEnvironment.slug, + newSlug: environmentSlug.toLowerCase() + } + }, + { + workspaceId: workspace._id + } + ); return res.status(200).send({ message: "Successfully update environment", @@ -179,6 +210,8 @@ export const deleteWorkspaceEnvironment = async ( if (envIndex === -1) { throw new Error("Invalid environment given"); } + + const oldEnvironment = workspace.environments[envIndex]; workspace.environments.splice(envIndex, 1); await workspace.save(); @@ -218,6 +251,20 @@ export const deleteWorkspaceEnvironment = async ( await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId)); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_ENVIRONMENT, + metadata: { + name: oldEnvironment.name, + slug: oldEnvironment.slug + } + }, + { + workspaceId: workspace._id + } + ); + return res.status(200).send({ message: "Successfully deleted environment", workspace: workspaceId, diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 10fb50ef1..c918e6953 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -4,7 +4,8 @@ import bcrypt from "bcrypt"; import { ServiceTokenData } from "../../models"; import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; -import { ActorType } from "../../ee/models"; +import { ActorType, EventType } from "../../ee/models"; +import { EEAuditLogService } from "../../ee/services"; /** * Return service token data associated with service token on request @@ -99,6 +100,20 @@ export const createServiceTokenData = async (req: Request, res: Response) => { if (!serviceTokenData) throw new Error("Failed to find service token data"); const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_SERVICE_TOKEN, + metadata: { + name, + scopes + } + }, + { + workspaceId + } + ); return res.status(200).send({ serviceToken, @@ -116,6 +131,24 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { const { serviceTokenDataId } = req.params; const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + if (!serviceTokenData) return res.status(200).send({ + message: "Failed to delete service token" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_SERVICE_TOKEN, + metadata: { + name: serviceTokenData.name, + scopes: serviceTokenData?.scopes + } + }, + { + workspaceId: serviceTokenData.workspace + } + ); return res.status(200).send({ serviceTokenData diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index a77cdca71..4db8c4855 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -13,13 +13,14 @@ import { ServiceActor, TFolderRootVersionSchema, TrustedIP, - UserActor + UserActor, + EventType } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import Folder, { TFolderSchema } from "../../../models/folder"; import { searchByFolderId } from "../../../services/FolderService"; -import { EELicenseService } from "../../services"; +import { EELicenseService, EEAuditLogService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; /** @@ -730,6 +731,21 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { isActive, comment, }).save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_TRUSTED_IP, + metadata: { + trustedIpId: trustedIp._id.toString(), + ipAddress: trustedIp.ipAddress, + prefix: trustedIp.prefix + } + }, + { + workspaceId: trustedIp.workspace + } + ); return res.status(200).send({ trustedIp @@ -793,6 +809,25 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { } ); + if (!trustedIp) return res.status(400).send({ + message: "Failed to update trusted IP" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_TRUSTED_IP, + metadata: { + trustedIpId: trustedIp._id.toString(), + ipAddress: trustedIp.ipAddress, + prefix: trustedIp.prefix + } + }, + { + workspaceId: trustedIp.workspace + } + ); + return res.status(200).send({ trustedIp }); @@ -816,6 +851,25 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { _id: new Types.ObjectId(trustedIpId), workspace: new Types.ObjectId(workspaceId) }); + + if (!trustedIp) return res.status(400).send({ + message: "Failed to delete trusted IP" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_TRUSTED_IP, + metadata: { + trustedIpId: trustedIp._id.toString(), + ipAddress: trustedIp.ipAddress, + prefix: trustedIp.prefix + } + }, + { + workspaceId: trustedIp.workspace + } + ); return res.status(200).send({ trustedIp diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index ce415e8c9..73dcd39bc 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -16,5 +16,25 @@ export enum EventType { REVEAL_SECRET = "reveal-secret", CREATE_SECRET = "create-secret", UPDATE_SECRET = "update-secret", - DELETE_SECRET = "delete-secret" + DELETE_SECRET = "delete-secret", + AUTHORIZE_INTEGRATION = "authorize-integration", + UNAUTHORIZE_INTEGRATION = "unauthorize-integration", + CREATE_INTEGRATION = "create-integration", + DELETE_INTEGRATION = "delete-integration", + ADD_TRUSTED_IP = "add-trusted-ip", + UPDATE_TRUSTED_IP = "update-trusted-ip", + DELETE_TRUSTED_IP = "delete-trusted-ip", + CREATE_SERVICE_TOKEN = "create-service-token", + DELETE_SERVICE_TOKEN = "delete-service-token", + CREATE_ENVIRONMENT = "create-environment", + UPDATE_ENVIRONMENT = "update-environment", + DELETE_ENVIRONMENT = "delete-environment", + ADD_WORKSPACE_MEMBER = "add-workspace-member", + REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + CREATE_FOLDER = "create-folder", + UPDATE_FOLDER = "update-folder", + DELETE_FOLDER = "delete-folder", + CREATE_WEBHOOK = "create-webhook", + UPDATE_WEBHOOK_STATUS = "update-webhook-status", + DELETE_WEBHOOK = "delete-webhook" } \ No newline at end of file diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 848fb88db..04ff317e6 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -80,9 +80,224 @@ interface DeleteSecretEvent { } } +interface AuthorizeIntegrationEvent { + type: EventType.AUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface UnauthorizeIntegrationEvent { + type: EventType.UNAUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface CreateIntegrationEvent { + type: EventType.CREATE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; // TODO: consider adding other vars + } +} + +interface DeleteIntegrationEvent { + type: EventType.DELETE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + } +} + +interface AddTrustedIPEvent { + type: EventType.ADD_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface UpdateTrustedIPEvent { + type: EventType.UPDATE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface DeleteTrustedIPEvent { + type: EventType.DELETE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface CreateServiceTokenEvent { + type: EventType.CREATE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface DeleteServiceTokenEvent { + type: EventType.DELETE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface CreateEnvironmentEvent { + type: EventType.CREATE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface UpdateEnvironmentEvent { + type: EventType.UPDATE_ENVIRONMENT; + metadata: { + oldName: string; + newName: string; + oldSlug: string; + newSlug: string; + } +} + +interface DeleteEnvironmentEvent { + type: EventType.DELETE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface AddWorkspaceMemberEvent { + type: EventType.ADD_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface RemoveWorkspaceMemberEvent { + type: EventType.REMOVE_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface CreateFolderEvent { + type: EventType.CREATE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface UpdateFolderEvent { + type: EventType.UPDATE_FOLDER; + metadata: { + environment: string; + folderId: string; + oldFolderName: string; + newFolderName: string; + folderPath: string; + } +} + +interface DeleteFolderEvent { + type: EventType.DELETE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface CreateWebhookEvent { + type: EventType.CREATE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface UpdateWebhookStatusEvent { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface DeleteWebhookEvent { + type: EventType.DELETE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + export type Event = | GetSecretsEvent | GetSecretEvent | CreateSecretEvent | UpdateSecretEvent - | DeleteSecretEvent; \ No newline at end of file + | DeleteSecretEvent + | AuthorizeIntegrationEvent + | UnauthorizeIntegrationEvent + | CreateIntegrationEvent + | DeleteIntegrationEvent + | AddTrustedIPEvent + | UpdateTrustedIPEvent + | DeleteTrustedIPEvent + | CreateServiceTokenEvent + | DeleteServiceTokenEvent + | CreateEnvironmentEvent + | UpdateEnvironmentEvent + | DeleteEnvironmentEvent + | AddWorkspaceMemberEvent + | RemoveWorkspaceMemberEvent + | CreateFolderEvent + | UpdateFolderEvent + | DeleteFolderEvent + | CreateWebhookEvent + | UpdateWebhookStatusEvent + | DeleteWebhookEvent; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 90eea76e6..2143f0781 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -97,8 +97,8 @@ router.get( query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), query("actor").isString().optional({ nullable: true }), - query("offset").isString().default("0"), - query("limit").isString().default("20"), + query("offset").default("0"), + query("limit").default("20"), validateRequest, workspaceController.getWorkspaceAuditLogs ); diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index de8e4369d..569f2483f 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -63,12 +63,12 @@ class EELicenseService { environmentsUsed: 0, secretVersioning: true, pitRecovery: false, - ipAllowlisting: false, + ipAllowlisting: true, rbac: true, customRateLimits: true, customAlerts: true, - auditLogs: false, - auditLogsRetentionDays: 0, + auditLogs: true, + auditLogsRetentionDays: 30, samlSSO: false, status: null, trial_end: null, diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index f2dfb3050..7d3e4bd59 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -1,6 +1,7 @@ import { nanoid } from "nanoid"; import { Types } from "mongoose"; -import Folder, { TFolderSchema } from "../models/folder"; +import Folder, { TFolderSchema, TFolderRootSchema } from "../models/folder"; +import { ResourceNotFoundError } from "../utils/errors"; type TAppendFolderDTO = { folderName: string; @@ -172,6 +173,20 @@ export const searchByFolderIdWithDir = ( return; }; +export const getFolderPath = ( + folders: TFolderRootSchema, + folderId: string + ) => { + const folderBySearch = searchByFolderIdWithDir(folders.nodes, folderId); + + if (!folderBySearch) throw ResourceNotFoundError({ + message: "Failed to find folder" + }); + + const folderPath = folderBySearch.dir.map((folder) => folder.name).join("/"); + return folderPath; + } + // to get folder of a path given // Like /frontend/folder#1 export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index f54b20b1a..3b15efa91 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -1,11 +1,31 @@ import { EventType, UserAgentType } from "./enums"; export const eventToNameMap: { [K in EventType]: string } = { - [EventType.GET_SECRETS]: "Get Secrets", - [EventType.GET_SECRET]: "Get Secret", - [EventType.CREATE_SECRET]: "Create Secret", - [EventType.UPDATE_SECRET]: "Update Secret", - [EventType.DELETE_SECRET]: "Delete Secret", + [EventType.GET_SECRETS]: "Get secrets", + [EventType.GET_SECRET]: "Get secret", + [EventType.CREATE_SECRET]: "Create secret", + [EventType.UPDATE_SECRET]: "Update secret", + [EventType.DELETE_SECRET]: "Delete secret", + [EventType.AUTHORIZE_INTEGRATION]: "Authorize integration", + [EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration", + [EventType.CREATE_INTEGRATION]: "Create integration", + [EventType.DELETE_INTEGRATION]: "Delete integration", + [EventType.ADD_TRUSTED_IP]: "Add trusted IP", + [EventType.UPDATE_TRUSTED_IP]: "Update trusted IP", + [EventType.DELETE_TRUSTED_IP]: "Delete trusted IP", + [EventType.CREATE_SERVICE_TOKEN]: "Create service token", + [EventType.DELETE_SERVICE_TOKEN]: "Delete service token", + [EventType.CREATE_ENVIRONMENT]: "Create environment", + [EventType.UPDATE_ENVIRONMENT]: "Update environment", + [EventType.DELETE_ENVIRONMENT]: "Delete environment", + [EventType.ADD_WORKSPACE_MEMBER]: "Add member", + [EventType.REMOVE_WORKSPACE_MEMBER]: "Remove member", + [EventType.CREATE_FOLDER]: "Create folder", + [EventType.UPDATE_FOLDER]: "Update folder", + [EventType.DELETE_FOLDER]: "Delete folder", + [EventType.CREATE_WEBHOOK]: "Create webhook", + [EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status", + [EventType.DELETE_WEBHOOK]: "Delete webhook", }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 4c75f6395..3e159552d 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -11,9 +11,29 @@ export enum UserAgentType { } export enum EventType { - GET_SECRETS = "get-secrets", - GET_SECRET = "get-secret", - CREATE_SECRET = "create-secret", - UPDATE_SECRET = "update-secret", - DELETE_SECRET = "delete-secret" + GET_SECRETS = "get-secrets", + GET_SECRET = "get-secret", + CREATE_SECRET = "create-secret", + UPDATE_SECRET = "update-secret", + DELETE_SECRET = "delete-secret", + AUTHORIZE_INTEGRATION = "authorize-integration", + UNAUTHORIZE_INTEGRATION = "unauthorize-integration", + CREATE_INTEGRATION = "create-integration", + DELETE_INTEGRATION = "delete-integration", + ADD_TRUSTED_IP = "add-trusted-ip", + UPDATE_TRUSTED_IP = "update-trusted-ip", + DELETE_TRUSTED_IP = "delete-trusted-ip", + CREATE_SERVICE_TOKEN = "create-service-token", + DELETE_SERVICE_TOKEN = "delete-service-token", + CREATE_ENVIRONMENT = "create-environment", + UPDATE_ENVIRONMENT = "update-environment", + DELETE_ENVIRONMENT = "delete-environment", + ADD_WORKSPACE_MEMBER = "add-workspace-member", + REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + CREATE_FOLDER = "create-folder", + UPDATE_FOLDER = "update-folder", + DELETE_FOLDER = "delete-folder", + CREATE_WEBHOOK = "create-webhook", + UPDATE_WEBHOOK_STATUS = "update-webhook-status", + DELETE_WEBHOOK = "delete-webhook" } \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 05148c941..930de7f0a 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -2,28 +2,22 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { EventType, UserAgentType } from "./enums"; import { Actor, - AuditLog} from "./types"; + AuditLog, + AuditLogFilters +} from "./types"; export const workspaceKeys = { - getAuditLogs: (workspaceId: string, filters: { - eventType?: EventType; - userAgentType?: UserAgentType; - actor?: string; - }) => [{ workspaceId, filters }, "audit-logs"] as const, + getAuditLogs: (workspaceId: string, filters: AuditLogFilters) => [{ workspaceId, filters }, "audit-logs"] as const, getAuditLogActorFilterOpts: (workspaceId: string) => [{ workspaceId }, "audit-log-actor-filters"] as const } -export const useGetAuditLogs = (workspaceId: string, filters: { - eventType?: EventType; - userAgentType?: UserAgentType; - actor?: string; -}) => { +export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => { return useQuery({ queryKey: workspaceKeys.getAuditLogs(workspaceId, filters), queryFn: async () => { + const params = new URLSearchParams(); if (filters.eventType) { params.append("eventType", filters.eventType); @@ -37,6 +31,13 @@ export const useGetAuditLogs = (workspaceId: string, filters: { params.append("actor", filters.actor); } + if (filters.actor) { + params.append("actor", filters.actor); + } + + params.append("offset ", String(filters.offset)); + params.append("limit ", String(filters.limit)); + const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params }); return data.auditLogs; } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index e836521ec..b7c3cc47a 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -82,12 +82,227 @@ interface DeleteSecretEvent { } } +interface AuthorizeIntegrationEvent { + type: EventType.AUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface UnauthorizeIntegrationEvent { + type: EventType.UNAUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface CreateIntegrationEvent { + type: EventType.CREATE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; // TODO: consider adding other vars + } +} + +interface DeleteIntegrationEvent { + type: EventType.DELETE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + } +} + +interface AddTrustedIPEvent { + type: EventType.ADD_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface UpdateTrustedIPEvent { + type: EventType.UPDATE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface DeleteTrustedIPEvent { + type: EventType.DELETE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface CreateServiceTokenEvent { + type: EventType.CREATE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface DeleteServiceTokenEvent { + type: EventType.DELETE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface CreateEnvironmentEvent { + type: EventType.CREATE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface UpdateEnvironmentEvent { + type: EventType.UPDATE_ENVIRONMENT; + metadata: { + oldName: string; + newName: string; + oldSlug: string; + newSlug: string; + } +} + +interface DeleteEnvironmentEvent { + type: EventType.DELETE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface AddWorkspaceMemberEvent { + type: EventType.ADD_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface RemoveWorkspaceMemberEvent { + type: EventType.REMOVE_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface CreateFolderEvent { + type: EventType.CREATE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface UpdateFolderEvent { + type: EventType.UPDATE_FOLDER; + metadata: { + environment: string; + folderId: string; + oldFolderName: string; + newFolderName: string; + folderPath: string; + } +} + +interface DeleteFolderEvent { + type: EventType.DELETE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface CreateWebhookEvent { + type: EventType.CREATE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface UpdateWebhookStatusEvent { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface DeleteWebhookEvent { + type: EventType.DELETE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + export type Event = | GetSecretsEvent | GetSecretEvent | CreateSecretEvent | UpdateSecretEvent - | DeleteSecretEvent; + | DeleteSecretEvent + | AuthorizeIntegrationEvent + | UnauthorizeIntegrationEvent + | CreateIntegrationEvent + | DeleteIntegrationEvent + | AddTrustedIPEvent + | UpdateTrustedIPEvent + | DeleteTrustedIPEvent + | CreateServiceTokenEvent + | DeleteServiceTokenEvent + | CreateEnvironmentEvent + | UpdateEnvironmentEvent + | DeleteEnvironmentEvent + | AddWorkspaceMemberEvent + | RemoveWorkspaceMemberEvent + | CreateFolderEvent + | UpdateFolderEvent + | DeleteFolderEvent + | CreateWebhookEvent + | UpdateWebhookStatusEvent + | DeleteWebhookEvent; export type AuditLog = { _id: string; @@ -101,3 +316,11 @@ export type AuditLog = { createdAt: string; updatedAt: string; } + +export type AuditLogFilters = { + eventType?: EventType; + userAgentType?: UserAgentType; + actor?: string; + offset: number; + limit: number; +} \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx index 823e3f56d..0262a640d 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx @@ -33,7 +33,9 @@ export const LogsTable = ({ const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", { eventType, userAgentType, - actor + actor, + offset: 0, // TODO: update with pagination + limit: 20 // TODO: update with pagination }); return ( diff --git a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx index 05dfec9b0..7c4ecf90d 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx @@ -79,6 +79,169 @@ export const LogsTableRow = ({

{`Secret: ${event.metadata.secretKey}`}

); + case EventType.AUTHORIZE_INTEGRATION: + return ( + +

{`Integration: ${event.metadata.integration}`}

+ + ); + case EventType.UNAUTHORIZE_INTEGRATION: + return ( + +

{`Integration: ${event.metadata.integration}`}

+ + ); + case EventType.CREATE_INTEGRATION: + return ( + +

{`Integration: ${event.metadata.integration}`}

+

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+ {event.metadata.app && ( +

{`Target app: ${event.metadata.app}`}

+ )} + {event.metadata.targetEnvironment && ( +

{`Target environment: ${event.metadata.targetEnvironment}`}

+ )} + {event.metadata.targetEnvironmentId && ( +

{`Target environment ID: ${event.metadata.targetEnvironmentId}`}

+ )} + + ); + case EventType.DELETE_INTEGRATION: + return ( + +

{`Integration: ${event.metadata.integration}`}

+

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.secretPath}`}

+ {event.metadata.app && ( +

{`Target App: ${event.metadata.app}`}

+ )} + {event.metadata.targetEnvironment && ( +

{`Target environment: ${event.metadata.targetEnvironment}`}

+ )} + {event.metadata.targetEnvironmentId && ( +

{`Target environment ID: ${event.metadata.targetEnvironmentId}`}

+ )} + + ); + case EventType.ADD_TRUSTED_IP: + return ( + +

{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}

+ + ); + case EventType.UPDATE_TRUSTED_IP: + return ( + +

{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}

+ + ); + case EventType.DELETE_TRUSTED_IP: + return ( + +

{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}

+ + ); + case EventType.CREATE_SERVICE_TOKEN: + return ( + +

{`Name: ${event.metadata.name}`}

+ + ); + case EventType.DELETE_SERVICE_TOKEN: + return ( + +

{`Name: ${event.metadata.name}`}

+ + ); + case EventType.CREATE_ENVIRONMENT: + return ( + +

{`Name: ${event.metadata.name}`}

+

{`Slug: ${event.metadata.slug}`}

+ + ); + case EventType.UPDATE_ENVIRONMENT: + return ( + +

{`Old name: ${event.metadata.oldName}`}

+

{`New name: ${event.metadata.newName}`}

+

{`Old slug: ${event.metadata.oldSlug}`}

+

{`New slug: ${event.metadata.newSlug}`}

+ + ); + case EventType.DELETE_ENVIRONMENT: + return ( + +

{`Name: ${event.metadata.name}`}

+

{`Slug: ${event.metadata.slug}`}

+ + ); + case EventType.ADD_WORKSPACE_MEMBER: + return ( + +

{`Email: ${event.metadata.email}`}

+ + ); + case EventType.REMOVE_WORKSPACE_MEMBER: + return ( + +

{`Email: ${event.metadata.email}`}

+ + ); + case EventType.CREATE_FOLDER: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.folderPath}`}

+

{`Folder: ${event.metadata.folderName}`}

+ + ); + case EventType.UPDATE_FOLDER: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.folderPath}`}

+

{`Old folder: ${event.metadata.oldFolderName}`}

+

{`New folder: ${event.metadata.newFolderName}`}

+ + ); + case EventType.DELETE_FOLDER: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Path: ${event.metadata.folderPath}`}

+

{`Folder: ${event.metadata.folderName}`}

+ + ); + case EventType.CREATE_WEBHOOK: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Secret Path: ${event.metadata.secretPath}`}

+

{`Webhook URL: ${event.metadata.webhookUrl}`}

+

{`Disabled: ${event.metadata.isDisabled}`}

+ + ); + case EventType.UPDATE_WEBHOOK_STATUS: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Secret Path: ${event.metadata.secretPath}`}

+

{`Webhook URL: ${event.metadata.webhookUrl}`}

+

{`Disabled: ${event.metadata.isDisabled}`}

+ + ); + case EventType.DELETE_WEBHOOK: + return ( + +

{`Environment: ${event.metadata.environment}`}

+

{`Secret Path: ${event.metadata.secretPath}`}

+

{`Webhook URL: ${event.metadata.webhookUrl}`}

+

{`Disabled: ${event.metadata.isDisabled}`}

+ + ); default: return ( Test From 6cb8cf53f838ca77e89ee93f8588d8d1707d18fa Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 8 Aug 2023 12:52:34 +0700 Subject: [PATCH 15/64] Add date filter and pagination component to audit log v2 --- .../controllers/v1/membershipController.ts | 2 +- .../controllers/v1/secretsFolderController.ts | 4 +- .../controllers/v2/environmentController.ts | 4 +- .../ee/controllers/v1/workspaceController.ts | 28 ++- backend/src/ee/routes/v1/workspace.ts | 8 +- backend/src/services/FolderService.ts | 2 +- .../components/v2/Pagination/Pagination.tsx | 2 +- frontend/src/hooks/api/auditLogs/queries.tsx | 16 +- frontend/src/hooks/api/auditLogs/types.tsx | 2 + .../LogsPage/components/LogsFilter.tsx | 182 ++++++++++++------ .../LogsPage/components/LogsSection.tsx | 24 ++- .../Project/LogsPage/components/LogsTable.tsx | 49 +++-- .../Project/LogsPage/components/types.tsx | 17 +- 13 files changed, 235 insertions(+), 105 deletions(-) diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index a473f0c4a..48689be1c 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Key, Membership, MembershipOrg, User, IUser } from "../../models"; +import { IUser, Key, Membership, MembershipOrg, User } from "../../models"; import { EventType } from "../../ee/models"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { sendMail } from "../../helpers/nodemailer"; diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 742852343..334df6d4f 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -17,8 +17,8 @@ import { } from "../../services/FolderService"; import { ADMIN, MEMBER } from "../../variables"; import { validateMembership } from "../../helpers/membership"; -import { FolderVersion, EventType } from "../../ee/models"; -import { EESecretService, EEAuditLogService } from "../../ee/services"; +import { EventType, FolderVersion } from "../../ee/models"; +import { EEAuditLogService, EESecretService } from "../../ee/services"; // verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 20680f1c6..8cbf1a229 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -8,8 +8,8 @@ import { ServiceTokenData, Workspace, } from "../../models"; -import { SecretVersion, EventType } from "../../ee/models"; -import { EELicenseService, EEAuditLogService } from "../../ee/services"; +import { EventType, SecretVersion } from "../../ee/models"; +import { EEAuditLogService, EELicenseService } from "../../ee/services"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import _ from "lodash"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 4db8c4855..9354c4f13 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -4,6 +4,7 @@ import { Membership, Secret, ServiceTokenData, User } from "../../../models"; import { ActorType, AuditLog, + EventType, FolderVersion, IPType, ISecretVersion, @@ -13,14 +14,13 @@ import { ServiceActor, TFolderRootVersionSchema, TrustedIP, - UserActor, - EventType + UserActor } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import Folder, { TFolderSchema } from "../../../models/folder"; import { searchByFolderId } from "../../../services/FolderService"; -import { EELicenseService, EEAuditLogService } from "../../services"; +import { EEAuditLogService, EELicenseService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; /** @@ -611,7 +611,10 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { const offset: number = parseInt(req.query.offset as string); const limit: number = parseInt(req.query.limit as string); - const auditLogs = await AuditLog.find({ + const startDate = req.query.startDate as string; + const endDate = req.query.endDate as string; + + const query = { workspace: new Types.ObjectId(workspaceId), ...(eventType ? { "event.type": eventType @@ -626,14 +629,25 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { } : { "actor.metadata.serviceId": actor.split("-", 2)[1] }) + } : {}), + ...(startDate || endDate ? { + createdAt: { + ...(startDate && { $gte: new Date(startDate) }), + ...(endDate && { $lte: new Date(endDate) }) + } } : {}) - }) + } + + const auditLogs = await AuditLog.find(query) .sort({ createdAt: -1 }) .skip(offset) .limit(limit); - + + const totalCount = await AuditLog.countDocuments(query); + return res.status(200).send({ - auditLogs + auditLogs, + totalCount }); } diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 2143f0781..529ba0550 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -96,9 +96,11 @@ router.get( param("workspaceId").exists().trim(), query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), - query("actor").isString().optional({ nullable: true }), - query("offset").default("0"), - query("limit").default("20"), + query("actor").optional({ nullable: true }), + query("startDate").isISO8601().withMessage("Invalid start date format").optional({ nullable: true }), + query("endDate").isISO8601().withMessage("Invalid end date format").optional({ nullable: true }), + query("offset"), + query("limit"), validateRequest, workspaceController.getWorkspaceAuditLogs ); diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index 7d3e4bd59..8e1c00750 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -1,6 +1,6 @@ import { nanoid } from "nanoid"; import { Types } from "mongoose"; -import Folder, { TFolderSchema, TFolderRootSchema } from "../models/folder"; +import Folder, { TFolderRootSchema, TFolderSchema } from "../models/folder"; import { ResourceNotFoundError } from "../utils/errors"; type TAppendFolderDTO = { diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index 99d50a07f..f4a75eac7 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -21,7 +21,7 @@ export type PaginationProps = { perPage?: number; onChangePage: (pageNumber: number) => void; onChangePerPage: (newRows: number) => void; - className: string; + className?: string; perPageList?: number[]; }; diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 930de7f0a..94df8bf8b 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -31,15 +31,19 @@ export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) = params.append("actor", filters.actor); } - if (filters.actor) { - params.append("actor", filters.actor); + if (filters.startDate) { + params.append("startDate", filters.startDate.toISOString()); } - params.append("offset ", String(filters.offset)); - params.append("limit ", String(filters.limit)); + if (filters.endDate) { + params.append("endDate", filters.endDate.toISOString()); + } + + params.append("offset", String(filters.offset)); + params.append("limit", String(filters.limit)); - const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params }); - return data.auditLogs; + const { data } = await apiRequest.get<{ auditLogs: AuditLog[], totalCount: number }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params }); + return data; } }); } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index b7c3cc47a..71cf01c5c 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -323,4 +323,6 @@ export type AuditLogFilters = { actor?: string; offset: number; limit: number; + startDate?: Date; + endDate?: Date; } \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx b/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx index d1273f3aa..dce7c4938 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx @@ -1,18 +1,19 @@ +import { useState } from "react"; import { Control, Controller, UseFormReset } from "react-hook-form"; import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, + DatePicker, FormControl, Select, SelectItem} from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetAuditLogActorFilterOpts } from "@app/hooks/api"; - -import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants"; -import { ActorType } from "~/hooks/api/auditLogs/enums"; -import { Actor } from "~/hooks/api/auditLogs/types"; +import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; +import { ActorType } from "@app/hooks/api/auditLogs/enums"; +import { Actor } from "@app/hooks/api/auditLogs/types"; import { AuditLogFilterFormData } from "./types"; @@ -28,6 +29,9 @@ export const LogsFilter = ({ control, reset }: Props) => { + const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); + const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); + const { currentWorkspace } = useWorkspace(); const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? ""); @@ -58,65 +62,41 @@ export const LogsFilter = ({ return (
-
- ( - ( + + onChange(e)} - className="w-full" - > - {eventTypes.map(({ label, value }) => ( - - {label} - - ))} - - - )} - /> -
+ {eventTypes.map(({ label, value }) => ( + + {label} + + ))} + + + )} + /> {!isLoading && data && data.length > 0 && ( -
- ( - - - - )} - /> -
- )} -
( )} /> -
+ )} + ( + + + + )} + /> + { + return ( + + { + onChange(date); + setIsStartDatePickerOpen(false); + }} + popUpProps={{ + open: isStartDatePickerOpen, + onOpenChange: setIsStartDatePickerOpen + }} + popUpContentProps={{}} + /> + + ); + }} + /> + { + return ( + + { + onChange(date); + setIsEndDatePickerOpen(false); + }} + popUpProps={{ + open: isEndDatePickerOpen, + onOpenChange: setIsEndDatePickerOpen + }} + popUpContentProps={{}} + /> + + ); + }} + />
-
- {subscription && ( - handlePopUpClose("upgradePlan")} - text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."} - /> - )} + return ( +
+ + {t("common.head-title", { title: t("billing.title") })} + + + +
- ); + ); } -Activity.requireAuth = true; +export default Logs; +Logs.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/pages/project/[id]/logs/index.tsx b/frontend/src/pages/project/[id]/logs/index.tsx index ba24ba88f..0c44abbef 100644 --- a/frontend/src/pages/project/[id]/logs/index.tsx +++ b/frontend/src/pages/project/[id]/logs/index.tsx @@ -1,23 +1,197 @@ +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Head from "next/head"; +import { useRouter } from "next/router"; -import { LogsPage } from "@app/views/Project/LogsPage"; +import Button from "@app/components/basic/buttons/Button"; +import EventFilter from "@app/components/basic/EventFilter"; +import { UpgradePlanModal } from "@app/components/v2"; +import { useSubscription } from "@app/context"; +import ActivitySideBar from "@app/ee/components/ActivitySideBar"; +import { usePopUp } from "@app/hooks/usePopUp"; -const Logs = () => { - const { t } = useTranslation(); +import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs"; +import ActivityTable from "../../../../ee/components/ActivityTable"; - return ( -
- - {t("common.head-title", { title: t("billing.title") })} - - - - -
- ); +interface LogData { + _id: string; + channel: string; + createdAt: string; + ipAddress: string; + user: { + email: string; + }; + serviceAccount?: { + string: string; + }, + serviceTokenData?: { + name: string; + } + actions: { + _id: string; + name: string; + payload: { + secretVersions: string[]; + }; + }[]; } -export default Logs; +interface PayloadProps { + _id: string; + name: string; + secretVersions: string[]; +} + +interface LogDataPoint { + _id: string; + channel: string; + createdAt: string; + ipAddress: string; + user: string; + serviceAccount: { + name: string; + }; + serviceTokenData: { + name: string; + }; + payload: PayloadProps[]; +} + +/** + * This is the tab that includes all of the user activity logs + */ +export default function Activity() { + const router = useRouter(); + const [eventChosen, setEventChosen] = useState(""); + const [logsData, setLogsData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [currentOffset, setCurrentOffset] = useState(0); + const currentLimit = 10; + const [currentSidebarAction, toggleSidebar] = useState(); + const { t } = useTranslation(); + const { subscription } = useSubscription(); + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "upgradePlan" + ] as const); + + // this use effect updates the data in case of a new filter being added + useEffect(() => { + setCurrentOffset(0); + const getLogData = async () => { + setIsLoading(true); + const tempLogsData = await getProjectLogs({ + workspaceId: String(router.query.id), + offset: 0, + limit: currentLimit, + userId: "", + actionNames: eventChosen + }); + + setLogsData( + tempLogsData.map((log: LogData) => ({ + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log?.user?.email, + serviceAccount: log?.serviceAccount, + serviceTokenData: log?.serviceTokenData, + payload: log.actions.map((action) => ({ + _id: action._id, + name: action.name, + secretVersions: action.payload.secretVersions + })) + })) + ); + setIsLoading(false); + }; + getLogData(); + }, [eventChosen]); + + // this use effect adds more data in case 'View More' button is clicked + useEffect(() => { + const getLogData = async () => { + setIsLoading(true); + const tempLogsData = await getProjectLogs({ + workspaceId: String(router.query.id), + offset: currentOffset, + limit: currentLimit, + userId: "", + actionNames: eventChosen + }); + setLogsData( + logsData.concat( + tempLogsData.map((log: LogData) => ({ + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log?.user?.email, + serviceAccount: log?.serviceAccount, + serviceTokenData: log?.serviceTokenData, + payload: log.actions.map((action) => ({ + _id: action._id, + name: action.name, + secretVersions: action.payload.secretVersions + })) + })) + ) + ); + setIsLoading(false); + }; + getLogData(); + }, [currentLimit, currentOffset]); + + const loadMoreLogs = () => { + if (subscription?.auditLogs === false) { + handlePopUpOpen("upgradePlan"); + } else { + setCurrentOffset(currentOffset + currentLimit); + } + }; + + return ( +
+ + Audit Logs + + + + {currentSidebarAction && ( + + )} +
+
+

{t("activity.title")}

+
+

{t("activity.subtitle")}

+
+
+ +
+ +
+
+
+
+ {subscription && ( + handlePopUpClose("upgradePlan")} + text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."} + /> + )} +
+ ); +} + +Activity.requireAuth = true; -Logs.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx index ab5c8719f..182eea8f4 100644 --- a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx +++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx @@ -27,7 +27,7 @@ type Props = { onEnvCompare: (secretKey: string) => void; secretVersion?: Array<{ id: string; createdAt: string; value: string }>; // to record the ids of deleted ones - onSecretDelete: (index: number, id?: string, overrideId?: string) => void; + onSecretDelete: (index: number, secretName: string, id?: string, overrideId?: string) => void; onSave: () => void; }; @@ -45,6 +45,15 @@ export const SecretDetailDrawer = ({ const [canRevealSecOverride, setCanRevealSecOverride] = useToggle(); const { register, setValue, control, getValues } = useFormContext(); + + const secKey = useWatch({ + control, + name: `secrets.${index}.key`, + disabled: false, + exact: true + }); + + console.log("secKeyyy", secKey); const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` }); const isOverridden = diff --git a/frontend/src/views/Project/LogsPage/LogsPage.tsx b/frontend/src/views/Project/AuditLogsPage/AuditLogsPage.tsx similarity index 90% rename from frontend/src/views/Project/LogsPage/LogsPage.tsx rename to frontend/src/views/Project/AuditLogsPage/AuditLogsPage.tsx index c17fd7092..0a192110b 100644 --- a/frontend/src/views/Project/LogsPage/LogsPage.tsx +++ b/frontend/src/views/Project/AuditLogsPage/AuditLogsPage.tsx @@ -2,7 +2,7 @@ import { LogsSection } from "./components"; -export const LogsPage = () => { +export const AuditLogsPage = () => { return (
diff --git a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx similarity index 97% rename from frontend/src/views/Project/LogsPage/components/LogsFilter.tsx rename to frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx index 88523e012..9b2be6f4c 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsFilter.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx @@ -73,7 +73,8 @@ export const LogsFilter = ({ className="w-40 mr-4" > -
- {serviceToken} -
-
- - - {t("common.click-to-copy")} - -
-
-
-
-
- - )} - -
-
- - -
- ); -}; - -export default AddServiceTokenDialog; diff --git a/frontend/src/components/basic/table/EnvironmentsTable.tsx b/frontend/src/components/basic/table/EnvironmentsTable.tsx deleted file mode 100644 index b9c94534a..000000000 --- a/frontend/src/components/basic/table/EnvironmentsTable.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { useEffect, useState } from "react"; -import { faPencil, faPlus, faX } from "@fortawesome/free-solid-svg-icons"; -import { plans } from "public/data/frequentConstants"; - -import { usePopUp } from "../../../hooks/usePopUp"; -import getOrganizationSubscriptions from "../../../pages/api/organization/GetOrgSubscription"; -import Button from "../buttons/Button"; -import { AddUpdateEnvironmentDialog } from "../dialog/AddUpdateEnvironmentDialog"; -import DeleteActionModal from "../dialog/DeleteActionModal"; -import UpgradePlanModal from "../dialog/UpgradePlan"; - -type Env = { name: string; slug: string }; - -type Props = { - data: Env[]; - onCreateEnv: (arg0: Env) => Promise; - onUpdateEnv: (oldSlug: string, arg0: Env) => Promise; - onDeleteEnv: (slug: string) => Promise; -}; - -const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => { - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "createUpdateEnv", - "deleteEnv", - "upgradePlan" - ] as const); - const [plan, setPlan] = useState(""); - const host = window.location.origin; - - useEffect(() => { - // on initial load - run auth check - (async () => { - const orgId = localStorage.getItem("orgData.id") as string; - const subscriptions = await getOrganizationSubscriptions({ - orgId - }); - if (subscriptions) { - setPlan(subscriptions.data[0].plan.product) - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onEnvCreateCB = async (env: Env) => { - try { - await onCreateEnv(env); - handlePopUpClose("createUpdateEnv"); - } catch (error) { - console.error(error); - } - }; - - const onEnvUpdateCB = async (env: Env) => { - try { - await onUpdateEnv((popUp.createUpdateEnv?.data as Pick)?.slug, env); - handlePopUpClose("createUpdateEnv"); - } catch (error) { - console.error(error); - } - }; - - const onEnvDeleteCB = async () => { - try { - await onDeleteEnv((popUp.deleteEnv?.data as Pick)?.slug); - handlePopUpClose("deleteEnv"); - } catch (error) { - console.error(error); - } - }; - - return ( - <> -
-
-

Project Environments

-

- Choose which environments will show up in your dashboard like development, staging, - production -

-

- Note: the text in slugs shows how these environmant should be accessed in CLI. -

-
-
-
-
-
-
- - - - - - - - - {data?.length > 0 ? ( - data.map(({ name, slug }) => ( - - - - - - )) - ) : ( - - - - )} - -
NameSlug -
{name}{slug} -
-
-
-
-
- No environments found -
- handlePopUpClose("deleteEnv")} - onSubmit={onEnvDeleteCB} - /> - handlePopUpClose("createUpdateEnv")} - onCreateSubmit={onEnvCreateCB} - onEditSubmit={onEnvUpdateCB} - /> - handlePopUpClose("upgradePlan")} - text="You can add custom environments if you switch to Infisical's Team plan." - /> -
- - ); -}; - -export default EnvironmentTable; diff --git a/frontend/src/components/basic/table/ServiceTokenTable.tsx b/frontend/src/components/basic/table/ServiceTokenTable.tsx deleted file mode 100644 index 9c02c059c..000000000 --- a/frontend/src/components/basic/table/ServiceTokenTable.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { faX } from "@fortawesome/free-solid-svg-icons"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; - -import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; -import guidGenerator from "../../utilities/randomId"; -import Button from "../buttons/Button"; - -interface TokenProps { - _id: string; - name: string; - environment: string; - expiresAt: string; -} - -interface ServiceTokensProps { - data: TokenProps[]; - workspaceName: string; - setServiceTokens: (value: TokenProps[]) => void; -} - -/** - * This is the component that we utilize for the service token table - * #TODO: add the possibility of choosing and doing operations on multiple users. - * @param {object} obj - * @param {any[]} obj.data - current state of the service token table - * @param {string} obj.workspaceName - name of the current project - * @param {function} obj.setServiceTokens - updating the state of the service token table - * @returns - */ -const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => { - const { createNotification } = useNotificationContext(); - - return ( -
-
- - - - - - - - - - - {data?.length > 0 ? ( - data?.map((row) => ( - - - - - - - - )) - ) : ( - - - - )} - -
TOKEN NAMEPROJECTENVIRONMENTVAILD UNTIL -
- {row.name} - - {workspaceName} - - {row.environment} - - {new Date(row.expiresAt).toUTCString()} - -
-
-
- No service tokens yet -
-
- ); -}; - -export default ServiceTokenTable; diff --git a/frontend/src/components/basic/table/UserTable.tsx b/frontend/src/components/basic/table/UserTable.tsx deleted file mode 100644 index f029ae4f0..000000000 --- a/frontend/src/components/basic/table/UserTable.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { faX } from "@fortawesome/free-solid-svg-icons"; - -import changeUserRoleInOrganization from "@app/pages/api/organization/changeUserRoleInOrganization"; -import deleteUserFromOrganization from "@app/pages/api/organization/deleteUserFromOrganization"; -import getOrganizationProjectMemberships from "@app/pages/api/organization/GetOrgProjectMemberships"; -import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; -import uploadKeys from "@app/pages/api/workspace/uploadKeys"; - -import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto"; -import guidGenerator from "../../utilities/randomId"; -import Button from "../buttons/Button"; -import Listbox from "../Listbox"; - -// const roles = ['admin', 'user']; -// TODO: Set type for this -type Props = { - userData: any[]; - changeData: (users: any[]) => void; - myUser: string; - filter: string; - resendInvite: (email: string) => void; - isOrg: boolean; -}; - -/** - * This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. - * #TODO: add the possibility of choosing and doing operations on multiple users. - * @param {*} props - * @returns - */ -const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }: Props) => { - const [roleSelected, setRoleSelected] = useState( - Array(userData?.length).fill(userData.map((user) => user.role)) - ); - const router = useRouter(); - const [myRole, setMyRole] = useState("member"); - const [userProjectMemberships, setUserProjectMemberships] = useState([]); - - const workspaceId = router.query.id as string; - // Delete the row in the table (e.g. a user) - // #TODO: Add a pop-up that warns you that the user is going to be deleted. - const handleDelete = (membershipId: string, index: number) => { - // setUserIdToBeDeleted(userId); - // onClick(); - if (isOrg) { - deleteUserFromOrganization(membershipId); - } else { - deleteUserFromWorkspace(membershipId); - } - changeData(userData.filter((v, i) => i !== index)); - setRoleSelected([ - ...roleSelected.slice(0, index), - ...roleSelected.slice(index + 1, userData?.length) - ]); - }; - - // Update the role of a certain user - const handleRoleUpdate = (index: number, e: string) => { - changeUserRoleInOrganization(String(localStorage.getItem("orgData.id")), userData[index].membershipId, e); - changeData([ - ...userData.slice(0, index), - ...[ - { - key: userData[index].key, - firstName: userData[index].firstName, - lastName: userData[index].lastName, - email: userData[index].email, - role: e, - status: userData[index].status, - userId: userData[index].userId, - membershipId: userData[index].membershipId, - publicKey: userData[index].publicKey - } - ], - ...userData.slice(index + 1, userData?.length) - ]); - }; - - useEffect(() => { - setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); - (async () => { - const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))}) - setUserProjectMemberships(result); - })(); - }, [userData, myUser]); - - const grantAccess = async (id: string, publicKey: string) => { - const result = await getLatestFileKey({ workspaceId }); - - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: result.latestKey.encryptedKey, - nonce: result.latestKey.nonce, - publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: PRIVATE_KEY - }); - - uploadKeys(workspaceId, id, ciphertext, nonce); - router.reload(); - }; - - const deleteMembershipAndResendInvite = (email: string) => { - // deleteUserFromWorkspace(membershipId); - resendInvite(email); - }; - - return ( -
-
- - - - - - - - - - - {userData?.filter( - (user) => - user.firstName?.toLowerCase().includes(filter) || - user.lastName?.toLowerCase().includes(filter) || - user.email?.toLowerCase().includes(filter) - ).length > 0 && - userData - ?.filter( - (user) => - user.firstName?.toLowerCase().includes(filter) || - user.lastName?.toLowerCase().includes(filter) || - user.email?.toLowerCase().includes(filter) - ) - .map((row, index) => ( - - - - - - - - ))} - -
NAMEEMAILROLEPROJECTS -
- {row.firstName} {row.lastName} - - {row.email} - -
- {row.status === "accepted" && - ((myRole === "admin" && row.role !== "owner") || myRole === "owner") && - (myUser !== row.email) ? ( - handleRoleUpdate(index, e)} - data={ - myRole === "owner" ? ["owner", "admin", "member"] : ["admin", "member"] - } - /> - ) : ( - row.status !== "invited" && - row.status !== "verified" && ( - { - throw new Error("Function not implemented."); - }} - data={null} - /> - ) - )} - {(row.status === "invited" || row.status === "verified") && ( -
-
- )} - {row.status === "completed" && myUser !== row.email && ( -
-
- )} -
-
-
- {userProjectMemberships[row.userId] - ? userProjectMemberships[row.userId]?.map((project: any) => ( -
- {project.name} -
- )) - : This user isn't part of any projects yet.} -
-
- {myUser !== row.email && - // row.role !== "admin" && - myRole !== "member" ? ( -
-
- ) : ( -
- )} -
-
- ); -}; - -export default UserTable; diff --git a/frontend/src/components/dashboard/CompareSecretsModal.tsx b/frontend/src/components/dashboard/CompareSecretsModal.tsx deleted file mode 100644 index bf0575e09..000000000 --- a/frontend/src/components/dashboard/CompareSecretsModal.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { SetStateAction, useEffect, useState } from "react"; -import Image from "next/image"; - -import { WorkspaceEnv } from "@app/hooks/api/types"; - -import getSecretsForProject from "../utilities/secrets/getSecretsForProject"; -import { Modal, ModalContent } from "../v2"; - -interface Secrets { - label: string; - secret: string; -} - -interface CompareSecretsModalProps { - compareModal: boolean; - setCompareModal: React.Dispatch>; - selectedEnv: WorkspaceEnv; - workspaceEnvs: WorkspaceEnv[]; - workspaceId: string; - currentSecret: { - key: string; - value: string; - }; -} - -const CompareSecretsModal = ({ - compareModal, - setCompareModal, - selectedEnv, - workspaceEnvs, - workspaceId, - currentSecret -}: CompareSecretsModalProps) => { - const [secrets, setSecrets] = useState([]); - - const getEnvSecrets = async () => { - const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv); - const newSecrets = await Promise.all( - workspaceEnvironments.map(async (env) => { - // #TODO: optimize this query somehow... - const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId }); - const secret = - allSecrets.find((item) => item.key === currentSecret.key)?.value ?? "Not found"; - return { label: env.name, secret }; - }) - ); - setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]); - }; - - useEffect(() => { - if (compareModal) { - (async () => { - await getEnvSecrets(); - })(); - } - }, [compareModal]); - - return ( - - e.preventDefault()}> -
- {secrets.length === 0 ? ( -
- infisical loading indicator -
- ) : ( - secrets.map((item) => ( -
-

{item.label}

- -
- )) - )} -
-
-
- ); -}; -export default CompareSecretsModal; diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx deleted file mode 100644 index 48a080a3a..000000000 --- a/frontend/src/components/dashboard/SideBar.tsx +++ /dev/null @@ -1,222 +0,0 @@ -/* eslint-disable react/no-unused-prop-types */ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import Image from "next/image"; -import { faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import SecretVersionList from "@app/ee/components/SecretVersionList"; -import { WorkspaceEnv } from "@app/hooks/api/types"; - -import Button from "../basic/buttons/Button"; -import Toggle from "../basic/Toggle"; -import CommentField from "./CommentField"; -import CompareSecretsModal from "./CompareSecretsModal"; -import DashboardInputField from "./DashboardInputField"; -import { DeleteActionButton } from "./DeleteActionButton"; -import GenerateSecretMenu from "./GenerateSecretMenu"; - -interface SecretProps { - key: string; - value: string | undefined; - valueOverride: string | undefined; - pos: number; - id: string; - comment: string; -} - -export interface DeleteRowFunctionProps { - ids: string[]; - secretName: string; -} - -interface SideBarProps { - toggleSidebar: (value: string) => void; - data: SecretProps[]; - modifyKey: (value: string, id: string) => void; - modifyValue: (value: string, id: string) => void; - modifyValueOverride: (value: string | undefined, id: string) => void; - modifyComment: (value: string, id: string) => void; - buttonReady: boolean; - savePush: () => void; - sharedToHide: string[]; - setSharedToHide: (values: string[]) => void; - deleteRow: (props: DeleteRowFunctionProps) => void; - workspaceEnvs: WorkspaceEnv[]; - selectedEnv: WorkspaceEnv; - workspaceId: string; -} - -/** - * @param {object} obj - * @param {function} obj.toggleSidebar - function that opens or closes the sidebar - * @param {SecretProps[]} obj.data - data of a certain key valeu pair - * @param {function} obj.modifyKey - function that modifies the secret key - * @param {function} obj.modifyValue - function that modifies the secret value - * @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override - * @param {boolean} obj.buttonReady - is the button for saving chagnes active - * @param {function} obj.savePush - save changes andp ush secrets - * @param {function} obj.deleteRow - a function to delete a certain keyPair - * @returns the sidebar with 'secret's settings' - */ -const SideBar = ({ - toggleSidebar, - data, - modifyKey, - modifyValue, - modifyValueOverride, - modifyComment, - buttonReady, - savePush, - deleteRow, - workspaceEnvs, - selectedEnv, - workspaceId -}: SideBarProps) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [isLoading, setIsLoading] = useState(false); - const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined); - const [compareModal, setCompareModal] = useState(false); - const { t } = useTranslation(); - - return ( -
- {isLoading ? ( -
- infisical loading indicator -
- ) : ( -
-
-

{t("dashboard.sidebar.secret")}

-
null} - role="button" - tabIndex={0} - className="p-1" - onClick={() => toggleSidebar("None")} - > - -
-
-
-

{t("dashboard.sidebar.key")}

-
- -
-
- {data[0]?.value || data[0]?.value === "" ? ( -
-

{t("dashboard.sidebar.value")}

-
- -
-
- -
-
- ) : ( -
- - {t("common.note")}: - - {t("dashboard.sidebar.personal-explanation")} -
- )} -
- {(data[0]?.value || data[0]?.value === "") && ( -
-

{t("dashboard.sidebar.override")}

- -
- )} -
-
- -
-
- -
-
-
- - -
- )} -
-
-
-
-
-
-
- ); -}; - -export default SideBar; diff --git a/frontend/src/components/utilities/secrets/getSecretsForProject.ts b/frontend/src/components/utilities/secrets/getSecretsForProject.ts deleted file mode 100644 index de1a75369..000000000 --- a/frontend/src/components/utilities/secrets/getSecretsForProject.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Tag } from "public/data/frequentInterfaces"; - -import getSecrets from "@app/pages/api/files/GetSecrets"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; - -import { decryptAssymmetric, decryptSymmetric } from "../cryptography/crypto"; - -interface EncryptedSecretProps { - _id: string; - createdAt: string; - environment: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - type: "personal" | "shared"; - tags: Tag[]; -} - -interface SecretProps { - key: string; - value: string | undefined; - type: "personal" | "shared"; - comment: string; - id: string; - tags: Tag[]; -} - -interface FunctionProps { - env: string; - setIsKeyAvailable?: any; - setData?: any; - workspaceId: string; -} - -/** - * Gets the secrets for a certain project - * @param {object} obj - * @param {string} obj.env - environment for which we are getting secrets - * @param {boolean} obj.isKeyAvailable - if a person is able to create new key pairs - * @param {function} obj.setData - state function that manages the state of secrets in the dashboard - * @param {string} obj.workspaceId - id of a workspace for which we are getting secrets - */ -const getSecretsForProject = async ({ - env, - setIsKeyAvailable, - setData, - workspaceId -}: FunctionProps) => { - try { - let encryptedSecrets; - try { - encryptedSecrets = await getSecrets(workspaceId, env); - } catch (error) { - console.log("ERROR: Not able to access the latest version of secrets"); - } - - const latestKey = await getLatestFileKey({ workspaceId }); - // This is called isKeyAvailable but what it really means is if a person is able to create new key pairs - if (typeof setIsKeyAvailable === "function") { - setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true); - } - - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - const tempDecryptedSecrets: SecretProps[] = []; - if (latestKey) { - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: latestKey.latestKey.encryptedKey, - nonce: latestKey.latestKey.nonce, - publicKey: latestKey.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - // decrypt secret keys, values, and comments - encryptedSecrets.forEach((secret: EncryptedSecretProps) => { - const plainTextKey = decryptSymmetric({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key - }); - - let plainTextValue; - if (secret.secretValueCiphertext !== undefined) { - plainTextValue = decryptSymmetric({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); - } else { - plainTextValue = undefined; - } - - let plainTextComment; - if (secret.secretCommentCiphertext) { - plainTextComment = decryptSymmetric({ - ciphertext: secret.secretCommentCiphertext, - iv: secret.secretCommentIV, - tag: secret.secretCommentTag, - key - }); - } else { - plainTextComment = ""; - } - - tempDecryptedSecrets.push({ - id: secret._id, - key: plainTextKey, - value: plainTextValue, - type: secret.type, - comment: plainTextComment, - tags: secret.tags - }); - }); - } - - const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))]; - - const result = secretKeys.map((key, index) => ({ - id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === "shared")[0] - ?.id, - idOverride: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === "personal" - )[0]?.id, - pos: index, - key, - value: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === "shared" - )[0]?.value, - valueOverride: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === "personal" - )[0]?.value, - comment: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === "shared" - )[0]?.comment, - tags: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === "shared" - )[0]?.tags - })); - - if (typeof setData === "function") { - setData(result); - } - - return result; - } catch (error) { - console.log("Something went wrong during accessing or decripting secrets."); - } - return []; -}; - -export default getSecretsForProject; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 69fd7fe38..0c0878fa1 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,6 +1,6 @@ import { createContext, ReactNode, useContext, useMemo } from "react"; -import { useGetOrganization } from "@app/hooks/api"; +import { useGetOrganizations } from "@app/hooks/api"; import { Organization } from "@app/hooks/api/types"; @@ -17,8 +17,8 @@ type Props = { }; export const OrgProvider = ({ children }: Props): JSX.Element => { - const { data: userOrgs, isLoading } = useGetOrganization(); - + const { data: userOrgs, isLoading } = useGetOrganizations(); + // const currentWsOrgID = currentWorkspace?.organization; const currentWsOrgID = localStorage.getItem("orgData.id"); diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 30bd996d9..3b3dddf5a 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -2,9 +2,9 @@ import crypto from "crypto"; import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets"; -import addSecrets from "@app/pages/api/files/AddSecrets"; +import { createSecret } from "@app/hooks/api/secrets/queries"; +import { createWorkspace } from "@app/hooks/api/workspace/queries"; import getUser from "@app/pages/api/user/getUser"; -import createWorkspace from "@app/pages/api/workspace/createWorkspace"; import uploadKeys from "@app/pages/api/workspace/uploadKeys"; const secretsToBeAdded = [ @@ -95,10 +95,12 @@ const initProjectHelper = async ({ try { // create new project - project = await createWorkspace({ + const { data: { workspace } } = await createWorkspace({ workspaceName: projectName, organizationId }); + + project = workspace; // create and upload new (encrypted) project key const randomBytes = crypto.randomBytes(16).toString("hex"); @@ -116,19 +118,33 @@ const initProjectHelper = async ({ await uploadKeys(project._id, user._id, ciphertext, nonce); + const workspaceId = project._id; + // encrypt and upload secrets to new project const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, - workspaceId: project._id, + workspaceId, env: "dev" }); - - await addSecrets({ - secrets: secrets ?? [], - env: "dev", - workspaceId: project._id + + secrets?.forEach((secret) => { + createSecret({ + workspaceId, + environment: secret.environment, + type: secret.type, + secretKey: secret.secretName, + secretKeyCiphertext: secret.secretKeyCiphertext, + secretKeyIV: secret.secretKeyIV, + secretKeyTag: secret.secretKeyTag, + secretValueCiphertext: secret.secretValueCiphertext, + secretValueIV: secret.secretValueIV, + secretValueTag: secret.secretValueTag, + secretCommentCiphertext: secret.secretCommentCiphertext, + secretCommentIV: secret.secretCommentIV, + secretCommentTag: secret.secretCommentTag, + secretPath: "/" + }); }); - } catch (err) { console.error("Failed to init project in organization", err); } diff --git a/frontend/src/hooks/api/bots/queries.tsx b/frontend/src/hooks/api/bots/queries.tsx index 1c28c9640..3e35a58f0 100644 --- a/frontend/src/hooks/api/bots/queries.tsx +++ b/frontend/src/hooks/api/bots/queries.tsx @@ -8,29 +8,26 @@ const queryKeys = { getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const }; -const fetchWorkspaceBot = async (workspaceId: string) => { - const { data } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`); - return data.bot; -}; - export const useGetWorkspaceBot = (workspaceId: string) => useQuery({ queryKey: queryKeys.getBot(workspaceId), - queryFn: () => fetchWorkspaceBot(workspaceId), + queryFn: async () => { + const { data: { bot } } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`); + return bot; + }, enabled: Boolean(workspaceId) }); -// mutation - export const useUpdateBotActiveStatus = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TSetBotActiveStatusDto>({ - mutationFn: ({ botId, isActive, botKey }) => - apiRequest.patch(`/api/v1/bot/${botId}/active`, { + mutationFn: ({ botId, isActive, botKey }) => { + return apiRequest.patch(`/api/v1/bot/${botId}/active`, { isActive, botKey - }), + }); + }, onSuccess: (_, { workspaceId }) => { queryClient.invalidateQueries(queryKeys.getBot(workspaceId)); } diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index ded8c3f29..7134d0501 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -4,7 +4,7 @@ export { useCreateCustomerPortalSession, useDeleteOrgPmtMethod, useDeleteOrgTaxId, - useGetOrganization, + useGetOrganizations, useGetOrgBillingDetails, useGetOrgInvoices, useGetOrgLicenses, diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 66065c6ef..f7bd3e973 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -12,10 +12,11 @@ import { PmtMethod, ProductsTable, RenameOrgDTO, - TaxID} from "./types"; + TaxID +} from "./types"; const organizationKeys = { - getUserOrganization: ["organization"] as const, + getUserOrganizations: ["organization"] as const, getOrgPlanBillingInfo: (orgId: string) => [{ orgId }, "organization-plan-billing"] as const, getOrgPlanTable: (orgId: string) => [{ orgId }, "organization-plan-table"] as const, getOrgPlansTable: (orgId: string, billingCycle: "monthly" | "yearly") => [{ orgId, billingCycle }, "organization-plans-table"] as const, @@ -26,13 +27,12 @@ const organizationKeys = { getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const }; -export const useGetOrganization = () => { +export const useGetOrganizations = () => { return useQuery({ - queryKey: organizationKeys.getUserOrganization, + queryKey: organizationKeys.getUserOrganizations, queryFn: async () => { - const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); - - return data.organizations; + const { data: { organizations } } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); + return organizations; } }); } @@ -44,7 +44,7 @@ export const useRenameOrg = () => { mutationFn: ({ newOrgName, orgId }) => apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }), onSuccess: () => { - queryClient.invalidateQueries(organizationKeys.getUserOrganization); + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); } }); }; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 6f65a8edd..e1c3e10bf 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -11,13 +11,13 @@ import { apiRequest } from "@app/config/request"; import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { BatchSecretDTO, + CreateSecretDTO, DecryptedSecret, EncryptedSecret, EncryptedSecretVersion, GetProjectSecretsDTO, GetSecretVersionsDTO, - TGetProjectSecretsAllEnvDTO -} from "./types"; + TGetProjectSecretsAllEnvDTO} from "./types"; export const secretKeys = { // this is also used in secretSnapshot part @@ -324,3 +324,24 @@ export const useBatchSecretsOp = () => { } }); }; + +export const createSecret = async (dto: CreateSecretDTO) => { + const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto); + return data; +} + +export const useCreateSecret = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, CreateSecretDTO>({ + mutationFn: async (dto) => { + const data = createSecret(dto); + return data; + }, + onSuccess: (_, dto) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(dto.workspaceId, dto.environment) + ); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index c317da2c9..964bac6c2 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -147,3 +147,22 @@ export type TDeleteSecretsV3DTO = { secretPath: string; secretName: string; }; + +// --- v3 + +export type CreateSecretDTO = { + workspaceId: string; + environment: string; + type: "shared" | "personal"; + secretKey: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; + secretPath: string; +} \ No newline at end of file diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 8cabdb9b6..4a0241cdf 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -23,12 +23,13 @@ const fetchWorkspaceServiceTokens = async (workspaceID: string) => { type UseGetWorkspaceServiceTokensProps = { workspaceID: string }; -export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) => - useQuery({ +export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) => { + return useQuery({ queryKey: serviceTokenKeys.getAllWorkspaceServiceToken(workspaceID), queryFn: () => fetchWorkspaceServiceTokens(workspaceID), enabled: Boolean(workspaceID) }); +} // mutation export const useCreateServiceToken = () => { @@ -36,6 +37,7 @@ export const useCreateServiceToken = () => { return useMutation({ mutationFn: async (body) => { + console.log("useCreateServiceToken"); const { data } = await apiRequest.post("/api/v2/service-token/", body); data.serviceToken += `.${body.randomBytes}`; return data; @@ -51,6 +53,7 @@ export const useDeleteServiceToken = () => { return useMutation({ mutationFn: async (serviceTokenId) => { + console.log("useDeleteServiceToken"); const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`); return data; }, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 9833c06fd..860ee7757 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -166,13 +166,21 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => enabled: Boolean(workspaceId) }); -// mutation +export const createWorkspace = ({ + organizationId, + workspaceName +}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => { + return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId }); +} + export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ organizationId, workspaceName }) => - apiRequest.post("/api/v1/workspace", { workspaceName, organizationId }), + mutationFn: async ({ organizationId, workspaceName }) => createWorkspace({ + organizationId, + workspaceName + }), onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } @@ -183,8 +191,9 @@ export const useRenameWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, RenameWorkspaceDTO>({ - mutationFn: ({ workspaceID, newWorkspaceName }) => - apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName }), + mutationFn: ({ workspaceID, newWorkspaceName }) => { + return apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName }); + }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } @@ -220,11 +229,12 @@ export const useCreateWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, CreateEnvironmentDTO>({ - mutationFn: ({ workspaceID, environmentName, environmentSlug }) => - apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, { + mutationFn: ({ workspaceID, environmentName, environmentSlug }) => { + return apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, { environmentName, environmentSlug - }), + }); + }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } @@ -235,12 +245,13 @@ export const useUpdateWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, UpdateEnvironmentDTO>({ - mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) => - apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, { + mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) => { + return apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, { environmentName, environmentSlug, oldEnvironmentSlug - }), + }); + }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } @@ -251,10 +262,11 @@ export const useDeleteWsEnvironment = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, DeleteEnvironmentDTO>({ - mutationFn: ({ workspaceID, environmentSlug }) => - apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, { + mutationFn: ({ workspaceID, environmentSlug }) => { + return apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, { data: { environmentSlug } - }), + }); + }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } diff --git a/frontend/src/pages/api/bot/getBot.ts b/frontend/src/pages/api/bot/getBot.ts deleted file mode 100644 index 2579db67f..000000000 --- a/frontend/src/pages/api/bot/getBot.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - workspaceId: string; -} - -/** - * This function fetches the bot for a project - * @param {Object} obj - * @param {String} obj.workspaceId - * @returns - */ -const getBot = async ({ workspaceId }: Props) => - SecurityClient.fetchCall(`/api/v1/bot/${workspaceId}`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).bot; - } - console.log("Failed to get bot for project"); - return undefined; - }); - -export default getBot; diff --git a/frontend/src/pages/api/bot/setBotActiveStatus.ts b/frontend/src/pages/api/bot/setBotActiveStatus.ts deleted file mode 100644 index 68852b50c..000000000 --- a/frontend/src/pages/api/bot/setBotActiveStatus.ts +++ /dev/null @@ -1,42 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface BotKey { - encryptedKey: string; - nonce: string; -} - -interface Props { - botId: string; - isActive: boolean; - botKey?: BotKey; -} - -/** - * This function sets the active status of a bot and shares a copy of - * the project key (encrypted under the bot's public key) with the - * project's bot - * @param {Object} obj - * @param {String} obj.botId - * @param {String} obj.isActive - * @param {Object} obj.botKey - * @returns - */ -const setBotActiveStatus = async ({ botId, isActive, botKey }: Props) => - SecurityClient.fetchCall(`/api/v1/bot/${botId}/active`, { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - isActive, - botKey - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to get bot for project"); - return undefined; - }); - -export default setBotActiveStatus; diff --git a/frontend/src/pages/api/environments/createEnvironment.ts b/frontend/src/pages/api/environments/createEnvironment.ts deleted file mode 100644 index 52e7562c9..000000000 --- a/frontend/src/pages/api/environments/createEnvironment.ts +++ /dev/null @@ -1,28 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -type NewEnvironmentInfo = { - environmentSlug: string; - environmentName: string; -}; - -/** - * This route deletes a specified workspace. - * @param {*} workspaceId - * @returns - */ -const createEnvironment = (workspaceId: string, newEnv: NewEnvironmentInfo) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(newEnv) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to create environment"); - return undefined; - }); - -export default createEnvironment; diff --git a/frontend/src/pages/api/environments/deleteEnvironment.ts b/frontend/src/pages/api/environments/deleteEnvironment.ts deleted file mode 100644 index d94f411f7..000000000 --- a/frontend/src/pages/api/environments/deleteEnvironment.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; -/** - * This route deletes a specified env. - * @param {*} workspaceId - * @returns - */ -const deleteEnvironment = (workspaceId: string, environmentSlug: string) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ environmentSlug }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to delete environment"); - return undefined; - }); - -export default deleteEnvironment; diff --git a/frontend/src/pages/api/environments/updateEnvironment.ts b/frontend/src/pages/api/environments/updateEnvironment.ts deleted file mode 100644 index 2e671de98..000000000 --- a/frontend/src/pages/api/environments/updateEnvironment.ts +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -type EnvironmentInfo = { - oldEnvironmentSlug: string; - environmentSlug: string; - environmentName: string; -}; - -/** - * This route updates a specified environment. - * @param {*} workspaceId - * @returns - */ -const updateEnvironment = (workspaceId: string, env: EnvironmentInfo) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, { - method: "PUT", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(env) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to update environment"); - return undefined; - }); - -export default updateEnvironment; diff --git a/frontend/src/pages/api/files/AddSecrets.ts b/frontend/src/pages/api/files/AddSecrets.ts deleted file mode 100644 index fe23aa3a0..000000000 --- a/frontend/src/pages/api/files/AddSecrets.ts +++ /dev/null @@ -1,54 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface EncryptedSecretProps { - id: string; - createdAt: string; - environment: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - type: "personal" | "shared"; -} - -/** - * This function adds secrets to a certain project - * @param {object} obj - * @param {EncryptedSecretProps} obj.secrets - the ids of secrets that we want to add - * @param {string} obj.env - the environment to which we are adding secrets - * @param {string} obj.workspaceId - the project to which we are adding secrets - * @returns - */ -const addSecrets = async ({ - secrets, - env, - workspaceId -}: { - secrets: EncryptedSecretProps[]; - env: string; - workspaceId: string; -}) => - SecurityClient.fetchCall("/api/v2/secrets", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - environment: env, - workspaceId, - secrets - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to add certain project secrets"); - return undefined; - }); - -export default addSecrets; diff --git a/frontend/src/pages/api/files/DeleteSecrets.ts b/frontend/src/pages/api/files/DeleteSecrets.ts deleted file mode 100644 index 6ea22b8e4..000000000 --- a/frontend/src/pages/api/files/DeleteSecrets.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function deletes certain secrets from a certain project - * @param {string[]} secretIds - the ids of secrets that we want to be deleted - * @returns - */ -const deleteSecrets = async ({ secretIds }: { secretIds: string[] }) => - SecurityClient.fetchCall("/api/v2/secrets", { - method: "DELETE", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - secretIds - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to delete certain project secrets"); - return undefined; - }); - -export default deleteSecrets; diff --git a/frontend/src/pages/api/files/GetSecrets.ts b/frontend/src/pages/api/files/GetSecrets.ts deleted file mode 100644 index d8e8caa40..000000000 --- a/frontend/src/pages/api/files/GetSecrets.ts +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function fetches the encrypted secrets for a certain project - * @param {string} workspaceId - project is for which a user is trying to get secrets - * @param {string} env - environment of a project for which a user is trying ot get secrets - * @returns - */ -const getSecrets = async (workspaceId: string, env: string) => - SecurityClient.fetchCall( - `/api/v2/secrets?${new URLSearchParams({ - environment: env, - workspaceId - })}`, - { - method: "GET", - headers: { - "Content-Type": "application/json" - } - } - ).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).secrets; - } - console.log("Failed to get project secrets"); - return undefined; - }); - -export default getSecrets; diff --git a/frontend/src/pages/api/files/UpdateSecrets.ts b/frontend/src/pages/api/files/UpdateSecrets.ts deleted file mode 100644 index 03b8a18fb..000000000 --- a/frontend/src/pages/api/files/UpdateSecrets.ts +++ /dev/null @@ -1,42 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface EncryptedSecretProps { - id: string; - createdAt: string; - environment: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - type: "personal" | "shared"; -} - -/** - * This function updates certain secrets in a certain project - * @param {object} obj - * @param {EncryptedSecretProps[]} obj.secrets - the ids of secrets that we want to update - * @returns - */ -const updateSecrets = async ({ secrets }: { secrets: EncryptedSecretProps[] }) => - SecurityClient.fetchCall("/api/v2/secrets", { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - secrets - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to update certain project secrets"); - return undefined; - }); - -export default updateSecrets; diff --git a/frontend/src/pages/api/files/UploadSecrets.ts b/frontend/src/pages/api/files/UploadSecrets.ts deleted file mode 100644 index b23f4e5bb..000000000 --- a/frontend/src/pages/api/files/UploadSecrets.ts +++ /dev/null @@ -1,39 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - workspaceId: string; - secrets: any; - keys: string; - environment: string; -} - -/** - * This function uploads the encrypted .env file - * @param {object} obj - * @param {string} obj.workspaceId - * @param {} obj.secrets - * @param {} obj.keys - * @param {string} obj.environment - * @returns - */ -const uploadSecrets = async ({ workspaceId, secrets, keys, environment }: Props) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/secrets`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - secrets, - keys, - environment, - channel: "web" - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to push secrets"); - return undefined; - }); - -export default uploadSecrets; diff --git a/frontend/src/pages/api/files/batchSecrets.ts b/frontend/src/pages/api/files/batchSecrets.ts deleted file mode 100644 index 69b88a451..000000000 --- a/frontend/src/pages/api/files/batchSecrets.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { apiRequest } from "@app/config/request"; - -interface RequestType { - method: string; - secret: { - type: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - tags: string[]; - } -} - -const batchSecrets = async ({ - workspaceId, - environment, - requests -}: { - workspaceId: string; - environment: string; - requests: RequestType[]; -}) => { - const { data } = await apiRequest.post("/api/v2/secrets/batch", { - workspaceId, - environment, - requests - }); - - return data; -} - -export default batchSecrets; \ No newline at end of file diff --git a/frontend/src/pages/api/organization/GetOrgSubscription.ts b/frontend/src/pages/api/organization/GetOrgSubscription.ts deleted file mode 100644 index 9cef7f10a..000000000 --- a/frontend/src/pages/api/organization/GetOrgSubscription.ts +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get the current subscription of an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationSubscriptions = (req: { orgId: string }) => - SecurityClient.fetchCall(`/api/v1/organization/${req.orgId}/subscriptions`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).subscriptions; - } - console.log("Failed to get org subscriptions"); - return undefined; - }); - -export default getOrganizationSubscriptions; diff --git a/frontend/src/pages/api/organization/getOrgs.ts b/frontend/src/pages/api/organization/getOrgs.ts index da92206ec..09cd0f022 100644 --- a/frontend/src/pages/api/organization/getOrgs.ts +++ b/frontend/src/pages/api/organization/getOrgs.ts @@ -4,18 +4,20 @@ import SecurityClient from "@app/components/utilities/SecurityClient"; * This route lets us get the all the orgs of a certain user. * @returns */ -const getOrganizations = () => - SecurityClient.fetchCall("/api/v1/organization", { +const getOrganizations = () => { + return SecurityClient.fetchCall("/api/v1/organization", { method: "GET", headers: { "Content-Type": "application/json" } }).then(async (res) => { if (res?.status === 200) { - return (await res.json()).organizations; + const {organizations} = await res.json(); + return organizations; } console.log("Failed to get orgs of a user"); return undefined; }); +} export default getOrganizations; diff --git a/frontend/src/pages/api/serviceToken/addServiceToken.ts b/frontend/src/pages/api/serviceToken/addServiceToken.ts deleted file mode 100644 index f3ecc661d..000000000 --- a/frontend/src/pages/api/serviceToken/addServiceToken.ts +++ /dev/null @@ -1,56 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - name: string; - workspaceId: string; - environment: string; - expiresIn: number; - encryptedKey: string; - iv: string; - tag: string; -} - -/** - * This route adds a service token for a specific user in a project - * @param {object} obj - * @param {string} obj.name - name of the service token - * @param {string} obj.workspaceId - workspace for which we are issuing the token - * @param {string} obj.environment - environment for which we are issuing the token - * @param {string} obj.expiresIn - how soon the service token expires in ms - * @param {string} obj.encryptedKey - encrypted project key through random symmetric encryption - * @param {string} obj.iv - obtained through symmetric encryption - * @param {string} obj.tag - obtained through symmetric encryption - * @returns - */ -const addServiceToken = ({ - name, - workspaceId, - environment, - expiresIn, - encryptedKey, - iv, - tag -}: Props) => - SecurityClient.fetchCall("/api/v2/service-token/", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - name, - workspaceId, - environment, - expiresIn, - encryptedKey, - iv, - tag - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to add service tokens"); - return undefined; - }); - -export default addServiceToken; diff --git a/frontend/src/pages/api/serviceToken/deleteServiceToken.ts b/frontend/src/pages/api/serviceToken/deleteServiceToken.ts deleted file mode 100644 index e2d4b97ec..000000000 --- a/frontend/src/pages/api/serviceToken/deleteServiceToken.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - serviceTokenId: string; -} - -/** - * This route revokes a specific service token - * @param {object} obj - * @param {string} obj.serviceTokenId - id of a cervice token that we want to delete - * @returns - */ -const deleteServiceToken = ({ serviceTokenId }: Props) => - SecurityClient.fetchCall(`/api/v2/service-token/${serviceTokenId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to delete a service token"); - return undefined; - }); - -export default deleteServiceToken; diff --git a/frontend/src/pages/api/serviceToken/getServiceTokens.ts b/frontend/src/pages/api/serviceToken/getServiceTokens.ts deleted file mode 100644 index 38f02cb52..000000000 --- a/frontend/src/pages/api/serviceToken/getServiceTokens.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route gets service tokens for a specific user in a project - * @param {*} param0 - * @returns - */ -const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/service-token-data`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).serviceTokenData; - } - console.log("Failed to get service tokens"); - return undefined; - }); - -export default getServiceTokens; diff --git a/frontend/src/pages/api/workspace/createWorkspace.ts b/frontend/src/pages/api/workspace/createWorkspace.ts deleted file mode 100644 index f241a4fe8..000000000 --- a/frontend/src/pages/api/workspace/createWorkspace.ts +++ /dev/null @@ -1,33 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route creates a new workspace for a user within a certain organization. - * @param {string} workspaceName - project Name - * @param {string} organizationId - org ID - * @returns - */ -const createWorkspace = ({ - workspaceName, - organizationId -}: { - workspaceName: string; - organizationId: string; -}) => - SecurityClient.fetchCall("/api/v1/workspace", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - workspaceName, - organizationId - }) - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).workspace; - } - console.log("Failed to create a project"); - return undefined; - }); - -export default createWorkspace; diff --git a/frontend/src/pages/api/workspace/renameWorkspace.ts b/frontend/src/pages/api/workspace/renameWorkspace.ts deleted file mode 100644 index 7e910a42a..000000000 --- a/frontend/src/pages/api/workspace/renameWorkspace.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us rename a certain workspace. - * @param {*} req - * @param {*} res - * @returns - */ -const renameWorkspace = (workspaceId: string, newWorkspaceName: string) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/name`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - name: newWorkspaceName - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to rename a project"); - return undefined; - }); - -export default renameWorkspace; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx index 58e88e593..8e765f3ff 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx @@ -1,30 +1,17 @@ -import { useEffect, useState } from "react"; - import { decryptAssymmetric, encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import { Checkbox } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api"; -import getBot from "../../../../../pages/api/bot/getBot"; -import setBotActiveStatus from "../../../../../pages/api/bot/setBotActiveStatus"; import getLatestFileKey from "../../../../../pages/api/workspace/getLatestFileKey"; export const E2EESection = () => { const { currentWorkspace } = useWorkspace(); - const [bot, setBot] = useState(null); - - useEffect(() => { - (async () => { - if (currentWorkspace) { - // get project bot - setBot(await getBot({ - workspaceId: currentWorkspace._id - })); - } - })(); - }, [currentWorkspace]); + const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? ""); + const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus(); /** * Activate bot for project by performing the following steps: @@ -69,22 +56,20 @@ export const E2EESection = () => { encryptedKey: ciphertext, nonce }; - - const botx = await setBotActiveStatus({ - botId: bot._id, + + await updateBotActiveStatus({ + workspaceId: currentWorkspace._id, + botKey, isActive: true, - botKey + botId: bot._id }); - - setBot(botx.bot); } else { // bot is active -> deactivate bot - const botx = await setBotActiveStatus({ + await updateBotActiveStatus({ + isActive: false, botId: bot._id, - isActive: false + workspaceId: currentWorkspace._id }); - - setBot(botx.bot); } } } catch (err) { From b47f61f1ad10663d8b620352a0cec0a1e4da0618 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 9 Aug 2023 17:55:57 +0700 Subject: [PATCH 30/64] Delete more deprecated frontend calls --- .../src/controllers/v2/secretsController.ts | 4 +- backend/src/ee/services/EEAuditLogService.ts | 2 +- backend/src/routes/v1/userAction.ts | 2 +- .../basic/table/ProjectUsersTable.tsx | 11 +-- .../utilities/checks/OnboardingCheck.ts | 18 ++--- frontend/src/helpers/project.ts | 4 +- frontend/src/hooks/api/users/index.tsx | 6 +- frontend/src/hooks/api/users/queries.tsx | 30 ++++++- frontend/src/hooks/api/users/types.ts | 2 +- frontend/src/pages/api/user/getUser.ts | 20 ----- .../src/pages/api/user/updateMyMfaEnabled.ts | 32 -------- .../pages/api/userActions/checkUserAction.ts | 29 ------- .../api/userActions/registerUserAction.ts | 25 ------ .../src/pages/api/workspace/getAWorkspace.ts | 30 ------- .../src/pages/api/workspace/getProjectInfo.ts | 22 ------ .../api/workspace/getWorkspaceEnvironments.ts | 22 ------ .../src/pages/org/[id]/overview/index.tsx | 19 ++--- .../src/pages/project/[id]/members/index.tsx | 79 ++++++++++--------- .../SecuritySection/MFASection.tsx | 53 +++++-------- 19 files changed, 121 insertions(+), 289 deletions(-) delete mode 100644 frontend/src/pages/api/user/getUser.ts delete mode 100644 frontend/src/pages/api/user/updateMyMfaEnabled.ts delete mode 100644 frontend/src/pages/api/userActions/checkUserAction.ts delete mode 100644 frontend/src/pages/api/userActions/registerUserAction.ts delete mode 100644 frontend/src/pages/api/workspace/getAWorkspace.ts delete mode 100644 frontend/src/pages/api/workspace/getProjectInfo.ts delete mode 100644 frontend/src/pages/api/workspace/getWorkspaceEnvironments.ts diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 13537df9c..23f9a6d0a 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,7 +1,7 @@ import { Types } from "mongoose"; import { Request, Response } from "express"; import { ISecret, Secret, ServiceTokenData } from "../../models"; -import { IAction, SecretVersion, EventType, AuditLog } from "../../ee/models"; +import { AuditLog, EventType, IAction, SecretVersion } from "../../ee/models"; import { ACTION_ADD_SECRETS, ACTION_DELETE_SECRETS, @@ -14,7 +14,7 @@ import { import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EventService } from "../../services"; import { eventPushSecrets } from "../../events"; -import { EELogService, EESecretService, EEAuditLogService } from "../../ee/services"; +import { EEAuditLogService, EELogService, EESecretService } from "../../ee/services"; import { SecretService, TelemetryService } from "../../services"; import { getUserAgentType } from "../../utils/posthog"; import { PERMISSION_WRITE_SECRETS } from "../../variables"; diff --git a/backend/src/ee/services/EEAuditLogService.ts b/backend/src/ee/services/EEAuditLogService.ts index 91fbc4252..eb5c1bbb3 100644 --- a/backend/src/ee/services/EEAuditLogService.ts +++ b/backend/src/ee/services/EEAuditLogService.ts @@ -16,7 +16,7 @@ type ValidEventScope = | Required export default class EEAuditLogService { - static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope, shouldSave: boolean = true) { + static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope, shouldSave = true) { const MS_IN_DAY = 24 * 60 * 60 * 1000; diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index 042f73c10..7fd26f783 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -6,7 +6,7 @@ import { userActionController } from "../../controllers/v1"; import { AuthMode } from "../../variables"; // note: [userAction] will be deprecated in /v2 in favor of [action] -router.post( +router.post( // TODO endpoint: move this into /users/me "/", requireAuth({ acceptedAuthModes: [AuthMode.JWT], diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 022eda020..5a9795cff 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -4,12 +4,11 @@ import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Select, SelectItem } from "@app/components/v2"; -import { useSubscription } from "@app/context"; +import { useSubscription, useWorkspace } from "@app/context"; import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission"; import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace"; import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace"; import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; -import getProjectInfo from "@app/pages/api/workspace/getProjectInfo"; import uploadKeys from "@app/pages/api/workspace/uploadKeys"; import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto"; @@ -39,6 +38,7 @@ type EnvironmentProps = { * @returns */ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => { + const { currentWorkspace } = useWorkspace(); const { subscription } = useSubscription(); const [roleSelected, setRoleSelected] = useState( Array(userData?.length).fill(userData.map((user) => user.role)) @@ -163,10 +163,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa useEffect(() => { setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); (async () => { - const result = await getProjectInfo({ projectId: workspaceId }); - setWorkspaceEnvs(result.environments); + if (currentWorkspace) { + setWorkspaceEnvs(currentWorkspace.environments); + } })(); - }, [userData, myUser]); + }, [userData, myUser, currentWorkspace]); const grantAccess = async (id: string, publicKey: string) => { const result = await getLatestFileKey({ workspaceId }); diff --git a/frontend/src/components/utilities/checks/OnboardingCheck.ts b/frontend/src/components/utilities/checks/OnboardingCheck.ts index 20bb1cd78..f9b47a210 100644 --- a/frontend/src/components/utilities/checks/OnboardingCheck.ts +++ b/frontend/src/components/utilities/checks/OnboardingCheck.ts @@ -1,5 +1,5 @@ +import { fetchUserAction } from "@app/hooks/api/users/queries"; import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers"; -import checkUserAction from "@app/pages/api/userActions/checkUserAction"; interface OnboardingCheckProps { setTotalOnboardingActionsDone?: (value: number) => void; @@ -20,25 +20,23 @@ const onboardingCheck = async ({ setUsersInOrg }: OnboardingCheckProps) => { let countActions = 0; - const userActionSlack = await checkUserAction({ - action: "slack_cta_clicked" - }); + const userActionSlack = await fetchUserAction( + "slack_cta_clicked" + ); + if (userActionSlack) { countActions += 1; } if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack); - const userActionSecrets = await checkUserAction({ - action: "first_time_secrets_pushed" - }); + const userActionSecrets = await fetchUserAction("first_time_secrets_pushed"); + if (userActionSecrets) { countActions += 1; } if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets); - const userActionIntro = await checkUserAction({ - action: "intro_cta_clicked" - }); + const userActionIntro = await fetchUserAction("intro_cta_clicked"); if (userActionIntro) { countActions += 1; } diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 3b3dddf5a..c15baf1ef 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -3,8 +3,8 @@ import crypto from "crypto"; import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets"; import { createSecret } from "@app/hooks/api/secrets/queries"; +import { fetchUserDetails } from "@app/hooks/api/users/queries"; import { createWorkspace } from "@app/hooks/api/workspace/queries"; -import getUser from "@app/pages/api/user/getUser"; import uploadKeys from "@app/pages/api/workspace/uploadKeys"; const secretsToBeAdded = [ @@ -108,7 +108,7 @@ const initProjectHelper = async ({ if (!PRIVATE_KEY) throw new Error("Failed to find private key"); - const user = await getUser(); + const user = await fetchUserDetails(); const { ciphertext, nonce } = encryptAssymmetric({ plaintext: randomBytes, diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index a209367e2..e1b9fe402 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -3,8 +3,10 @@ export { useAddUserToOrg, useAddUserToWs, useCreateAPIKey, + useCreateMyAction, useDeleteAPIKey, useDeleteOrgMembership, + useGetMyActions, useGetMyAPIKeys, useGetMyIp, useGetMySessions, @@ -14,6 +16,6 @@ export { useLogoutUser, useRegisterUserAction, useRevokeMySessions, + useUpdateMfaEnabled, useUpdateOrgUserRole, - useUpdateUserAuthProvider -} from "./queries"; + useUpdateUserAuthProvider} from "./queries"; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 93d8d41de..938abf3aa 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -19,7 +19,8 @@ import { RenameUserDTO, TokenVersion, UpdateOrgUserRoleDTO, - User} from "./types"; + User +} from "./types"; const userKeys = { getUser: ["user"] as const, @@ -27,7 +28,7 @@ const userKeys = { getOrgUsers: (orgId: string) => [{ orgId }, "user"], myIp: ["ip"] as const, myAPIKeys: ["api-keys"] as const, - mySessions: ["sessions"] as const + mySessions: ["sessions"] as const, }; export const fetchUserDetails = async () => { @@ -38,7 +39,7 @@ export const fetchUserDetails = async () => { export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails); -const fetchUserAction = async (action: string) => { +export const fetchUserAction = async (action: string) => { const { data } = await apiRequest.get<{ userAction: string }>("/api/v1/user-action", { params: { action @@ -303,4 +304,27 @@ export const useRevokeMySessions = () => { queryClient.invalidateQueries(userKeys.mySessions); } }); +} + +export const useUpdateMfaEnabled = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + isMfaEnabled + }: { + isMfaEnabled: boolean; + }) => { + const { data: { user } } = await apiRequest.patch( + "/api/v2/users/me/mfa", + { + isMfaEnabled + } + ); + + return user; + }, + onSuccess() { + queryClient.invalidateQueries(userKeys.getUser); + } + }); } \ No newline at end of file diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 0c312b2b6..fdef407fa 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -9,7 +9,7 @@ export enum AuthProvider { export type User = { createdAt: Date; updatedAt: Date; - email?: string; + email: string; firstName?: string; lastName?: string; authProvider?: AuthProvider; diff --git a/frontend/src/pages/api/user/getUser.ts b/frontend/src/pages/api/user/getUser.ts deleted file mode 100644 index afdd268ba..000000000 --- a/frontend/src/pages/api/user/getUser.ts +++ /dev/null @@ -1,20 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route gets the information about a specific user. - */ -const getUser = () => - SecurityClient.fetchCall("/api/v1/user", { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).user; - } - console.log("Failed to get user info"); - return undefined; - }); - -export default getUser; diff --git a/frontend/src/pages/api/user/updateMyMfaEnabled.ts b/frontend/src/pages/api/user/updateMyMfaEnabled.ts deleted file mode 100644 index e22f14958..000000000 --- a/frontend/src/pages/api/user/updateMyMfaEnabled.ts +++ /dev/null @@ -1,32 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - isMfaEnabled: boolean; -} - -/** - * Update the user's MFA-enabled status to [isMfaEnabled] - * @param {Object} obj - * @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not - * @returns {User} user - user with updated MFA-enabled status - */ -const updateMyMfaEnabled = async ({ - isMfaEnabled -}: Props) => - SecurityClient.fetchCall("/api/v2/users/me/mfa", { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - isMfaEnabled, - }) - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).user; - } - console.log("Failed to update MFA status"); - return undefined; - }); - -export default updateMyMfaEnabled; \ No newline at end of file diff --git a/frontend/src/pages/api/userActions/checkUserAction.ts b/frontend/src/pages/api/userActions/checkUserAction.ts deleted file mode 100644 index 5663c2649..000000000 --- a/frontend/src/pages/api/userActions/checkUserAction.ts +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route registers a certain action for a user - * @param {*} email - * @param {*} workspaceId - * @returns - */ -const checkUserAction = ({ action }: { action: string }) => - SecurityClient.fetchCall( - "/api/v1/user-action" + - `?${new URLSearchParams({ - action - })}`, - { - method: "GET", - headers: { - "Content-Type": "application/json" - } - } - ).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).userAction; - } - console.log("Failed to check a user action"); - return undefined; - }); - -export default checkUserAction; diff --git a/frontend/src/pages/api/userActions/registerUserAction.ts b/frontend/src/pages/api/userActions/registerUserAction.ts deleted file mode 100644 index dd29b7f29..000000000 --- a/frontend/src/pages/api/userActions/registerUserAction.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route registers a certain action for a user - * @param {*} action - * @returns - */ -const registerUserAction = ({ action }: { action: string }) => - SecurityClient.fetchCall("/api/v1/user-action", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - action - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to register a user action"); - return undefined; - }); - -export default registerUserAction; diff --git a/frontend/src/pages/api/workspace/getAWorkspace.ts b/frontend/src/pages/api/workspace/getAWorkspace.ts deleted file mode 100644 index cf4a3d696..000000000 --- a/frontend/src/pages/api/workspace/getAWorkspace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Workspace { - __v: number; - _id: string; - name: string; - organization: string; - environments: Array<{ name: string; slug: string }>; -} - -/** - * This route lets us get the workspaces of a certain user - * @returns - */ -const getAWorkspace = (workspaceID: string) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceID}`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - const data = (await res.json()) as unknown as { workspace: Workspace }; - return data.workspace; - } - - throw new Error("Failed to get workspace"); - }); - -export default getAWorkspace; diff --git a/frontend/src/pages/api/workspace/getProjectInfo.ts b/frontend/src/pages/api/workspace/getProjectInfo.ts deleted file mode 100644 index cb1e54c04..000000000 --- a/frontend/src/pages/api/workspace/getProjectInfo.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get the information of a certain project. - * @param {*} projectId - project ID (we renamed workspaces to projects in the app) - * @returns - */ -const getProjectInfo = ({ projectId }: { projectId: string }) => - SecurityClient.fetchCall(`/api/v1/workspace/${projectId}`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).workspace; - } - console.log("Failed to get project info"); - return undefined; - }); - -export default getProjectInfo; diff --git a/frontend/src/pages/api/workspace/getWorkspaceEnvironments.ts b/frontend/src/pages/api/workspace/getWorkspaceEnvironments.ts deleted file mode 100644 index 2d7d9359b..000000000 --- a/frontend/src/pages/api/workspace/getWorkspaceEnvironments.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get the environments that a certain user has acess to in a certain project - * @param {string} workspaceId - * @returns - */ -const getWorkspaceEnvironments = ({ workspaceId }: { workspaceId: string }) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).accessibleEnvironments; - } - console.log("Failed to get accessible environments"); - return undefined; - }); - -export default getWorkspaceEnvironments; diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 1957d29f5..a026b4da3 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -36,11 +36,10 @@ import { } from "@app/components/v2"; import { TabsObject } from "@app/components/v2/Tabs"; import { useSubscription, useUser, useWorkspace } from "@app/context"; -import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api"; +import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; -import registerUserAction from "../../../api/userActions/registerUserAction"; const features = [ { @@ -70,6 +69,7 @@ const LearningItem = ({ userAction, link }: ItemProps): JSX.Element => { + const registerUserAction = useRegisterUserAction(); if (link) { return (
{ if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction({ - action: userAction - }); + await registerUserAction.mutateAsync( + userAction + ); } }} className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ @@ -130,9 +130,7 @@ const LearningItem = ({ tabIndex={0} onClick={async () => { if (userAction) { - await registerUserAction({ - action: userAction - }); + await registerUserAction.mutateAsync(userAction); } }} className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500" @@ -169,6 +167,7 @@ const LearningItemSquare = ({ userAction, link }: ItemProps): JSX.Element => { + const registerUserAction = useRegisterUserAction(); return ( { if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction({ - action: userAction - }); + await registerUserAction.mutateAsync(userAction); } }} className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${ diff --git a/frontend/src/pages/project/[id]/members/index.tsx b/frontend/src/pages/project/[id]/members/index.tsx index 471c1bca7..cbd13d208 100644 --- a/frontend/src/pages/project/[id]/members/index.tsx +++ b/frontend/src/pages/project/[id]/members/index.tsx @@ -11,14 +11,13 @@ import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMembe import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable"; import guidGenerator from "@app/components/utilities/randomId"; import { Input } from "@app/components/v2"; +import { useGetUser } from "@app/hooks/api"; import { decryptAssymmetric, encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; import getOrganizationUsers from "../../../api/organization/GetOrgUsers"; -import getUser from "../../../api/user/getUser"; -// import DeleteUserDialog from '@app/components/basic/dialog/DeleteUserDialog'; import addUserToWorkspace from "../../../api/workspace/addUserToWorkspace"; import getWorkspaceUsers from "../../../api/workspace/getWorkspaceUsers"; import uploadKeys from "../../../api/workspace/uploadKeys"; @@ -43,6 +42,7 @@ interface MembershipProps { // #TODO: Update all the workspaceIds export default function Users() { + const { data: user } = useGetUser(); const [isAddOpen, setIsAddOpen] = useState(false); // let [isDeleteOpen, setIsDeleteOpen] = useState(false); // let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false); @@ -60,46 +60,47 @@ export default function Users() { const [orgUserList, setOrgUserList] = useState([]); useEffect(() => { - (async () => { - const user = await getUser(); - setPersonalEmail(user.email); + if (user) { + (async () => { + setPersonalEmail(user.email); - // This part quiries the current users of a project - const workspaceUsers = await getWorkspaceUsers({ - workspaceId - }); - const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({ - key: guidGenerator(), - firstName: membership.user?.firstName, - lastName: membership.user?.lastName, - email: membership.user?.email === null ? membership.inviteEmail : membership.user?.email, - role: membership?.role, - status: membership?.status, - userId: membership.user?._id, - membershipId: membership._id, - deniedPermissions: membership.deniedPermissions, - publicKey: membership.user?.publicKey - })); - setUserList(tempUserList); + // This part quiries the current users of a project + const workspaceUsers = await getWorkspaceUsers({ + workspaceId + }); + const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({ + key: guidGenerator(), + firstName: membership.user?.firstName, + lastName: membership.user?.lastName, + email: membership.user?.email === null ? membership.inviteEmail : membership.user?.email, + role: membership?.role, + status: membership?.status, + userId: membership.user?._id, + membershipId: membership._id, + deniedPermissions: membership.deniedPermissions, + publicKey: membership.user?.publicKey + })); + setUserList(tempUserList); - setIsUserListLoading(false); + setIsUserListLoading(false); - // This is needed to know wha users from an org (if any), we are able to add to a certain project - const orgUsers = await getOrganizationUsers({ - orgId: String(localStorage.getItem("orgData.id")) - }); - setOrgUserList(orgUsers); - setEmail( - orgUsers - ?.filter((membership: MembershipProps) => membership.status === "accepted") - .map((membership: MembershipProps) => membership.user.email) - .filter( - (usEmail: string) => - !tempUserList?.map((user1: UserProps) => user1.email).includes(usEmail) - )[0] - ); - })(); - }, []); + // This is needed to know wha users from an org (if any), we are able to add to a certain project + const orgUsers = await getOrganizationUsers({ + orgId: String(localStorage.getItem("orgData.id")) + }); + setOrgUserList(orgUsers); + setEmail( + orgUsers + ?.filter((membership: MembershipProps) => membership.status === "accepted") + .map((membership: MembershipProps) => membership.user.email) + .filter( + (usEmail: string) => + !tempUserList?.map((user1: UserProps) => user1.email).includes(usEmail) + )[0] + ); + })(); + } + }, [user]); const closeAddModal = () => { setIsAddOpen(false); diff --git a/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/MFASection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/MFASection.tsx index 8f9b36db7..6f52b7427 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/MFASection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/SecuritySection/MFASection.tsx @@ -1,17 +1,14 @@ -import { useEffect, useState } from "react"; - import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Checkbox, EmailServiceSetupModal } from "@app/components/v2"; +import { + useGetUser, + useUpdateMfaEnabled} from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { usePopUp } from "@app/hooks/usePopUp"; -import { useGetUser } from "../../../../hooks/api"; -import { User } from "../../../../hooks/api/types"; -import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled"; - export const MFASection = () => { - const [isMfaEnabled, setIsMfaEnabled] = useState(false); const { data: user } = useGetUser(); + const { mutateAsync } = useUpdateMfaEnabled(); const { createNotification } = useNotificationContext(); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([ "setUpEmail" @@ -19,22 +16,12 @@ export const MFASection = () => { const {data: serverDetails } = useFetchServerStatus() - useEffect(() => { - if (user && typeof user.isMfaEnabled !== "undefined") { - setIsMfaEnabled(user.isMfaEnabled); - } - }, [user]); - const toggleMfa = async (state: boolean) => { try { - const newUser: User = await updateMyMfaEnabled({ + const newUser = await mutateAsync({ isMfaEnabled: state }); - if (newUser) { - setIsMfaEnabled(newUser.isMfaEnabled); - } - createNotification({ text: `${newUser.isMfaEnabled ? "Successfully turned on two-factor authentication." : "Successfully turned off two-factor authentication."}`, type: "success" @@ -55,20 +42,22 @@ export const MFASection = () => {

Two-factor Authentication

- { - if (serverDetails?.emailConfigured){ - toggleMfa(state as boolean); - } else { - handlePopUpOpen("setUpEmail"); - } - }} - > - Enable 2-factor authentication via your personal email. - + {user && ( + { + if (serverDetails?.emailConfigured){ + toggleMfa(state as boolean); + } else { + handlePopUpOpen("setUpEmail"); + } + }} + > + Enable 2-factor authentication via your personal email. + + )}
Date: Wed, 9 Aug 2023 21:54:56 +0700 Subject: [PATCH 31/64] Add logs for workspace user role and read/write permission changes --- .../controllers/v1/membershipController.ts | 24 ++++++++++++++--- .../ee/controllers/v1/membershipController.ts | 27 +++++++++++++++++-- backend/src/ee/models/auditLog/enums.ts | 2 ++ backend/src/ee/models/auditLog/types.ts | 26 +++++++++++++++++- .../UpdateUserProjectPermission.ts | 6 ++--- .../src/hooks/api/auditLogs/constants.tsx | 3 ++- frontend/src/hooks/api/auditLogs/enums.tsx | 4 ++- frontend/src/hooks/api/auditLogs/types.tsx | 27 ++++++++++++++++++- .../AuditLogsPage/components/LogsTableRow.tsx | 21 +++++++++++++++ 9 files changed, 128 insertions(+), 12 deletions(-) diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 48689be1c..885f16690 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -104,9 +104,9 @@ export const changeMembershipRole = async (req: Request, res: Response) => { } // validate target membership - const membershipToChangeRole = await findMembership({ - _id: membershipId - }); + const membershipToChangeRole = await Membership + .findById(membershipId) + .populate<{ user: IUser }>("user"); if (!membershipToChangeRole) { throw new Error("Failed to find membership to change role"); @@ -127,9 +127,27 @@ export const changeMembershipRole = async (req: Request, res: Response) => { // user is not an admin member of the workspace throw new Error("Insufficient role for changing member roles"); } + + const oldRole = membershipToChangeRole.role; membershipToChangeRole.role = role; await membershipToChangeRole.save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_USER_WORKSPACE_ROLE, + metadata: { + userId: membershipToChangeRole.user._id.toString(), + email: membershipToChangeRole.user.email, + oldRole, + newRole: membershipToChangeRole.role + } + }, + { + workspaceId: membershipToChangeRole.workspace + } + ); return res.status(200).send({ membership: membershipToChangeRole diff --git a/backend/src/ee/controllers/v1/membershipController.ts b/backend/src/ee/controllers/v1/membershipController.ts index f7bebfdc5..4d2321a45 100644 --- a/backend/src/ee/controllers/v1/membershipController.ts +++ b/backend/src/ee/controllers/v1/membershipController.ts @@ -1,10 +1,12 @@ import { Request, Response } from "express"; -import { Membership, Workspace } from "../../../models"; +import { IUser, Membership, Workspace } from "../../../models"; +import { EventType } from "../../../ee/models"; import { IMembershipPermission } from "../../../models/membership"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; import { ADMIN, MEMBER } from "../../../variables/organization"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables"; import _ from "lodash"; +import { EEAuditLogService } from "../../services"; export const denyMembershipPermissions = async (req: Request, res: Response) => { const { membershipId } = req.params; @@ -51,12 +53,33 @@ export const denyMembershipPermissions = async (req: Request, res: Response) => { _id: membershipToModify._id }, { $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } }, { new: true } - ) + ).populate<{ user: IUser }>("user"); if (!updatedMembershipWithPermissions) { throw BadRequestError({ message: "The resource has been removed before it can be modified" }) } + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, + metadata: { + userId: updatedMembershipWithPermissions.user._id.toString(), + email: updatedMembershipWithPermissions.user.email, + deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({ + environmentSlug, + ability + }) => ({ + environmentSlug, + ability + })) + } + }, + { + workspaceId: updatedMembershipWithPermissions.workspace + } + ); + res.send({ permissionsDenied: updatedMembershipWithPermissions.deniedPermissions, }) diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index c1523cc84..dda49c1cd 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -42,4 +42,6 @@ export enum EventType { CREATE_SECRET_IMPORT = "create-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import", DELETE_SECRET_IMPORT = "delete-secret-import", + UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", + UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions" } \ No newline at end of file diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 261421798..71507212e 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -346,6 +346,28 @@ interface DeleteSecretImportEvent { } } +interface UpdateUserRole { + type: EventType.UPDATE_USER_WORKSPACE_ROLE, + metadata: { + userId: string; + email: string; + oldRole: string; + newRole: string; + } +} + +interface UpdateUserDeniedPermissions { + type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, + metadata: { + userId: string; + email: string; + deniedPermissions: { + environmentSlug: string; + ability: string; + }[] + } +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -376,4 +398,6 @@ export type Event = | GetSecretImportsEvent | CreateSecretImportEvent | UpdateSecretImportEvent - | DeleteSecretImportEvent; \ No newline at end of file + | DeleteSecretImportEvent + | UpdateUserRole + | UpdateUserDeniedPermissions; \ No newline at end of file diff --git a/frontend/src/ee/api/memberships/UpdateUserProjectPermission.ts b/frontend/src/ee/api/memberships/UpdateUserProjectPermission.ts index d4371e252..c50710601 100644 --- a/frontend/src/ee/api/memberships/UpdateUserProjectPermission.ts +++ b/frontend/src/ee/api/memberships/UpdateUserProjectPermission.ts @@ -26,9 +26,9 @@ const updateUserProjectPermission = async ({ permissions: denials }) }).then(async (res) => { - console.log({ - permissions: denials - }, res) + // console.log({ + // permissions: denials + // }, res) if (res && res.status === 200) { return res.json(); } diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index f76e0f4be..cfdee0a10 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -31,7 +31,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_SECRET_IMPORT]: "Create secret import", [EventType.UPDATE_SECRET_IMPORT]: "Update secret import", [EventType.DELETE_SECRET_IMPORT]: "Delete secret import", - + [EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS]: "Update denied permissions", + [EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index dfc047b03..d19876525 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -40,5 +40,7 @@ export enum EventType { GET_SECRET_IMPORTS = "get-secret-imports", CREATE_SECRET_IMPORT = "create-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import", - DELETE_SECRET_IMPORT = "delete-secret-import" + DELETE_SECRET_IMPORT = "delete-secret-import", + UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", + UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions" } \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 9a41c85f0..40488f196 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -348,6 +348,29 @@ interface DeleteSecretImportEvent { } } +interface UpdateUserRole { + type: EventType.UPDATE_USER_WORKSPACE_ROLE, + metadata: { + userId: string; + email: string; + oldRole: string; + newRole: string; + } +} + +interface UpdateUserDeniedPermissions { + type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, + metadata: { + userId: string; + email: string; + deniedPermissions: { + environmentSlug: string; + ability: string; + }[] + } +} + + export type Event = | GetSecretsEvent | GetSecretEvent @@ -378,7 +401,9 @@ export type Event = | GetSecretImportsEvent | CreateSecretImportEvent | UpdateSecretImportEvent - | DeleteSecretImportEvent; + | DeleteSecretImportEvent + | UpdateUserRole + | UpdateUserDeniedPermissions; export type AuditLog = { _id: string; diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx index 795450d9d..0520d73ba 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx @@ -279,6 +279,27 @@ export const LogsTableRow = ({

{`Import to path: ${event.metadata.importToSecretPath}`}

); + case EventType.UPDATE_USER_WORKSPACE_ROLE: + return ( + +

{`Email: ${event.metadata.email}`}

+

{`Old role: ${event.metadata.oldRole}`}

+

{`New role: ${event.metadata.newRole}`}

+ + ); + case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS: + return ( + +

{`Email: ${event.metadata.email}`}

+ {event.metadata.deniedPermissions.map((permission) => { + return ( +

+ {`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`} +

+ ); + })} + + ); default: return ( From 0a538ac1a7413b7756fafa23b644a4265896bc85 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 9 Aug 2023 22:02:19 +0700 Subject: [PATCH 32/64] Add GitHub SSO to changelog --- docs/changelog/overview.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 2262704e3..9ea4cdf13 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,6 +4,10 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis. +## August 2023 + +- Add support for GitHub SSO. + ## July 2023 - Released [secret referencing and importing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments. From a31ffe96172428e8777683a7c972c713dcbe53d6 Mon Sep 17 00:00:00 2001 From: vmatsiiako <78047717+vmatsiiako@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:12:10 -0700 Subject: [PATCH 33/64] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 459ea506e..a44ab0508 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ git commit activity
- Cloudsmith downloads + Cloudsmith downloads Slack community channel From b49ef9efc95237cecdac16d2b6401bfd82498031 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 9 Aug 2023 12:03:00 -0700 Subject: [PATCH 34/64] minor frontend UX fixes --- .../src/views/DashboardPage/DashboardPage.tsx | 1 + .../SecretImportSection/SecretImportItem.tsx | 28 ++++++++++++++----- .../SecretImportSection.tsx | 4 ++- .../SecretInputRow/SecretInputRow.tsx | 2 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 10 +++++-- .../ProjectSettingsPage.tsx | 2 +- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index acb0f59ff..b83cb839d 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -950,6 +950,7 @@ export const DashboardPage = () => { secrets={secrets?.secrets} importedSecrets={importedSecrets} items={items} + searchTerm={searchFilter} /> { const [isExpanded, setIsExpanded] = useToggle(); const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({ @@ -46,6 +48,17 @@ export const SecretImportItem = ({ const { currentWorkspace } = useWorkspace(); const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv); + useEffect(() => { + const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())) + + if (filteredSecrets.length > 0 && searchTerm) { + setIsExpanded.on(); + } else { + setIsExpanded.off(); + } + }, [searchTerm]); + + useEffect(() => { if (isDragging) { setIsExpanded.off(); @@ -65,8 +78,10 @@ export const SecretImportItem = ({ className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700" onClick={() => setIsExpanded.toggle()} > - - + + + + {isExpanded && !isDragging && ( - -
-
Secrets Imported
+ +
@@ -132,7 +146,7 @@ export const SecretImportItem = ({ )} - {importedSecrets.map(({ key, value, overriden }, index) => ( + {importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => ( + diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 86f2a2071..df88d8138 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -288,8 +288,14 @@ export const SecretOverviewPage = () => { className="min-table-row min-w-[11rem] border-b-0 p-0 text-center" key={`secret-overview-${name}-${index + 1}`} > -
- {name} +
+ {missingKeyCount > 0 && ( { const { t } = useTranslation(); return ( -
+
From 9963724a6a116301c6bbaf22b464e01a82be79c0 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 10 Aug 2023 12:18:17 +0700 Subject: [PATCH 35/64] Continue removing unused frontend components/logic, improve querying in select pages --- .../basic/dialog/AddIncidentContactDialog.tsx | 99 ------ .../basic/table/ProjectUsersTable.tsx | 53 +-- .../integrations/CloudIntegration.tsx | 122 ------- .../integrations/CloudIntegrationSection.tsx | 63 ---- .../integrations/FrameworkIntegration.tsx | 36 -- .../FrameworkIntegrationSection.tsx | 38 --- .../components/integrations/Integration.tsx | 313 ------------------ .../integrations/IntegrationSection.tsx | 63 ---- frontend/src/helpers/project.ts | 100 +++--- .../hooks/api/incidentContacts/queries.tsx | 16 +- .../src/hooks/api/integrations/queries.tsx | 2 +- .../src/hooks/api/organization/queries.tsx | 6 +- frontend/src/hooks/api/tags/queries.tsx | 8 +- frontend/src/hooks/api/users/index.tsx | 2 - frontend/src/hooks/api/users/queries.tsx | 12 +- frontend/src/hooks/api/workspace/index.tsx | 7 +- frontend/src/hooks/api/workspace/queries.tsx | 78 ++++- .../api/integrations/DeleteIntegration.ts | 25 -- .../api/integrations/DeleteIntegrationAuth.ts | 26 -- .../api/integrations/GetIntegrationApps.ts | 21 -- .../api/integrations/GetIntegrationOptions.ts | 17 - .../api/integrations/StartIntegration.ts | 35 -- .../getWorkspaceAuthorizations.ts | 26 -- .../integrations/getWorkspaceIntegrations.ts | 26 -- .../api/integrations/updateIntegration.ts | 55 --- frontend/src/pages/api/organization/GetOrg.ts | 22 -- .../organization/GetOrgProjectMemberships.ts | 24 -- .../pages/api/organization/GetOrgProjects.ts | 25 -- .../pages/api/organization/StripeRedirect.ts | 23 -- .../api/organization/addIncidentContact.ts | 25 -- .../changeUserRoleInOrganization.ts | 27 -- .../api/organization/deleteIncidentContact.ts | 25 -- .../deleteUserFromOrganization.ts | 22 -- .../api/organization/getIncidentContacts.ts | 27 -- .../src/pages/api/organization/renameOrg.ts | 26 -- .../pages/api/workspace/addUserToWorkspace.ts | 26 -- .../workspace/changeUserRoleInWorkspace.ts | 26 -- .../api/workspace/deleteUserFromWorkspace.ts | 22 -- .../pages/api/workspace/deleteWorkspace.ts | 22 -- .../pages/api/workspace/getWorkspaceKeys.ts | 22 -- .../pages/api/workspace/getWorkspaceTags.ts | 22 -- .../pages/api/workspace/getWorkspaceUsers.ts | 22 -- .../src/pages/api/workspace/getWorkspaces.ts | 31 -- .../src/pages/project/[id]/members/index.tsx | 29 +- 44 files changed, 181 insertions(+), 1536 deletions(-) delete mode 100644 frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx delete mode 100644 frontend/src/components/integrations/CloudIntegration.tsx delete mode 100644 frontend/src/components/integrations/CloudIntegrationSection.tsx delete mode 100644 frontend/src/components/integrations/FrameworkIntegration.tsx delete mode 100644 frontend/src/components/integrations/FrameworkIntegrationSection.tsx delete mode 100644 frontend/src/components/integrations/Integration.tsx delete mode 100644 frontend/src/components/integrations/IntegrationSection.tsx delete mode 100644 frontend/src/pages/api/integrations/DeleteIntegration.ts delete mode 100644 frontend/src/pages/api/integrations/DeleteIntegrationAuth.ts delete mode 100644 frontend/src/pages/api/integrations/GetIntegrationApps.ts delete mode 100644 frontend/src/pages/api/integrations/GetIntegrationOptions.ts delete mode 100644 frontend/src/pages/api/integrations/StartIntegration.ts delete mode 100644 frontend/src/pages/api/integrations/getWorkspaceAuthorizations.ts delete mode 100644 frontend/src/pages/api/integrations/getWorkspaceIntegrations.ts delete mode 100644 frontend/src/pages/api/integrations/updateIntegration.ts delete mode 100644 frontend/src/pages/api/organization/GetOrg.ts delete mode 100644 frontend/src/pages/api/organization/GetOrgProjectMemberships.ts delete mode 100644 frontend/src/pages/api/organization/GetOrgProjects.ts delete mode 100644 frontend/src/pages/api/organization/StripeRedirect.ts delete mode 100644 frontend/src/pages/api/organization/addIncidentContact.ts delete mode 100644 frontend/src/pages/api/organization/changeUserRoleInOrganization.ts delete mode 100644 frontend/src/pages/api/organization/deleteIncidentContact.ts delete mode 100644 frontend/src/pages/api/organization/deleteUserFromOrganization.ts delete mode 100644 frontend/src/pages/api/organization/getIncidentContacts.ts delete mode 100644 frontend/src/pages/api/organization/renameOrg.ts delete mode 100644 frontend/src/pages/api/workspace/addUserToWorkspace.ts delete mode 100644 frontend/src/pages/api/workspace/changeUserRoleInWorkspace.ts delete mode 100644 frontend/src/pages/api/workspace/deleteUserFromWorkspace.ts delete mode 100644 frontend/src/pages/api/workspace/deleteWorkspace.ts delete mode 100644 frontend/src/pages/api/workspace/getWorkspaceKeys.ts delete mode 100644 frontend/src/pages/api/workspace/getWorkspaceTags.ts delete mode 100644 frontend/src/pages/api/workspace/getWorkspaceUsers.ts delete mode 100644 frontend/src/pages/api/workspace/getWorkspaces.ts diff --git a/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx b/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx deleted file mode 100644 index e3e8acaa1..000000000 --- a/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Fragment, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Dialog, Transition } from "@headlessui/react"; - -import addIncidentContact from "@app/pages/api/organization/addIncidentContact"; - -import Button from "../buttons/Button"; -import InputField from "../InputField"; - -type Props = { - isOpen: boolean; - closeModal: () => void; - incidentContacts: string[]; - setIncidentContacts: (arg: string[]) => void; -}; - -const AddIncidentContactDialog = ({ - isOpen, - closeModal, - incidentContacts, - setIncidentContacts -}: Props) => { - const [incidentContactEmail, setIncidentContactEmail] = useState(""); - const { t } = useTranslation(); - - const submit = () => { - setIncidentContacts( - incidentContacts?.length > 0 - ? incidentContacts.concat([incidentContactEmail]) - : [incidentContactEmail] - ); - addIncidentContact(localStorage.getItem("orgData.id") as string, incidentContactEmail); - closeModal(); - }; - return ( -
- - - -
- - -
-
- - - - {t("section.incident.add-dialog.title")} - -
-

- {t("section.incident.add-dialog.description")} -

-
-
- -
-
-
-
-
-
-
-
-
-
- ); -}; - -export default AddIncidentContactDialog; diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 5a9795cff..53a0e86f7 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -6,8 +6,10 @@ import { useNotificationContext } from "@app/components/context/Notifications/No import { Select, SelectItem } from "@app/components/v2"; import { useSubscription, useWorkspace } from "@app/context"; import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission"; -import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace"; -import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace"; +import { + useDeleteUserFromWorkspace, + useUpdateUserWorkspaceRole +} from "@app/hooks/api"; import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; import uploadKeys from "@app/pages/api/workspace/uploadKeys"; @@ -40,9 +42,11 @@ type EnvironmentProps = { const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => { const { currentWorkspace } = useWorkspace(); const { subscription } = useSubscription(); - const [roleSelected, setRoleSelected] = useState( - Array(userData?.length).fill(userData.map((user) => user.role)) - ); + const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace(); + const { mutateAsync: updateUserWorkspaceRoleMutateAsync } = useUpdateUserWorkspaceRole(); + // const [roleSelected, setRoleSelected] = useState( + // Array(userData?.length).fill(userData.map((user) => user.role)) + // ); const router = useRouter(); const [myRole, setMyRole] = useState("member"); const [workspaceEnvs, setWorkspaceEnvs] = useState([]); @@ -52,38 +56,15 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa const workspaceId = router.query.id as string; // Delete the row in the table (e.g. a user) // #TODO: Add a pop-up that warns you that the user is going to be deleted. - const handleDelete = (membershipId: string, index: number) => { - // setUserIdToBeDeleted(userId); - // onClick(); - deleteUserFromWorkspace(membershipId); - changeData(userData.filter((v, i) => i !== index)); - setRoleSelected([ - ...roleSelected.slice(0, index), - ...roleSelected.slice(index + 1, userData?.length) - ]); + const handleDelete = async (membershipId: string) => { + await deleteUserFromWorkspaceMutateAsync(membershipId); }; - // Update the rold of a certain user - const handleRoleUpdate = (index: number, e: string) => { - changeUserRoleInWorkspace(userData[index].membershipId, e.toLowerCase()); - changeData([ - ...userData.slice(0, index), - ...[ - { - key: userData[index].key, - firstName: userData[index].firstName, - lastName: userData[index].lastName, - email: userData[index].email, - role: e.toLocaleLowerCase(), - status: userData[index].status, - userId: userData[index].userId, - membershipId: userData[index].membershipId, - publicKey: userData[index].publicKey, - deniedPermissions: userData[index].deniedPermissions - } - ], - ...userData.slice(index + 1, userData?.length) - ]); + const handleRoleUpdate = async (index: number, e: string) => { + await updateUserWorkspaceRoleMutateAsync({ + membershipId: userData[index].membershipId, + role: e.toLowerCase() + }); createNotification({ text: "Successfully changed user role.", type: "success" @@ -373,7 +354,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa myRole !== "member" ? (
-
-
- ); -}; - -export default IntegrationTile; diff --git a/frontend/src/components/integrations/IntegrationSection.tsx b/frontend/src/components/integrations/IntegrationSection.tsx deleted file mode 100644 index 8ff114e35..000000000 --- a/frontend/src/components/integrations/IntegrationSection.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import IntegrationTile from "./Integration"; - -interface Props { - integrations: any; - setIntegrations: any; - bot: any; - setBot: any; - environments: Array<{ name: string; slug: string }>; - handleDeleteIntegration: (args: { integration: Integration }) => void; -} - -interface Integration { - _id: string; - isActive: boolean; - app: string | null; - appId: string | null; - path: string | null; - region: string | null; - createdAt: string; - updatedAt: string; - environment: string; - integration: string; - targetEnvironment: string; - workspace: string; - integrationAuth: string; - secretPath: string; -} - -const ProjectIntegrationSection = ({ - integrations, - setIntegrations, - bot, - setBot, - environments = [], - handleDeleteIntegration -}: Props) => { - return integrations.length > 0 ? ( -
-
-

Current Integrations

-

Manage integrations with third-party services.

-
- {integrations.map((integration: Integration) => { - return ( - - ); - })} -
- ) : ( -
- ); -}; - -export default ProjectIntegrationSection; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index c15baf1ef..c24d3b9cf 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -91,65 +91,55 @@ const initProjectHelper = async ({ organizationId: string; projectName: string; }) => { - let project; - try { + // create new project + const { data: { workspace } } = await createWorkspace({ + workspaceName: projectName, + organizationId + }); + + // create and upload new (encrypted) project key + const randomBytes = crypto.randomBytes(16).toString("hex"); + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + + if (!PRIVATE_KEY) throw new Error("Failed to find private key"); - // create new project - const { data: { workspace } } = await createWorkspace({ - workspaceName: projectName, - organizationId - }); - - project = workspace; + const user = await fetchUserDetails(); - // create and upload new (encrypted) project key - const randomBytes = crypto.randomBytes(16).toString("hex"); - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - if (!PRIVATE_KEY) throw new Error("Failed to find private key"); + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: randomBytes, + publicKey: user.publicKey, + privateKey: PRIVATE_KEY + }); - const user = await fetchUserDetails(); + await uploadKeys(workspace._id, user._id, ciphertext, nonce); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: user.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadKeys(project._id, user._id, ciphertext, nonce); - - const workspaceId = project._id; - - // encrypt and upload secrets to new project - const secrets = await encryptSecrets({ - secretsToEncrypt: secretsToBeAdded, - workspaceId, - env: "dev" - }); - - secrets?.forEach((secret) => { - createSecret({ - workspaceId, - environment: secret.environment, - type: secret.type, - secretKey: secret.secretName, - secretKeyCiphertext: secret.secretKeyCiphertext, - secretKeyIV: secret.secretKeyIV, - secretKeyTag: secret.secretKeyTag, - secretValueCiphertext: secret.secretValueCiphertext, - secretValueIV: secret.secretValueIV, - secretValueTag: secret.secretValueTag, - secretCommentCiphertext: secret.secretCommentCiphertext, - secretCommentIV: secret.secretCommentIV, - secretCommentTag: secret.secretCommentTag, - secretPath: "/" - }); - }); - } catch (err) { - console.error("Failed to init project in organization", err); - } - - return project; + // encrypt and upload secrets to new project + const secrets = await encryptSecrets({ + secretsToEncrypt: secretsToBeAdded, + workspaceId: workspace._id, + env: "dev" + }); + + secrets?.forEach((secret) => { + createSecret({ + workspaceId: workspace._id, + environment: secret.environment, + type: secret.type, + secretKey: secret.secretName, + secretKeyCiphertext: secret.secretKeyCiphertext, + secretKeyIV: secret.secretKeyIV, + secretKeyTag: secret.secretKeyTag, + secretValueCiphertext: secret.secretValueCiphertext, + secretValueIV: secret.secretValueIV, + secretValueTag: secret.secretValueTag, + secretCommentCiphertext: secret.secretCommentCiphertext, + secretCommentIV: secret.secretCommentIV, + secretCommentTag: secret.secretCommentTag, + secretPath: "/" + }); + }); + + return workspace; } export { diff --git a/frontend/src/hooks/api/incidentContacts/queries.tsx b/frontend/src/hooks/api/incidentContacts/queries.tsx index 708aaaf78..aecf56a3e 100644 --- a/frontend/src/hooks/api/incidentContacts/queries.tsx +++ b/frontend/src/hooks/api/incidentContacts/queries.tsx @@ -8,18 +8,16 @@ const incidentContactKeys = { getAllContact: (orgId: string) => ["org-incident-contacts", { orgId }] as const }; -const fetchOrgIncidentContacts = async (orgId: string) => { - const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>( - `/api/v1/organization/${orgId}/incidentContactOrg` - ); - - return data.incidentContactsOrg; -}; - export const useGetOrgIncidentContact = (orgId: string) => useQuery({ queryKey: incidentContactKeys.getAllContact(orgId), - queryFn: () => fetchOrgIncidentContacts(orgId), + queryFn: async () => { + const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>( + `/api/v1/organization/${orgId}/incidentContactOrg` + ); + + return data.incidentContactsOrg; + }, enabled: Boolean(orgId) }); diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index c1353b6c6..c982659cb 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -32,4 +32,4 @@ export const useDeleteIntegration = () => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); } }); -}; +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index f7bd3e973..13810febd 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -41,8 +41,10 @@ export const useRenameOrg = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, RenameOrgDTO>({ - mutationFn: ({ newOrgName, orgId }) => - apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }), + mutationFn: ({ newOrgName, orgId }) => { + console.log("useRenameOrg"); + return apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }); + }, onSuccess: () => { queryClient.invalidateQueries(organizationKeys.getUserOrganizations); } diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index 3134884b8..74900da0e 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -10,7 +10,6 @@ import { UserWsTags } from "./types"; - const workspaceTags = { getWsTags: (workspaceID: string) => ["workspace-tags", { workspaceID }] as const }; @@ -23,12 +22,13 @@ const fetchWsTag = async (workspaceID: string) => { return data.workspaceTags; }; -export const useGetWsTags = (workspaceID: string) => - useQuery({ +export const useGetWsTags = (workspaceID: string) => { + return useQuery({ queryKey: workspaceTags.getWsTags(workspaceID), queryFn: () => fetchWsTag(workspaceID), enabled: Boolean(workspaceID) }); +} export const useCreateWsTag = () => { const queryClient = useQueryClient(); @@ -59,4 +59,4 @@ export const useDeleteWsTag = () => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); -}; +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index e1b9fe402..cb421c633 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -3,10 +3,8 @@ export { useAddUserToOrg, useAddUserToWs, useCreateAPIKey, - useCreateMyAction, useDeleteAPIKey, useDeleteOrgMembership, - useGetMyActions, useGetMyAPIKeys, useGetMyIp, useGetMySessions, diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 938abf3aa..48dfdf564 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -158,8 +158,9 @@ export const useDeleteOrgMembership = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, DeletOrgMembershipDTO>({ - mutationFn: ({ membershipId, orgId }) => - apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`), + mutationFn: ({ membershipId, orgId }) => { + return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`) + }, onSuccess: (_, { orgId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(orgId)); } @@ -170,10 +171,11 @@ export const useUpdateOrgUserRole = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, UpdateOrgUserRoleDTO>({ - mutationFn: ({ organizationId, membershipId, role }) => - apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, { + mutationFn: ({ organizationId, membershipId, role }) => { + return apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, { role - }), + }); + }, onSuccess: (_, { organizationId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); }, diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index bd06fedd3..42d3467d1 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -1,6 +1,8 @@ export { + useAddUserToWorkspace, useCreateWorkspace, useCreateWsEnvironment, + useDeleteUserFromWorkspace, useDeleteWorkspace, useDeleteWsEnvironment, useGetUserWorkspaceMemberships, @@ -11,8 +13,9 @@ export { useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, useGetWorkspaceSecrets, + useGetWorkspaceUsers, useNameWorkspaceSecrets, useRenameWorkspace, useToggleAutoCapitalization, - useUpdateWsEnvironment -} from "./queries"; + useUpdateUserWorkspaceRole, + useUpdateWsEnvironment} from "./queries"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 860ee7757..e5c2d6c96 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -29,7 +29,8 @@ export const workspaceKeys = { getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], getAllUserWorkspace: ["workspaces"] as const, getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, - getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const + getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, + getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -218,7 +219,9 @@ export const useDeleteWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, DeleteWorkspaceDTO>({ - mutationFn: ({ workspaceID }) => apiRequest.delete(`/api/v1/workspace/${workspaceID}`), + mutationFn: ({ workspaceID }) => { + return apiRequest.delete(`/api/v1/workspace/${workspaceID}`); + }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } @@ -273,3 +276,74 @@ export const useDeleteWsEnvironment = () => { }); }; +export const useGetWorkspaceUsers = (workspaceId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceUsers(workspaceId), + queryFn: async () => { + const { data: { users } } = await apiRequest.get( + `/api/v1/workspace/${workspaceId}/users` + ); + return users; + }, + enabled: true + }); +} + +export const useAddUserToWorkspace = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + email, + workspaceId + }: { + email: string; + workspaceId: string; + }) => { + const { data: { invitee, latestKey } } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }); + + return ({ + invitee, + latestKey + }); + }, + onSuccess: (_, dto) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId)); + } + }); +}; + +export const useDeleteUserFromWorkspace = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (membershipId: string) => { + const { data: { deletedMembership } } = await apiRequest.delete(`/api/v1/membership/${membershipId}`); + return deletedMembership; + }, + onSuccess: (res) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(res.workspace)); + } + }); +}; + +export const useUpdateUserWorkspaceRole = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + role + }: { + membershipId: string; + role: string; + }) => { + const { data: { membership } } = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, { + role + }); + return membership; + }, + onSuccess: (res) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(res.workspace)); + } + }); +}; diff --git a/frontend/src/pages/api/integrations/DeleteIntegration.ts b/frontend/src/pages/api/integrations/DeleteIntegration.ts deleted file mode 100644 index b9253869a..000000000 --- a/frontend/src/pages/api/integrations/DeleteIntegration.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - integrationId: string; -} - -/** - * This route deletes an integration from a certain project - * @param {*} integrationId - * @returns - */ -const deleteIntegration = ({ integrationId }: Props) => - SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integration; - } - return undefined; - }); - -export default deleteIntegration; diff --git a/frontend/src/pages/api/integrations/DeleteIntegrationAuth.ts b/frontend/src/pages/api/integrations/DeleteIntegrationAuth.ts deleted file mode 100644 index c43de0b3a..000000000 --- a/frontend/src/pages/api/integrations/DeleteIntegrationAuth.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - integrationAuthId: string; -} - -/** - * This route deletes an integration authorization from a certain project - * @param {*} integrationAuthId - * @returns - */ -const deleteIntegrationAuth = ({ integrationAuthId }: Props) => - SecurityClient.fetchCall(`/api/v1/integration-auth/${integrationAuthId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integrationAuth; - } - console.log("Failed to delete an integration authorization"); - return undefined; - }); - -export default deleteIntegrationAuth; diff --git a/frontend/src/pages/api/integrations/GetIntegrationApps.ts b/frontend/src/pages/api/integrations/GetIntegrationApps.ts deleted file mode 100644 index c1784cf9c..000000000 --- a/frontend/src/pages/api/integrations/GetIntegrationApps.ts +++ /dev/null @@ -1,21 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - integrationAuthId: string; -} - -const getIntegrationApps = ({ integrationAuthId }: Props) => - SecurityClient.fetchCall(`/api/v1/integration-auth/${integrationAuthId}/apps`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).apps; - } - console.log("Failed to get available apps for an integration"); - return undefined; - }); - -export default getIntegrationApps; diff --git a/frontend/src/pages/api/integrations/GetIntegrationOptions.ts b/frontend/src/pages/api/integrations/GetIntegrationOptions.ts deleted file mode 100644 index eacdb9e9d..000000000 --- a/frontend/src/pages/api/integrations/GetIntegrationOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -const getIntegrationOptions = () => - SecurityClient.fetchCall("/api/v1/integration-auth/integration-options", { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integrationOptions; - } - console.log("Failed to get (cloud) integration options"); - return undefined; - }); - -export default getIntegrationOptions; diff --git a/frontend/src/pages/api/integrations/StartIntegration.ts b/frontend/src/pages/api/integrations/StartIntegration.ts deleted file mode 100644 index 9172b378b..000000000 --- a/frontend/src/pages/api/integrations/StartIntegration.ts +++ /dev/null @@ -1,35 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - integrationId: string; - appName: string; - environment: string; -} - -/** - * This route starts the integration after teh default one if gonna set up. - * @param {*} integrationId - * @returns - */ -const startIntegration = ({ integrationId, appName, environment }: Props) => - SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - update: { - app: appName, - environment, - isActive: true - } - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to start an integration"); - return undefined; - }); - -export default startIntegration; diff --git a/frontend/src/pages/api/integrations/getWorkspaceAuthorizations.ts b/frontend/src/pages/api/integrations/getWorkspaceAuthorizations.ts deleted file mode 100644 index 2c5822049..000000000 --- a/frontend/src/pages/api/integrations/getWorkspaceAuthorizations.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - workspaceId: string; -} - -/** - * This route gets authorizations of a certain project (Heroku, etc.) - * @param {*} workspaceId - * @returns - */ -const getWorkspaceAuthorizations = ({ workspaceId }: Props) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/authorizations`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).authorizations; - } - console.log("Failed to get project authorizations"); - return undefined; - }); - -export default getWorkspaceAuthorizations; diff --git a/frontend/src/pages/api/integrations/getWorkspaceIntegrations.ts b/frontend/src/pages/api/integrations/getWorkspaceIntegrations.ts deleted file mode 100644 index 19a4b78c9..000000000 --- a/frontend/src/pages/api/integrations/getWorkspaceIntegrations.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - workspaceId: string; -} - -/** - * This route gets integrations of a certain project (Heroku, etc.) - * @param {*} workspaceId - * @returns - */ -const getWorkspaceIntegrations = ({ workspaceId }: Props) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/integrations`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integrations; - } - console.log("Failed to get the project integrations"); - return undefined; - }); - -export default getWorkspaceIntegrations; diff --git a/frontend/src/pages/api/integrations/updateIntegration.ts b/frontend/src/pages/api/integrations/updateIntegration.ts deleted file mode 100644 index dbcdea899..000000000 --- a/frontend/src/pages/api/integrations/updateIntegration.ts +++ /dev/null @@ -1,55 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route starts the integration after teh default one if gonna set up. - * Update integration with id [integrationId] to sync envars from the project's - * [environment] to the integration [app] with active state [isActive] - * @param {Object} obj - * @param {String} obj.integrationId - id of integration - * @param {Boolean} obj.isActive - active state - * @param {String} obj.environment - project environment to push secrets from - * @param {String} obj.app - name of app - * @param {String} obj.appId - (optional) app ID for integration - * @param {String} obj.targetEnvironment - target environment for integration - * @param {String} obj.owner - (optional) owner login of repo for GitHub integration - * @returns - */ -const updateIntegration = ({ - integrationId, - isActive, - environment, - app, - appId, - targetEnvironment, - owner -}: { - integrationId: string; - isActive: boolean; - environment: string; - app: string; - appId: string | null; - targetEnvironment: string | null; - owner: string | null; -}) => - SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - app, - environment, - isActive, - appId, - targetEnvironment, - owner - }) - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integration; - } - console.log("Failed to start an integration"); - return undefined; - }); - -export default updateIntegration; diff --git a/frontend/src/pages/api/organization/GetOrg.ts b/frontend/src/pages/api/organization/GetOrg.ts deleted file mode 100644 index 8009a4b3b..000000000 --- a/frontend/src/pages/api/organization/GetOrg.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get info about a certain org - * @param {string} orgId - the organization ID - * @returns - */ -const getOrganization = ({ orgId }: { orgId: string }) => - SecurityClient.fetchCall(`/api/v1/organization/${orgId}`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).organization; - } - console.log("Failed to get org info"); - return undefined; - }); - -export default getOrganization; diff --git a/frontend/src/pages/api/organization/GetOrgProjectMemberships.ts b/frontend/src/pages/api/organization/GetOrgProjectMemberships.ts deleted file mode 100644 index 72c871c21..000000000 --- a/frontend/src/pages/api/organization/GetOrgProjectMemberships.ts +++ /dev/null @@ -1,24 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get all the project memebrships of users in an org. - * @param {*} req - * @param {*} res - * @returns - */ - -const getOrganizationProjectMemberships = (req: { orgId: string }) => - SecurityClient.fetchCall(`/api/v1/organization/${req.orgId}/workspace-memberships`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to get project memberships for users in an org"); - return undefined; - }); - -export default getOrganizationProjectMemberships; diff --git a/frontend/src/pages/api/organization/GetOrgProjects.ts b/frontend/src/pages/api/organization/GetOrgProjects.ts deleted file mode 100644 index 2e374e031..000000000 --- a/frontend/src/pages/api/organization/GetOrgProjects.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get all the users in an org. - * @param {*} req - * @param {*} res - * @returns - */ - -// TODO: this file is not used anywhere -const getOrganizationProjects = (req: { orgId: string }) => - SecurityClient.fetchCall(`/api/organization/${req.orgId}/workspaces`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).workspaces; - } - console.log("Failed to get projects for an org"); - return undefined; - }); - -export default getOrganizationProjects; diff --git a/frontend/src/pages/api/organization/StripeRedirect.ts b/frontend/src/pages/api/organization/StripeRedirect.ts deleted file mode 100644 index 50a359b58..000000000 --- a/frontend/src/pages/api/organization/StripeRedirect.ts +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route redirects the user to the right stripe billing page. - * @param {*} req - * @param {*} res - * @returns - */ -const StripeRedirect = ({ orgId }: { orgId: string }) => - SecurityClient.fetchCall(`/api/v1/organization/${orgId}/customer-portal-session`, { - method: "POST", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - window.location.href = (await res.json()).url; - return; - } - console.log("Failed to redirect to Stripe"); - }); - -export default StripeRedirect; diff --git a/frontend/src/pages/api/organization/addIncidentContact.ts b/frontend/src/pages/api/organization/addIncidentContact.ts deleted file mode 100644 index fe3407025..000000000 --- a/frontend/src/pages/api/organization/addIncidentContact.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route add an incident contact email to a certain organization - * @param {*} param0 - * @returns - */ -const addIncidentContact = (organizationId: string, email: string) => - SecurityClient.fetchCall(`/api/v1/organization/${organizationId}/incidentContactOrg`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to add an incident contact"); - return undefined; - }); - -export default addIncidentContact; diff --git a/frontend/src/pages/api/organization/changeUserRoleInOrganization.ts b/frontend/src/pages/api/organization/changeUserRoleInOrganization.ts deleted file mode 100644 index da9d20e0f..000000000 --- a/frontend/src/pages/api/organization/changeUserRoleInOrganization.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function change the access of a user in a certain organization - * @param {string} organizationId - * @param {string} membershipId - * @param {string} role - * @returns - */ -const changeUserRoleInOrganization = (organizationId: string, membershipId: string, role: string) => - SecurityClient.fetchCall(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - role - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to change the user role in an org"); - return undefined; - }); - -export default changeUserRoleInOrganization; diff --git a/frontend/src/pages/api/organization/deleteIncidentContact.ts b/frontend/src/pages/api/organization/deleteIncidentContact.ts deleted file mode 100644 index 5375384c6..000000000 --- a/frontend/src/pages/api/organization/deleteIncidentContact.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route deletes an incident Contact from a certain organization - * @param {*} param0 - * @returns - */ -const deleteIncidentContact = (organizationId: string, email: string) => - SecurityClient.fetchCall(`/api/v1/organization/${organizationId}/incidentContactOrg`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to delete an incident contact"); - return undefined; - }); - -export default deleteIncidentContact; diff --git a/frontend/src/pages/api/organization/deleteUserFromOrganization.ts b/frontend/src/pages/api/organization/deleteUserFromOrganization.ts deleted file mode 100644 index 3041b7b71..000000000 --- a/frontend/src/pages/api/organization/deleteUserFromOrganization.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function removes a certain member from a certain organization - * @param {*} membershipId - * @returns - */ -const deleteUserFromOrganization = (membershipId: string) => - SecurityClient.fetchCall(`/api/v1/membership-org/${membershipId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to delete a user from an org"); - return undefined; - }); - -export default deleteUserFromOrganization; diff --git a/frontend/src/pages/api/organization/getIncidentContacts.ts b/frontend/src/pages/api/organization/getIncidentContacts.ts deleted file mode 100644 index 5ed7760ac..000000000 --- a/frontend/src/pages/api/organization/getIncidentContacts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -export interface IIncidentContactOrg { - _id: string; - email: string; - organization: string; -} -/** - * This routes gets all the incident contacts of a certain organization - * @param {*} workspaceId - * @returns - */ -const getIncidentContacts = (organizationId: string): Promise => - SecurityClient.fetchCall(`/api/v1/organization/${organizationId}/incidentContactOrg`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).incidentContactsOrg; - } - console.log("Failed to get incident contacts"); - return undefined; - }); - -export default getIncidentContacts; diff --git a/frontend/src/pages/api/organization/renameOrg.ts b/frontend/src/pages/api/organization/renameOrg.ts deleted file mode 100644 index 53fb7a51b..000000000 --- a/frontend/src/pages/api/organization/renameOrg.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us rename a certain org. - * @param {*} req - * @param {*} res - * @returns - */ -const renameOrg = (orgId: string, newOrgName: string) => - SecurityClient.fetchCall(`/api/v1/organization/${orgId}/name`, { - method: "PATCH", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - name: newOrgName - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to rename an organization"); - return undefined; - }); - -export default renameOrg; diff --git a/frontend/src/pages/api/workspace/addUserToWorkspace.ts b/frontend/src/pages/api/workspace/addUserToWorkspace.ts deleted file mode 100644 index 19612ec1d..000000000 --- a/frontend/src/pages/api/workspace/addUserToWorkspace.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function adds a user to a project - * @param {*} email - * @param {*} workspaceId - * @returns - */ -const addUserToWorkspace = (email: string, workspaceId: string) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/invite-signup`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to add a user to project"); - return undefined; - }); - -export default addUserToWorkspace; diff --git a/frontend/src/pages/api/workspace/changeUserRoleInWorkspace.ts b/frontend/src/pages/api/workspace/changeUserRoleInWorkspace.ts deleted file mode 100644 index 21ea91c45..000000000 --- a/frontend/src/pages/api/workspace/changeUserRoleInWorkspace.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function change the access of a user in a certain workspace - * @param {*} membershipId - * @param {*} role - * @returns - */ -const changeUserRoleInWorkspace = (membershipId: string, role: string) => - SecurityClient.fetchCall(`/api/v1/membership/${membershipId}/change-role`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - role - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to change the user role in a project"); - return undefined; - }); - -export default changeUserRoleInWorkspace; diff --git a/frontend/src/pages/api/workspace/deleteUserFromWorkspace.ts b/frontend/src/pages/api/workspace/deleteUserFromWorkspace.ts deleted file mode 100644 index 33927f1c8..000000000 --- a/frontend/src/pages/api/workspace/deleteUserFromWorkspace.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function removes a certain member from a certain workspace - * @param {*} membershipId - * @returns - */ -const deleteUserFromWorkspace = (membershipId: string) => - SecurityClient.fetchCall(`/api/v1/membership/${membershipId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to delete a user from a project"); - return undefined; - }); - -export default deleteUserFromWorkspace; diff --git a/frontend/src/pages/api/workspace/deleteWorkspace.ts b/frontend/src/pages/api/workspace/deleteWorkspace.ts deleted file mode 100644 index 7e1551613..000000000 --- a/frontend/src/pages/api/workspace/deleteWorkspace.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route deletes a specified workspace. - * @param {*} workspaceId - * @returns - */ -const deleteWorkspace = (workspaceId: string) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to delete a project"); - return undefined; - }); - -export default deleteWorkspace; diff --git a/frontend/src/pages/api/workspace/getWorkspaceKeys.ts b/frontend/src/pages/api/workspace/getWorkspaceKeys.ts deleted file mode 100644 index e4c4475e9..000000000 --- a/frontend/src/pages/api/workspace/getWorkspaceKeys.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get the public keys of everyone in your workspace. - * @param {string} workspaceId - * @returns - */ -const getWorkspaceKeys = ({ workspaceId }: { workspaceId: string }) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/keys`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).publicKeys; - } - console.log("Failed to get the public keys of everyone in the workspace"); - return undefined; - }); - -export default getWorkspaceKeys; diff --git a/frontend/src/pages/api/workspace/getWorkspaceTags.ts b/frontend/src/pages/api/workspace/getWorkspaceTags.ts deleted file mode 100644 index 535731bfb..000000000 --- a/frontend/src/pages/api/workspace/getWorkspaceTags.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get the tags for a certain project - * @param {string} workspaceId - * @returns - */ -const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) => - SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).workspaceTags; - } - console.log("Failed to get the tags available in a certain project"); - return undefined; - }); - -export default getWorkspaceTags; diff --git a/frontend/src/pages/api/workspace/getWorkspaceUsers.ts b/frontend/src/pages/api/workspace/getWorkspaceUsers.ts deleted file mode 100644 index 4ef0a8dbc..000000000 --- a/frontend/src/pages/api/workspace/getWorkspaceUsers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get all the users in the workspace. - * @param {string} workspaceId - workspace ID - * @returns - */ -const getWorkspaceUsers = ({ workspaceId }: { workspaceId: string }) => - SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/users`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).users; - } - console.log("Failed to get Project Users"); - return undefined; - }); - -export default getWorkspaceUsers; diff --git a/frontend/src/pages/api/workspace/getWorkspaces.ts b/frontend/src/pages/api/workspace/getWorkspaces.ts deleted file mode 100644 index 4a08c9d83..000000000 --- a/frontend/src/pages/api/workspace/getWorkspaces.ts +++ /dev/null @@ -1,31 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Workspace { - __v: number; - _id: string; - name: string; - autoCapitalization: boolean; - organization: string; - environments: Array<{ name: string; slug: string }>; -} - -/** - * This route lets us get the workspaces of a certain user - * @returns - */ -const getWorkspaces = () => - SecurityClient.fetchCall("/api/v1/workspace", { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - const data = (await res.json()) as unknown as { workspaces: Workspace[] }; - return data.workspaces; - } - - throw new Error("Failed to get projects"); - }); - -export default getWorkspaces; diff --git a/frontend/src/pages/project/[id]/members/index.tsx b/frontend/src/pages/project/[id]/members/index.tsx index cbd13d208..5287c0430 100644 --- a/frontend/src/pages/project/[id]/members/index.tsx +++ b/frontend/src/pages/project/[id]/members/index.tsx @@ -11,15 +11,13 @@ import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMembe import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable"; import guidGenerator from "@app/components/utilities/randomId"; import { Input } from "@app/components/v2"; -import { useGetUser } from "@app/hooks/api"; +import { useAddUserToWorkspace,useGetUser , useGetWorkspaceUsers } from "@app/hooks/api"; import { decryptAssymmetric, encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; import getOrganizationUsers from "../../../api/organization/GetOrgUsers"; -import addUserToWorkspace from "../../../api/workspace/addUserToWorkspace"; -import getWorkspaceUsers from "../../../api/workspace/getWorkspaceUsers"; import uploadKeys from "../../../api/workspace/uploadKeys"; interface UserProps { @@ -42,7 +40,13 @@ interface MembershipProps { // #TODO: Update all the workspaceIds export default function Users() { + const router = useRouter(); + const workspaceId = router.query.id as string; + const { data: user } = useGetUser(); + const { data: workspaceUsers } = useGetWorkspaceUsers(workspaceId); + const { mutateAsync: addUserToWorkspaceMutateAsync } = useAddUserToWorkspace(); + const [isAddOpen, setIsAddOpen] = useState(false); // let [isDeleteOpen, setIsDeleteOpen] = useState(false); // let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false); @@ -52,22 +56,16 @@ export default function Users() { const { t } = useTranslation(); - const router = useRouter(); - const workspaceId = router.query.id as string; const [userList, setUserList] = useState([]); const [isUserListLoading, setIsUserListLoading] = useState(true); const [orgUserList, setOrgUserList] = useState([]); useEffect(() => { - if (user) { + if (user && workspaceUsers) { (async () => { setPersonalEmail(user.email); - - // This part quiries the current users of a project - const workspaceUsers = await getWorkspaceUsers({ - workspaceId - }); + const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({ key: guidGenerator(), firstName: membership.user?.firstName, @@ -100,7 +98,7 @@ export default function Users() { ); })(); } - }, [user]); + }, [user, workspaceUsers]); const closeAddModal = () => { setIsAddOpen(false); @@ -123,7 +121,11 @@ export default function Users() { // } const submitAddModal = async () => { - const result = await addUserToWorkspace(email, workspaceId); + const result = await addUserToWorkspaceMutateAsync({ + email, + workspaceId + }); + if (result?.invitee && result?.latestKey) { const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; @@ -145,7 +147,6 @@ export default function Users() { } setEmail(""); setIsAddOpen(false); - router.reload(); }; return userList ? ( From 78802409bdc362dcb4753e697d6d9d01d670ce10 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 10 Aug 2023 14:15:24 +0700 Subject: [PATCH 36/64] Move all integration queries/mutations to hooks --- .../src/hooks/api/integrationAuth/index.tsx | 5 +- .../src/hooks/api/integrationAuth/queries.tsx | 63 +++++++++++++++++ frontend/src/hooks/api/integrations/index.tsx | 6 +- .../src/hooks/api/integrations/queries.tsx | 57 ++++++++++++++++ .../integrations/ChangeHerokuConfigVars.ts | 37 ---------- .../api/integrations/authorizeIntegration.ts | 35 ---------- .../api/integrations/createIntegration.ts | 67 ------------------- .../saveIntegrationAccessToken.ts | 53 --------------- .../aws-parameter-store/authorize.tsx | 9 ++- .../aws-parameter-store/create.tsx | 8 ++- .../aws-secret-manager/authorize.tsx | 7 +- .../aws-secret-manager/create.tsx | 8 ++- .../integrations/azure-key-vault/create.tsx | 8 ++- .../azure-key-vault/oauth2/callback.tsx | 7 +- .../pages/integrations/bitbucket/create.tsx | 8 ++- .../bitbucket/oauth2/callback.tsx | 7 +- .../pages/integrations/checkly/authorize.tsx | 9 ++- .../src/pages/integrations/checkly/create.tsx | 7 +- .../pages/integrations/circleci/authorize.tsx | 9 ++- .../pages/integrations/circleci/create.tsx | 8 ++- .../pages/integrations/cloud-66/authorize.tsx | 9 ++- .../pages/integrations/cloud-66/create.tsx | 8 ++- .../cloudflare-pages/authorize.tsx | 9 ++- .../integrations/cloudflare-pages/create.tsx | 9 ++- .../integrations/codefresh/authorize.tsx | 9 ++- .../pages/integrations/codefresh/create.tsx | 8 ++- .../digital-ocean-app-platform/authorize.tsx | 9 ++- .../digital-ocean-app-platform/create.tsx | 8 ++- .../pages/integrations/flyio/authorize.tsx | 9 ++- .../src/pages/integrations/flyio/create.tsx | 8 ++- .../src/pages/integrations/github/create.tsx | 8 ++- .../integrations/github/oauth2/callback.tsx | 8 ++- .../src/pages/integrations/gitlab/create.tsx | 8 ++- .../integrations/gitlab/oauth2/callback.tsx | 8 ++- .../hashicorp-vault/authorize.tsx | 8 ++- .../integrations/hashicorp-vault/create.tsx | 8 ++- .../src/pages/integrations/heroku/create.tsx | 8 ++- .../integrations/heroku/oauth2/callback.tsx | 7 +- .../integrations/laravel-forge/authorize.tsx | 9 ++- .../integrations/laravel-forge/create.tsx | 8 ++- .../src/pages/integrations/netlify/create.tsx | 8 ++- .../integrations/netlify/oauth2/callback.tsx | 7 +- .../integrations/northflank/authorize.tsx | 9 ++- .../pages/integrations/northflank/create.tsx | 8 ++- .../pages/integrations/railway/authorize.tsx | 9 ++- .../src/pages/integrations/railway/create.tsx | 8 ++- .../pages/integrations/render/authorize.tsx | 7 +- .../src/pages/integrations/render/create.tsx | 8 ++- .../pages/integrations/supabase/authorize.tsx | 9 ++- .../pages/integrations/supabase/create.tsx | 8 ++- .../pages/integrations/teamcity/authorize.tsx | 9 ++- .../pages/integrations/teamcity/create.tsx | 8 ++- .../terraform-cloud/authorize.tsx | 9 ++- .../integrations/terraform-cloud/create.tsx | 6 +- .../pages/integrations/travisci/authorize.tsx | 9 ++- .../pages/integrations/travisci/create.tsx | 8 ++- .../src/pages/integrations/vercel/create.tsx | 8 ++- .../integrations/vercel/oauth2/callback.tsx | 9 ++- .../pages/integrations/windmill/authorize.tsx | 9 ++- .../pages/integrations/windmill/create.tsx | 8 ++- 60 files changed, 448 insertions(+), 300 deletions(-) delete mode 100644 frontend/src/pages/api/integrations/ChangeHerokuConfigVars.ts delete mode 100644 frontend/src/pages/api/integrations/authorizeIntegration.ts delete mode 100644 frontend/src/pages/api/integrations/createIntegration.ts delete mode 100644 frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index e5e53c808..d6e1b11e3 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -1,4 +1,5 @@ export { + useAuthorizeIntegration, useDeleteIntegrationAuth, useGetIntegrationAuthApps, useGetIntegrationAuthBitBucketWorkspaces, @@ -7,4 +8,6 @@ export { useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, useGetIntegrationAuthTeams, - useGetIntegrationAuthVercelBranches} from "./queries"; + useGetIntegrationAuthVercelBranches, + useSaveIntegrationAccessToken +} from "./queries"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index bf9b5c940..1ec08d839 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -312,6 +312,69 @@ export const useGetIntegrationAuthNorthflankSecretGroups = ({ }); }; +export const useAuthorizeIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + workspaceId, + code, + integration + }: { + workspaceId: string; + code: string; + integration: string; + }) => { + const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/oauth-token", { + workspaceId, + code, + integration + }); + + return integrationAuth; + }, + onSuccess: (res) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(res.workspace)); + } + }); +}; + +export const useSaveIntegrationAccessToken = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + workspaceId, + integration, + accessId, + accessToken, + url, + namespace + }: { + workspaceId: string | null; + integration: string | undefined; + accessId: string | null; + accessToken: string; + url: string | null; + namespace: string | null; + }) => { + const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/access-token", { + workspaceId, + integration, + accessId, + accessToken, + url, + namespace + }); + + return integrationAuth; + }, + onSuccess: (res) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(res.workspace)); + } + }); +}; + export const useDeleteIntegrationAuth = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/integrations/index.tsx b/frontend/src/hooks/api/integrations/index.tsx index 3ad1051f4..d0eb50ca6 100644 --- a/frontend/src/hooks/api/integrations/index.tsx +++ b/frontend/src/hooks/api/integrations/index.tsx @@ -1 +1,5 @@ -export { useDeleteIntegration,useGetCloudIntegrations } from "./queries"; +export { + useCreateIntegration, + useDeleteIntegration, + useGetCloudIntegrations +} from "./queries"; diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index c982659cb..bfca2f407 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -23,6 +23,63 @@ export const useGetCloudIntegrations = () => queryFn: () => fetchIntegrations() }); +export const useCreateIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + integrationAuthId, + isActive, + app, + appId, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + secretPath + }: { + integrationAuthId: string; + isActive: boolean; + secretPath: string; + app: string | null; + appId: string | null; + sourceEnvironment: string; + targetEnvironment: string | null; + targetEnvironmentId: string | null; + targetService: string | null; + targetServiceId: string | null; + owner: string | null; + path: string | null; + region: string | null; + }) => { + const { data: { integration } } = await apiRequest.post("/api/v1/integration", { + integrationAuthId, + isActive, + app, + appId, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + secretPath + }); + + return integration; + }, + onSuccess: (res) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(res.workspace)); + } + }); +}; + export const useDeleteIntegration = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/pages/api/integrations/ChangeHerokuConfigVars.ts b/frontend/src/pages/api/integrations/ChangeHerokuConfigVars.ts deleted file mode 100644 index 5d79ebc64..000000000 --- a/frontend/src/pages/api/integrations/ChangeHerokuConfigVars.ts +++ /dev/null @@ -1,37 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - integrationId: string; - key: { encryptedKey: any; nonce: any }; - secrets: { - ciphertextKey: any; - ivKey: any; - tagKey: any; - hashKey: any; - ciphertextValue: any; - ivValue: any; - tagValue: any; - hashValue: any; - type: string; - }[]; -} - -const changeHerokuConfigVars = ({ integrationId, key, secrets }: Props) => - SecurityClient.fetchCall(`/api/v1/integration/${integrationId}/sync`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - key, - secrets - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to sync secrets to Heroku"); - return undefined; - }); - -export default changeHerokuConfigVars; diff --git a/frontend/src/pages/api/integrations/authorizeIntegration.ts b/frontend/src/pages/api/integrations/authorizeIntegration.ts deleted file mode 100644 index b80a6e36d..000000000 --- a/frontend/src/pages/api/integrations/authorizeIntegration.ts +++ /dev/null @@ -1,35 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - workspaceId: string; - code: string; - integration: string; -} -/** - * This is the first step of the change password process (pake) - * @param {object} obj - * @param {object} obj.workspaceId - project id for which we want to authorize the integration - * @param {object} obj.code - * @param {object} obj.integration - integration which a user is trying to turn on - * @returns - */ -const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) => - SecurityClient.fetchCall("/api/v1/integration-auth/oauth-token", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - workspaceId, - code, - integration - }) - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integrationAuth; - } - console.log("Failed to authorize the integration"); - return undefined; - }); - -export default AuthorizeIntegration; diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts deleted file mode 100644 index 9235e7176..000000000 --- a/frontend/src/pages/api/integrations/createIntegration.ts +++ /dev/null @@ -1,67 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - integrationAuthId: string; - isActive: boolean; - secretPath: string; - app: string | null; - appId: string | null; - sourceEnvironment: string; - targetEnvironment: string | null; - targetEnvironmentId: string | null; - targetService: string | null; - targetServiceId: string | null; - owner: string | null; - path: string | null; - region: string | null; -} -/** - * This route creates a new integration based on the integration authorization with id [integrationAuthId] - * @param {Object} obj - * @param {String} obj.accessToken - id of integration authorization for which to create the integration - * @returns - */ -const createIntegration = ({ - integrationAuthId, - isActive, - app, - appId, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region, - secretPath, -}: Props) => - SecurityClient.fetchCall("/api/v1/integration", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - integrationAuthId, - isActive, - app, - appId, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region, - secretPath, - }) - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integration; - } - console.log("Failed to create integration"); - return undefined; - }); - -export default createIntegration; diff --git a/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts b/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts deleted file mode 100644 index 5da617783..000000000 --- a/frontend/src/pages/api/integrations/saveIntegrationAccessToken.ts +++ /dev/null @@ -1,53 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - workspaceId: string | null; - integration: string | undefined; - accessId: string | null; - accessToken: string; - url: string | null; - namespace: string | null; -} -/** - * This route creates a new integration authorization for integration [integration] - * that requires the user to input their access token manually (e.g. Render). It - * saves access token [accessToken] under that integration for workspace with id - * [workspaceId]. - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace to authorize integration for - * @param {String} obj.integration - integration - * @param {String} obj.accessToken - access token to save - * @param {String} obj.url - URL of the Vault instance - * @param {String} obj.namespace - Vault-specific namespace param - * @returns - */ -const saveIntegrationAccessToken = ({ - workspaceId, - integration, - accessId, - accessToken, - url, - namespace -}: Props) => - SecurityClient.fetchCall("/api/v1/integration-auth/access-token", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - workspaceId, - integration, - accessId, - accessToken, - url, - namespace - }) - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).integrationAuth; - } - console.log("Failed to save integration access details"); - return undefined; - }); - -export default saveIntegrationAccessToken; diff --git a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx index 230c8b6ea..d71273fa5 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function AWSParameterStoreAuthorizeIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [isLoading, setIsLoading] = useState(false); const [accessKey, setAccessKey] = useState(""); @@ -30,7 +35,7 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "aws-parameter-store", accessId: accessKey, diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx index 6f7f45bc6..b537a9827 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -13,7 +17,6 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; const awsRegions = [ { name: "US East (Ohio)", slug: "us-east-2" }, @@ -49,6 +52,7 @@ const awsRegions = [ export default function AWSParameterStoreCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -90,7 +94,7 @@ export default function AWSParameterStoreCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: null, diff --git a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx index 664a66087..11da85224 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx @@ -1,11 +1,14 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { useSaveIntegrationAccessToken } from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function AWSSecretManagerCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [isLoading, setIsLoading] = useState(false); const [accessKey, setAccessKey] = useState(""); @@ -30,7 +33,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "aws-secret-manager", accessId: accessKey, diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index 7baab5813..b919a6daf 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -13,7 +17,6 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; const awsRegions = [ { name: "US East (Ohio)", slug: "us-east-2" }, @@ -49,6 +52,7 @@ const awsRegions = [ export default function AWSSecretManagerCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -89,7 +93,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetSecretName.trim(), diff --git a/frontend/src/pages/integrations/azure-key-vault/create.tsx b/frontend/src/pages/integrations/azure-key-vault/create.tsx index 963fdf530..7bc6fc9f0 100644 --- a/frontend/src/pages/integrations/azure-key-vault/create.tsx +++ b/frontend/src/pages/integrations/azure-key-vault/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -13,10 +17,10 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function AzureKeyVaultCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -52,7 +56,7 @@ export default function AzureKeyVaultCreateIntegrationPage() { if (!integrationAuth?._id) return; setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: vaultBaseUrl, diff --git a/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx index 32364d85b..2f23f729f 100644 --- a/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx @@ -2,10 +2,13 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function AzureKeyVaultOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); const { code, state } = queryString.parse(router.asPath.split("?")[1]); @@ -16,7 +19,7 @@ export default function AzureKeyVaultOAuth2CallbackPage() { if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await AuthorizeIntegration({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "azure-key-vault" diff --git a/frontend/src/pages/integrations/bitbucket/create.tsx b/frontend/src/pages/integrations/bitbucket/create.tsx index 6bdc87f0d..1901360af 100644 --- a/frontend/src/pages/integrations/bitbucket/create.tsx +++ b/frontend/src/pages/integrations/bitbucket/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -17,10 +21,10 @@ import { useGetIntegrationAuthById, } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function BitBucketCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const [targetAppId, setTargetAppId] = useState(""); const [targetEnvironmentId, setTargetEnvironmentId] = useState(""); @@ -79,7 +83,7 @@ export default function BitBucketCreateIntegrationPage() { if (!targetApp || !targetApp.appId || !targetEnvironment) return; - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp.name, diff --git a/frontend/src/pages/integrations/bitbucket/oauth2/callback.tsx b/frontend/src/pages/integrations/bitbucket/oauth2/callback.tsx index 43a58bc6f..a7326722a 100644 --- a/frontend/src/pages/integrations/bitbucket/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/bitbucket/oauth2/callback.tsx @@ -2,10 +2,13 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function BitBucketOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); const { code, state } = queryString.parse(router.asPath.split("?")[1]); useEffect(() => { @@ -15,7 +18,7 @@ export default function BitBucketOAuth2CallbackPage() { if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await AuthorizeIntegration({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "bitbucket" diff --git a/frontend/src/pages/integrations/checkly/authorize.tsx b/frontend/src/pages/integrations/checkly/authorize.tsx index 73ad821aa..a20084c1b 100644 --- a/frontend/src/pages/integrations/checkly/authorize.tsx +++ b/frontend/src/pages/integrations/checkly/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function ChecklyCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [accessToken, setAccessToken] = useState(""); const [accessTokenErrorText, setAccessTokenErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function ChecklyCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "checkly", accessId: null, diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index 2ae0435cf..7f3b5397b 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -11,16 +11,19 @@ import { Select, SelectItem } from "@app/components/v2"; +import { + useCreateIntegration +} from "@app/hooks/api"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function ChecklyCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -62,7 +65,7 @@ export default function ChecklyCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/circleci/authorize.tsx b/frontend/src/pages/integrations/circleci/authorize.tsx index efb8a0ad9..e2d757897 100644 --- a/frontend/src/pages/integrations/circleci/authorize.tsx +++ b/frontend/src/pages/integrations/circleci/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function CircleCICreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function CircleCICreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "circleci", accessToken: apiKey, diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx index 162cd565c..199b4bece 100644 --- a/frontend/src/pages/integrations/circleci/create.tsx +++ b/frontend/src/pages/integrations/circleci/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function CircleCICreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -58,7 +62,7 @@ export default function CircleCICreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/cloud-66/authorize.tsx b/frontend/src/pages/integrations/cloud-66/authorize.tsx index 8c6434936..0697e7fd1 100644 --- a/frontend/src/pages/integrations/cloud-66/authorize.tsx +++ b/frontend/src/pages/integrations/cloud-66/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function Cloud66CreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function Cloud66CreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "cloud-66", accessId: null, diff --git a/frontend/src/pages/integrations/cloud-66/create.tsx b/frontend/src/pages/integrations/cloud-66/create.tsx index dbf566464..76e874542 100644 --- a/frontend/src/pages/integrations/cloud-66/create.tsx +++ b/frontend/src/pages/integrations/cloud-66/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById, } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function Cloud66CreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function Cloud66CreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx b/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx index 6e4b134d1..d63a3de43 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button,Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function CloudflarePagesIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [accessKey, setAccessKey] = useState(""); const [accessKeyErrorText, setAccessKeyErrorText] = useState(""); const [accountId, setAccountId] = useState(""); @@ -24,7 +29,7 @@ export default function CloudflarePagesIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "cloudflare-pages", accessId: accountId, diff --git a/frontend/src/pages/integrations/cloudflare-pages/create.tsx b/frontend/src/pages/integrations/cloudflare-pages/create.tsx index 6b7bc8d3c..49ea9b191 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/create.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/create.tsx @@ -2,10 +2,12 @@ import { useEffect,useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +, useGetWorkspaceById } from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2"; -import { useGetWorkspaceById } from "../../../hooks/api"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; -import createIntegration from "../../api/integrations/createIntegration"; const cloudflareEnvironments = [ { name: "Production", slug: "production" }, @@ -14,6 +16,7 @@ const cloudflareEnvironments = [ export default function CloudflarePagesIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); @@ -54,7 +57,7 @@ export default function CloudflarePagesIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/codefresh/authorize.tsx b/frontend/src/pages/integrations/codefresh/authorize.tsx index 17416ffab..67d857bdb 100644 --- a/frontend/src/pages/integrations/codefresh/authorize.tsx +++ b/frontend/src/pages/integrations/codefresh/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function CodefreshCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function CodefreshCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "codefresh", accessId: null, diff --git a/frontend/src/pages/integrations/codefresh/create.tsx b/frontend/src/pages/integrations/codefresh/create.tsx index 214487b6e..550fac89d 100644 --- a/frontend/src/pages/integrations/codefresh/create.tsx +++ b/frontend/src/pages/integrations/codefresh/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function CodefreshCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function CodefreshCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx index 9c3912b2e..3d410efd5 100644 --- a/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function DigitalOceanAppPlatformCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "digital-ocean-app-platform", accessId: null, diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx index 925729c94..1a5968007 100644 --- a/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function DigitalOceanAppPlatformCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx index 9424471ef..b8b0884f6 100644 --- a/frontend/src/pages/integrations/flyio/authorize.tsx +++ b/frontend/src/pages/integrations/flyio/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function FlyioCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [accessToken, setAccessToken] = useState(""); const [accessTokenErrorText, setAccessTokenErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function FlyioCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "flyio", accessId: null, diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx index 2eeb7f823..b0549cc58 100644 --- a/frontend/src/pages/integrations/flyio/create.tsx +++ b/frontend/src/pages/integrations/flyio/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function FlyioCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -59,7 +63,7 @@ export default function FlyioCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx index 9344996ab..885155570 100644 --- a/frontend/src/pages/integrations/github/create.tsx +++ b/frontend/src/pages/integrations/github/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function GitHubCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -63,7 +67,7 @@ export default function GitHubCreateIntegrationPage() { if (!targetApp || !targetApp.owner) return; - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp.name, diff --git a/frontend/src/pages/integrations/github/oauth2/callback.tsx b/frontend/src/pages/integrations/github/oauth2/callback.tsx index 7ad1aa2bb..d93e02478 100644 --- a/frontend/src/pages/integrations/github/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/github/oauth2/callback.tsx @@ -2,10 +2,14 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function GitHubOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); + const { code, state } = queryString.parse(router.asPath.split("?")[1]); useEffect(() => { @@ -15,7 +19,7 @@ export default function GitHubOAuth2CallbackPage() { if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await AuthorizeIntegration({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "github" diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx index ca90e3c1a..57524ef1e 100644 --- a/frontend/src/pages/integrations/gitlab/create.tsx +++ b/frontend/src/pages/integrations/gitlab/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -17,7 +21,6 @@ import { useGetIntegrationAuthTeams } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; const gitLabEntities = [ { name: "Individual", value: "individual" }, @@ -26,6 +29,7 @@ const gitLabEntities = [ export default function GitLabCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -87,7 +91,7 @@ export default function GitLabCreateIntegrationPage() { setIsLoading(true); if (!integrationAuth?._id) return; - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: diff --git a/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx b/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx index 27c4c0672..4df89f072 100644 --- a/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx @@ -2,10 +2,14 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function GitLabOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); + const { code, state } = queryString.parse(router.asPath.split("?")[1]); useEffect(() => { (async () => { @@ -14,7 +18,7 @@ export default function GitLabOAuth2CallbackPage() { if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await AuthorizeIntegration({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "gitlab" diff --git a/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx b/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx index 6857fd59e..948e17e0f 100644 --- a/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx +++ b/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx @@ -1,11 +1,15 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function HashiCorpVaultAuthorizeIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); const [vaultURL, setVaultURL] = useState(""); const [vaultURLErrorText, setVaultURLErrorText] = useState(""); @@ -57,7 +61,7 @@ export default function HashiCorpVaultAuthorizeIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "hashicorp-vault", accessId: vaultRoleID, diff --git a/frontend/src/pages/integrations/hashicorp-vault/create.tsx b/frontend/src/pages/integrations/hashicorp-vault/create.tsx index 0287a5c99..0fb973851 100644 --- a/frontend/src/pages/integrations/hashicorp-vault/create.tsx +++ b/frontend/src/pages/integrations/hashicorp-vault/create.tsx @@ -2,6 +2,10 @@ import { useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -13,10 +17,10 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function HashiCorpVaultCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -57,7 +61,7 @@ export default function HashiCorpVaultCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: vaultEnginePath, diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx index b6f272dbc..9bce95a0d 100644 --- a/frontend/src/pages/integrations/heroku/create.tsx +++ b/frontend/src/pages/integrations/heroku/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function HerokuCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -57,7 +61,7 @@ export default function HerokuCreateIntegrationPage() { if (!integrationAuth?._id) return; - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/heroku/oauth2/callback.tsx b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx index b213b2b1d..a3a978a6a 100644 --- a/frontend/src/pages/integrations/heroku/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx @@ -2,10 +2,13 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function HerokuOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); const { code, state } = queryString.parse(router.asPath.split("?")[1]); @@ -15,7 +18,7 @@ export default function HerokuOAuth2CallbackPage() { // validate state if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await AuthorizeIntegration({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "heroku" diff --git a/frontend/src/pages/integrations/laravel-forge/authorize.tsx b/frontend/src/pages/integrations/laravel-forge/authorize.tsx index d69b263e9..98c5569c1 100644 --- a/frontend/src/pages/integrations/laravel-forge/authorize.tsx +++ b/frontend/src/pages/integrations/laravel-forge/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function LaravelForgeCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [serverId, setServerId] = useState(""); @@ -29,7 +34,7 @@ export default function LaravelForgeCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "laravel-forge", accessId: serverId, diff --git a/frontend/src/pages/integrations/laravel-forge/create.tsx b/frontend/src/pages/integrations/laravel-forge/create.tsx index acfeb6932..b1090c442 100644 --- a/frontend/src/pages/integrations/laravel-forge/create.tsx +++ b/frontend/src/pages/integrations/laravel-forge/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function LaravelForgeCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function LaravelForgeCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/netlify/create.tsx b/frontend/src/pages/integrations/netlify/create.tsx index 9d5dbb280..0a729c076 100644 --- a/frontend/src/pages/integrations/netlify/create.tsx +++ b/frontend/src/pages/integrations/netlify/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,7 +20,6 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; const netlifyEnvironments = [ { name: "Local development", slug: "dev" }, @@ -27,6 +30,7 @@ const netlifyEnvironments = [ export default function NetlifyCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -65,7 +69,7 @@ export default function NetlifyCreateIntegrationPage() { if (!integrationAuth?._id) return; - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/netlify/oauth2/callback.tsx b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx index c74e03127..a3d680adc 100644 --- a/frontend/src/pages/integrations/netlify/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx @@ -2,10 +2,13 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function NetlifyOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); const { code, state } = queryString.parse(router.asPath.split("?")[1]); @@ -16,7 +19,7 @@ export default function NetlifyOAuth2CallbackPage() { if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - const integrationAuth = await AuthorizeIntegration({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "netlify" diff --git a/frontend/src/pages/integrations/northflank/authorize.tsx b/frontend/src/pages/integrations/northflank/authorize.tsx index 8e2baa3cb..636fb5ad3 100644 --- a/frontend/src/pages/integrations/northflank/authorize.tsx +++ b/frontend/src/pages/integrations/northflank/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function NorthflankCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function NorthflankCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "northflank", accessToken: apiKey, diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx index 4ebcf95ec..ac6c0f0aa 100644 --- a/frontend/src/pages/integrations/northflank/create.tsx +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -17,10 +21,10 @@ import { useGetIntegrationAuthNorthflankSecretGroups } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function NorthflankCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -78,7 +82,7 @@ export default function NorthflankCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: integrationAuthApps?.find( diff --git a/frontend/src/pages/integrations/railway/authorize.tsx b/frontend/src/pages/integrations/railway/authorize.tsx index c145048d6..498b86ab1 100644 --- a/frontend/src/pages/integrations/railway/authorize.tsx +++ b/frontend/src/pages/integrations/railway/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function RailwayAuthorizeIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function RailwayAuthorizeIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "railway", accessId: null, diff --git a/frontend/src/pages/integrations/railway/create.tsx b/frontend/src/pages/integrations/railway/create.tsx index 44b5ff232..4029f51a1 100644 --- a/frontend/src/pages/integrations/railway/create.tsx +++ b/frontend/src/pages/integrations/railway/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -18,10 +22,10 @@ import { useGetIntegrationAuthRailwayServices } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function RailwayCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const [targetAppId, setTargetAppId] = useState(""); const [targetEnvironmentId, setTargetEnvironmentId] = useState(""); @@ -96,7 +100,7 @@ export default function RailwayCreateIntegrationPage() { (service) => service.serviceId === targetServiceId ); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp.name, diff --git a/frontend/src/pages/integrations/render/authorize.tsx b/frontend/src/pages/integrations/render/authorize.tsx index 376d8fd8f..e20663a05 100644 --- a/frontend/src/pages/integrations/render/authorize.tsx +++ b/frontend/src/pages/integrations/render/authorize.tsx @@ -1,11 +1,14 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { useSaveIntegrationAccessToken} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function RenderCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +23,7 @@ export default function RenderCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "render", accessId: null, diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx index 087682dde..fc347f835 100644 --- a/frontend/src/pages/integrations/render/create.tsx +++ b/frontend/src/pages/integrations/render/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function RenderCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function RenderCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/supabase/authorize.tsx b/frontend/src/pages/integrations/supabase/authorize.tsx index 1c411a487..f580aacab 100644 --- a/frontend/src/pages/integrations/supabase/authorize.tsx +++ b/frontend/src/pages/integrations/supabase/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function SupabaseCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function SupabaseCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "supabase", accessToken: apiKey, diff --git a/frontend/src/pages/integrations/supabase/create.tsx b/frontend/src/pages/integrations/supabase/create.tsx index a759e2ab9..0b9332918 100644 --- a/frontend/src/pages/integrations/supabase/create.tsx +++ b/frontend/src/pages/integrations/supabase/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function SupabaseCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -57,7 +61,7 @@ export default function SupabaseCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/teamcity/authorize.tsx b/frontend/src/pages/integrations/teamcity/authorize.tsx index b417e3c19..da48730b4 100644 --- a/frontend/src/pages/integrations/teamcity/authorize.tsx +++ b/frontend/src/pages/integrations/teamcity/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function TeamCityCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [serverUrl, setServerUrl] = useState(""); @@ -29,7 +34,7 @@ export default function TeamCityCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "teamcity", accessId: null, diff --git a/frontend/src/pages/integrations/teamcity/create.tsx b/frontend/src/pages/integrations/teamcity/create.tsx index a2aa86db4..a6282af92 100644 --- a/frontend/src/pages/integrations/teamcity/create.tsx +++ b/frontend/src/pages/integrations/teamcity/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function TeamCityCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function TeamCityCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/terraform-cloud/authorize.tsx b/frontend/src/pages/integrations/terraform-cloud/authorize.tsx index c569bdcca..aba5152a0 100644 --- a/frontend/src/pages/integrations/terraform-cloud/authorize.tsx +++ b/frontend/src/pages/integrations/terraform-cloud/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function TerraformCloudCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [workspacesId, setWorkSpacesId] = useState(""); @@ -29,7 +34,7 @@ export default function TerraformCloudCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "terraform-cloud", accessId: workspacesId, diff --git a/frontend/src/pages/integrations/terraform-cloud/create.tsx b/frontend/src/pages/integrations/terraform-cloud/create.tsx index 47c33948c..6245265a0 100644 --- a/frontend/src/pages/integrations/terraform-cloud/create.tsx +++ b/frontend/src/pages/integrations/terraform-cloud/create.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { useCreateIntegration } from "@app/hooks/api"; + import { Button, Card, @@ -16,7 +18,6 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; const variableTypes = [ { name: "env" }, @@ -25,6 +26,7 @@ const variableTypes = [ export default function TerraformCloudCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -70,7 +72,7 @@ export default function TerraformCloudCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/travisci/authorize.tsx b/frontend/src/pages/integrations/travisci/authorize.tsx index ec285d8aa..8c688c981 100644 --- a/frontend/src/pages/integrations/travisci/authorize.tsx +++ b/frontend/src/pages/integrations/travisci/authorize.tsx @@ -1,11 +1,16 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function TravisCICreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -20,7 +25,7 @@ export default function TravisCICreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "travisci", accessToken: apiKey, diff --git a/frontend/src/pages/integrations/travisci/create.tsx b/frontend/src/pages/integrations/travisci/create.tsx index 5708bea08..5f83b35d2 100644 --- a/frontend/src/pages/integrations/travisci/create.tsx +++ b/frontend/src/pages/integrations/travisci/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function TravisCICreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -56,7 +60,7 @@ export default function TravisCICreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, diff --git a/frontend/src/pages/integrations/vercel/create.tsx b/frontend/src/pages/integrations/vercel/create.tsx index 18c1835f6..39791d6c9 100644 --- a/frontend/src/pages/integrations/vercel/create.tsx +++ b/frontend/src/pages/integrations/vercel/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -17,7 +21,6 @@ import { useGetIntegrationAuthVercelBranches } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; const vercelEnvironments = [ { name: "Development", slug: "development" }, @@ -27,6 +30,7 @@ const vercelEnvironments = [ export default function VercelCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -81,7 +85,7 @@ export default function VercelCreateIntegrationPage() { const path = targetEnvironment === "preview" && targetBranch !== "" ? targetBranch : null; - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp.name, diff --git a/frontend/src/pages/integrations/vercel/oauth2/callback.tsx b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx index ebde5869a..dd884d8b0 100644 --- a/frontend/src/pages/integrations/vercel/oauth2/callback.tsx +++ b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx @@ -2,10 +2,13 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration"; +import { + useAuthorizeIntegration +} from "@app/hooks/api"; export default function VercelOAuth2CallbackPage() { const router = useRouter(); + const { mutateAsync } = useAuthorizeIntegration(); const { code, state } = queryString.parse(router.asPath.split("?")[1]); @@ -15,8 +18,8 @@ export default function VercelOAuth2CallbackPage() { // validate state if (state !== localStorage.getItem("latestCSRFToken")) return; localStorage.removeItem("latestCSRFToken"); - - const integrationAuth = await AuthorizeIntegration({ + + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id") as string, code: code as string, integration: "vercel" diff --git a/frontend/src/pages/integrations/windmill/authorize.tsx b/frontend/src/pages/integrations/windmill/authorize.tsx index f1dbf4a7c..11aece281 100644 --- a/frontend/src/pages/integrations/windmill/authorize.tsx +++ b/frontend/src/pages/integrations/windmill/authorize.tsx @@ -1,12 +1,17 @@ import { useState } from "react"; import { useRouter } from "next/router"; +import { + useSaveIntegrationAccessToken +} from "@app/hooks/api"; + import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; -import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; export default function WindmillCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -21,7 +26,7 @@ export default function WindmillCreateIntegrationPage() { setIsLoading(true); - const integrationAuth = await saveIntegrationAccessToken({ + const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "windmill", accessToken: apiKey, diff --git a/frontend/src/pages/integrations/windmill/create.tsx b/frontend/src/pages/integrations/windmill/create.tsx index a6cb6f8eb..66559d020 100644 --- a/frontend/src/pages/integrations/windmill/create.tsx +++ b/frontend/src/pages/integrations/windmill/create.tsx @@ -2,6 +2,10 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; +import { + useCreateIntegration +} from "@app/hooks/api"; + import { Button, Card, @@ -16,10 +20,10 @@ import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; -import createIntegration from "../../api/integrations/createIntegration"; export default function WindmillCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -57,7 +61,7 @@ export default function WindmillCreateIntegrationPage() { setIsLoading(true); - await createIntegration({ + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, From 2dba7847b6c88a5da64eced4fd3fb2ea2e1edf63 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 10 Aug 2023 17:19:23 +0700 Subject: [PATCH 37/64] Convert all SecurityClient API calls to hooks except auth --- .../basic/table/ProjectUsersTable.tsx | 49 ++- .../src/components/signup/TeamInviteStep.tsx | 13 +- .../src/components/signup/UserInfoStep.tsx | 5 +- .../components/utilities/attemptCliLogin.ts | 10 +- .../utilities/attemptCliLoginMfa.ts | 10 +- .../src/components/utilities/attemptLogin.ts | 11 +- .../components/utilities/attemptLoginMfa.ts | 10 +- .../utilities/checks/OnboardingCheck.ts | 8 +- .../utilities/secrets/encryptSecrets.ts | 15 +- .../src/ee/components/ActivitySideBar.tsx | 18 +- .../src/ee/components/PITRecoverySidebar.tsx | 287 ------------ .../src/ee/components/SecretVersionList.tsx | 138 ------ frontend/src/helpers/project.ts | 9 +- frontend/src/hooks/api/keys/queries.tsx | 21 +- .../src/hooks/api/organization/queries.tsx | 9 +- frontend/src/hooks/api/users/index.tsx | 1 + frontend/src/hooks/api/users/queries.tsx | 23 +- .../api/organization/GetOrgUserProjects.ts | 23 - .../src/pages/api/organization/GetOrgUsers.ts | 38 -- .../pages/api/organization/addUserToOrg.ts | 27 -- .../src/pages/api/organization/getOrgs.ts | 23 - .../pages/api/workspace/getLatestFileKey.ts | 13 - .../src/pages/api/workspace/uploadKeys.ts | 32 -- frontend/src/pages/dashboard.tsx | 12 +- .../src/pages/project/[id]/members/index.tsx | 28 +- .../service-accounts/[serviceAccountId].tsx | 18 - frontend/src/pages/signup/index.tsx | 4 +- frontend/src/pages/signupinvite.tsx | 5 +- frontend/src/views/Login/Login.tsx | 4 +- .../components/InitialStep/InitialStep.tsx | 4 +- .../Login/components/MFAStep/MFAStep.tsx | 4 +- .../components/PasswordStep/PasswordStep.tsx | 6 +- .../CreateServiceAccountPage.tsx | 46 -- .../CopyServiceAccountIDSection.tsx | 49 --- .../CopyServiceAccountIDSection/index.tsx | 1 - .../CopyServiceAccountPublicKeySection.tsx | 53 --- .../index.tsx | 1 - .../SAProjectLevelPermissionsTable.tsx | 412 ------------------ .../SAProjectLevelPermissionsTable/index.tsx | 1 - .../ServiceAccountNameChangeSection.tsx | 88 ---- .../ServiceAccountNameChangeSection/index.tsx | 1 - .../components/index.tsx | 4 - .../CreateServiceAccountPage/index.tsx | 1 - .../components/E2EESection/E2EESection.tsx | 17 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 4 +- 45 files changed, 179 insertions(+), 1377 deletions(-) delete mode 100644 frontend/src/ee/components/PITRecoverySidebar.tsx delete mode 100644 frontend/src/ee/components/SecretVersionList.tsx delete mode 100644 frontend/src/pages/api/organization/GetOrgUserProjects.ts delete mode 100644 frontend/src/pages/api/organization/GetOrgUsers.ts delete mode 100644 frontend/src/pages/api/organization/addUserToOrg.ts delete mode 100644 frontend/src/pages/api/organization/getOrgs.ts delete mode 100644 frontend/src/pages/api/workspace/getLatestFileKey.ts delete mode 100644 frontend/src/pages/api/workspace/uploadKeys.ts delete mode 100644 frontend/src/pages/settings/org/[id]/service-accounts/[serviceAccountId].tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx delete mode 100644 frontend/src/views/Settings/CreateServiceAccountPage/index.tsx diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 53a0e86f7..5838aaf41 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -8,10 +8,9 @@ import { useSubscription, useWorkspace } from "@app/context"; import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission"; import { useDeleteUserFromWorkspace, - useUpdateUserWorkspaceRole -} from "@app/hooks/api"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; -import uploadKeys from "@app/pages/api/workspace/uploadKeys"; + useGetUserWsKey, + useUpdateUserWorkspaceRole, + useUploadWsKey} from "@app/hooks/api"; import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto"; import guidGenerator from "../../utilities/randomId"; @@ -42,7 +41,10 @@ type EnvironmentProps = { const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => { const { currentWorkspace } = useWorkspace(); const { subscription } = useSubscription(); + const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); + const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace(); + const { mutateAsync: uploadWsKeyMutateAsync } = useUploadWsKey(); const { mutateAsync: updateUserWorkspaceRoleMutateAsync } = useUpdateUserWorkspaceRole(); // const [roleSelected, setRoleSelected] = useState( // Array(userData?.length).fill(userData.map((user) => user.role)) @@ -151,26 +153,31 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa }, [userData, myUser, currentWorkspace]); const grantAccess = async (id: string, publicKey: string) => { - const result = await getLatestFileKey({ workspaceId }); + if (wsKey) { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + // assymmetrically decrypt symmetric key with local private key + const key = decryptAssymmetric({ + ciphertext: wsKey.encryptedKey, + nonce: wsKey.nonce, + publicKey: wsKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: result.latestKey.encryptedKey, - nonce: result.latestKey.nonce, - publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: key, + publicKey, + privateKey: PRIVATE_KEY + }); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: PRIVATE_KEY - }); - - uploadKeys(workspaceId, id, ciphertext, nonce); - router.reload(); + await uploadWsKeyMutateAsync({ + workspaceId, + userId: id, + encryptedKey: ciphertext, + nonce + }); + router.reload(); + } }; const closeUpgradeModal = () => { diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index 398ccd78d..d1cc6ef7d 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -2,9 +2,9 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; +import { useAddUserToOrg } from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { usePopUp } from "@app/hooks/usePopUp"; -import addUserToOrg from "@app/pages/api/organization/addUserToOrg"; import { Button, EmailServiceSetupModal } from "../v2"; @@ -12,10 +12,12 @@ import { Button, EmailServiceSetupModal } from "../v2"; * This is the last step of the signup flow. People can optionally invite their teammates here. */ export default function TeamInviteStep(): JSX.Element { - const [emails, setEmails] = useState(""); const { t } = useTranslation(); const router = useRouter(); + const [emails, setEmails] = useState(""); const { data: serverDetails } = useFetchServerStatus(); + + const { mutateAsync } = useAddUserToOrg(); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const); // Redirect user to the getting started page @@ -27,7 +29,12 @@ export default function TeamInviteStep(): JSX.Element { inviteEmails .split(",") .map((email) => email.trim()) - .map(async (email) => addUserToOrg(email, String(localStorage.getItem("orgData.id")))); + .map(async (email) => { + mutateAsync({ + inviteeEmail: email, + organizationId: String(localStorage.getItem("orgData.id")) + }); + }); await redirectToHome(); }; diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index e0d99548a..b0a0d420f 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -9,8 +9,8 @@ import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import { useGetCommonPasswords } from "@app/hooks/api"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; import ProjectService from "@app/services/ProjectService"; import InputField from "../basic/InputField"; @@ -190,7 +190,8 @@ export default function UserInfoStep({ privateKey }); - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); + const orgId = userOrgs[0]?._id; const project = await ProjectService.initProject({ organizationId: orgId, diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index 099351baa..3b9ebc910 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -1,10 +1,10 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; import login1 from "@app/pages/api/auth/Login1"; import login2 from "@app/pages/api/auth/Login2"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects"; import KeyService from "@app/services/KeyService"; import Telemetry from "./telemetry/Telemetry"; @@ -125,13 +125,11 @@ const attemptLogin = async ( privateKey }); - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); - const orgUserProjects = await getOrganizationUserProjects({ - orgId - }); + const orgUserProjects = await fetchMyOrganizationProjects(orgId); if (orgUserProjects.length > 0) { localStorage.setItem("projectData.id", orgUserProjects[0]._id); diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts index cb33d3bad..2fc6f17b9 100644 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ b/frontend/src/components/utilities/attemptCliLoginMfa.ts @@ -1,10 +1,10 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; import login1 from "@app/pages/api/auth/Login1"; import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects"; import KeyService from "@app/services/KeyService"; import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; @@ -96,13 +96,11 @@ const attemptLoginMfa = async ({ // TODO: in the future - move this logic elsewhere // because this function is about logging the user in // and not initializing the login details - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); - const orgUserProjects = await getOrganizationUserProjects({ - orgId - }); + const orgUserProjects = await fetchMyOrganizationProjects(orgId); localStorage.setItem("projectData.id", orgUserProjects[0]._id); resolve({ diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index b3e4b38be..29c730e3f 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -1,10 +1,10 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; import login1 from "@app/pages/api/auth/Login1"; import login2 from "@app/pages/api/auth/Login2"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects"; import KeyService from "@app/services/KeyService"; import Telemetry from "./telemetry/Telemetry"; @@ -36,7 +36,6 @@ const attemptLogin = async ( providerAuthToken?: string; } ): Promise => { - const telemetry = new Telemetry().getInstance(); return new Promise((resolve, reject) => { client.init( @@ -124,14 +123,12 @@ const attemptLogin = async ( // TODO: in the future - move this logic elsewhere // because this function is about logging the user in // and not initializing the login details - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); - const orgUserProjects = await getOrganizationUserProjects({ - orgId - }); + const orgUserProjects = await fetchMyOrganizationProjects(orgId); if (orgUserProjects.length > 0) { localStorage.setItem("projectData.id", orgUserProjects[0]._id); diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts index 967881357..feb58b596 100644 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ b/frontend/src/components/utilities/attemptLoginMfa.ts @@ -1,10 +1,10 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; import login1 from "@app/pages/api/auth/Login1"; import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects"; import KeyService from "@app/services/KeyService"; import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; @@ -87,13 +87,11 @@ const attemptLoginMfa = async ({ // TODO: in the future - move this logic elsewhere // because this function is about logging the user in // and not initializing the login details - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); - const orgUserProjects = await getOrganizationUserProjects({ - orgId - }); + const orgUserProjects = await fetchMyOrganizationProjects(orgId); localStorage.setItem("projectData.id", orgUserProjects[0]._id); resolve(true); diff --git a/frontend/src/components/utilities/checks/OnboardingCheck.ts b/frontend/src/components/utilities/checks/OnboardingCheck.ts index f9b47a210..01d7a2d55 100644 --- a/frontend/src/components/utilities/checks/OnboardingCheck.ts +++ b/frontend/src/components/utilities/checks/OnboardingCheck.ts @@ -1,5 +1,4 @@ -import { fetchUserAction } from "@app/hooks/api/users/queries"; -import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers"; +import { fetchOrgUsers,fetchUserAction } from "@app/hooks/api/users/queries"; interface OnboardingCheckProps { setTotalOnboardingActionsDone?: (value: number) => void; @@ -43,9 +42,8 @@ const onboardingCheck = async ({ if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro); const orgId = localStorage.getItem("orgData.id"); - const orgUsers = await getOrganizationUsers({ - orgId: orgId || "" - }); + const orgUsers = await fetchOrgUsers(orgId || ""); + if (orgUsers.length > 1) { countActions += 1; } diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts index 49fcf9736..610e2a6f1 100644 --- a/frontend/src/components/utilities/secrets/encryptSecrets.ts +++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts @@ -2,7 +2,7 @@ import crypto from "crypto"; import { SecretDataProps, Tag } from "public/data/frequentInterfaces"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; +import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; import { decryptAssymmetric, encryptSymmetric } from "../cryptography/crypto"; @@ -42,19 +42,21 @@ const encryptSecrets = async ({ }) => { let secrets; try { - const sharedKey = await getLatestFileKey({ workspaceId }); + // const sharedKey = await getLatestFileKey({ workspaceId }); + const wsKey = await fetchUserWsKey(workspaceId); const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; let randomBytes: string; - if (Object.keys(sharedKey).length > 0) { + if (wsKey) { // case: a (shared) key exists for the workspace randomBytes = decryptAssymmetric({ - ciphertext: sharedKey.latestKey.encryptedKey, - nonce: sharedKey.latestKey.nonce, - publicKey: sharedKey.latestKey.sender.publicKey, + ciphertext: wsKey.encryptedKey, + nonce: wsKey.nonce, + publicKey: wsKey.sender.publicKey, privateKey: PRIVATE_KEY }); + } else { // case: a (shared) key does not exist for the workspace randomBytes = crypto.randomBytes(16).toString("hex"); @@ -114,6 +116,7 @@ const encryptSecrets = async ({ return result; }); + } catch (error) { console.log("Error while encrypting secrets"); } diff --git a/frontend/src/ee/components/ActivitySideBar.tsx b/frontend/src/ee/components/ActivitySideBar.tsx index 67d673c3d..d1e068177 100644 --- a/frontend/src/ee/components/ActivitySideBar.tsx +++ b/frontend/src/ee/components/ActivitySideBar.tsx @@ -8,7 +8,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import getActionData from "@app/ee/api/secrets/GetActionData"; import patienceDiff from "@app/ee/utilities/findTextDifferences"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; +import { + useGetUserWsKey +} from "@app/hooks/api"; import { decryptAssymmetric, @@ -59,25 +61,24 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => { const [actionData, setActionData] = useState(); const [actionMetaData, setActionMetaData] = useState(); const [isLoading, setIsLoading] = useState(false); + const { data: wsKey } = useGetUserWsKey(String(router.query.id)); useEffect(() => { const getLogData = async () => { setIsLoading(true); const tempActionData = await getActionData({ actionId: currentAction }); - const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }); const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); // #TODO: make this a separate function and reuse across the app let decryptedLatestKey: string; - if (latestKey) { + if (wsKey) { // assymmetrically decrypt symmetric key with local private key decryptedLatestKey = decryptAssymmetric({ - ciphertext: latestKey.latestKey.encryptedKey, - nonce: latestKey.latestKey.nonce, - publicKey: latestKey.latestKey.sender.publicKey, + ciphertext: wsKey.encryptedKey, + nonce: wsKey.nonce, + publicKey: wsKey.sender.publicKey, privateKey: String(PRIVATE_KEY) }); - } const decryptedSecretVersions = tempActionData.payload.secretVersions.map( (encryptedSecretVersion: { @@ -122,9 +123,10 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => { setActionData(decryptedSecretVersions); setActionMetaData({ name: tempActionData.name }); setIsLoading(false); + } }; getLogData(); - }, [currentAction]); + }, [currentAction, wsKey]); return (
void; - setSnapshotData: (value: any) => void; - chosenSnapshot: string; -} - -interface SnaphotProps { - _id: string; - createdAt: string; - secretVersions: string[]; -} - -interface EncrypetedSecretVersionListProps { - _id: string; - createdAt: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - environment: string; - type: "personal" | "shared"; - tags: Tag[]; -} - -/** - * @param {object} obj - * @param {function} obj.toggleSidebar - function that opens or closes the sidebar - * @param {function} obj.setSnapshotData - state manager for snapshot data - * @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected - * @returns the sidebar with the options for point-in-time recovery (commits) - */ -const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }: SideBarProps) => { - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState([]); - const [currentOffset, setCurrentOffset] = useState(0); - const currentLimit = 15; - - const loadMoreSnapshots = () => { - setCurrentOffset(currentOffset + currentLimit); - }; - - useEffect(() => { - const getLogData = async () => { - setIsLoading(true); - const results = await getProjectSecretShanpshots({ - workspaceId: String(router.query.id), - limit: currentLimit, - offset: currentOffset - }); - setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results)); - setIsLoading(false); - }; - getLogData(); - }, [currentOffset]); - - const exploreSnapshot = async ({ snapshotId }: { snapshotId: string }) => { - const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId }); - - const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }); - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - let decryptedLatestKey: string; - if (latestKey) { - // assymmetrically decrypt symmetric key with local private key - decryptedLatestKey = decryptAssymmetric({ - ciphertext: latestKey.latestKey.encryptedKey, - nonce: latestKey.latestKey.nonce, - publicKey: latestKey.latestKey.sender.publicKey, - privateKey: String(PRIVATE_KEY) - }); - } - - const decryptedSecretVersions = secretSnapshotData.secretVersions - .filter( - (sv: EncrypetedSecretVersionListProps) => - sv.type !== undefined && sv.environment !== undefined - ) - .map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({ - id: encryptedSecretVersion._id, - pos, - type: encryptedSecretVersion.type, - environment: encryptedSecretVersion.environment, - tags: encryptedSecretVersion.tags, - key: decryptSymmetric({ - ciphertext: encryptedSecretVersion.secretKeyCiphertext, - iv: encryptedSecretVersion.secretKeyIV, - tag: encryptedSecretVersion.secretKeyTag, - key: decryptedLatestKey - }), - value: decryptSymmetric({ - ciphertext: encryptedSecretVersion.secretValueCiphertext, - iv: encryptedSecretVersion.secretValueIV, - tag: encryptedSecretVersion.secretValueTag, - key: decryptedLatestKey - }) - })); - - const secretKeys = [ - ...new Set( - decryptedSecretVersions - .filter((dsv: any) => dsv.type !== undefined || dsv.environemnt !== undefined) - .map((secret: SecretDataProps) => secret.key) - ) - ]; - - const result = secretKeys.map((key, index) => - decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "shared" - )[0]?.id - ? { - id: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "shared" - )[0].id, - pos: index, - key, - environment: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "shared" - )[0].environment, - tags: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "shared" - )[0].tags, - value: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "shared" - )[0]?.value, - valueOverride: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "personal" - )[0]?.value - } - : { - id: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "personal" - )[0].id, - pos: index, - key, - environment: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "personal" - )[0].environment, - tags: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "personal" - )[0].tags, - value: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "shared" - )[0]?.value, - valueOverride: decryptedSecretVersions.filter( - (secret: SecretDataProps) => secret.key === key && secret.type === "personal" - )[0]?.value - } - ); - - setSnapshotData({ - id: secretSnapshotData._id, - version: secretSnapshotData.version, - createdAt: secretSnapshotData.createdAt, - secretVersions: result, - comment: "" - }); - }; - - return ( -
- {isLoading ? ( -
- -
- ) : ( -
-
-

Point In Recovery

-
null} - role="button" - tabIndex={0} - className="p-1" - onClick={() => toggleSidebar(false)} - > - -
-
-
- - Note: This will recover secrets for all enviroments in this project. - - {secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => ( -
null} - role="button" - tabIndex={0} - key={snapshot._id} - onClick={() => exploreSnapshot({ snapshotId: snapshot._id })} - className={`${ - chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === "") - ? "pointer-events-none bg-primary text-black" - : "cursor-pointer bg-mineshaft-700 duration-200 hover:bg-mineshaft-500" - } mb-2 flex flex-row items-center justify-between rounded-md py-3 px-4`} - > -
-
- {timeSince(new Date(snapshot.createdAt))} -
-
{` - ${snapshot.secretVersions.length} Secrets`}
-
-
- {id === 0 - ? "Current Version" - : chosenSnapshot === snapshot._id - ? "Currently Viewing" - : "Explore"} -
-
- ))} -
-
-
-
-
-
- )} -
- ); -}; - -export default PITRecoverySidebar; diff --git a/frontend/src/ee/components/SecretVersionList.tsx b/frontend/src/ee/components/SecretVersionList.tsx deleted file mode 100644 index 41a085082..000000000 --- a/frontend/src/ee/components/SecretVersionList.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faCircle, faDotCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { - decryptAssymmetric, - decryptSymmetric -} from "@app/components/utilities/cryptography/crypto"; -import getSecretVersions from "@app/ee/api/secrets/GetSecretVersions"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; - -interface DecryptedSecretVersionListProps { - createdAt: string; - value: string; -} - -interface EncrypetedSecretVersionListProps { - createdAt: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; -} - -/** - * @param {string} secretId - the id of a secret for which are querying version history - * @returns a list of versions for a specific secret - */ -const SecretVersionList = ({ secretId }: { secretId: string }) => { - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - const [secretVersions, setSecretVersions] = useState([]); - - useEffect(() => { - const getSecretVersionHistory = async () => { - setIsLoading(true); - try { - const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10 }); - const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }); - - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - let decryptedLatestKey: string; - if (latestKey) { - // assymmetrically decrypt symmetric key with local private key - decryptedLatestKey = decryptAssymmetric({ - ciphertext: latestKey.latestKey.encryptedKey, - nonce: latestKey.latestKey.nonce, - publicKey: latestKey.latestKey.sender.publicKey, - privateKey: String(PRIVATE_KEY) - }); - } - - const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map( - (encryptedSecretVersion: EncrypetedSecretVersionListProps) => ({ - createdAt: encryptedSecretVersion.createdAt, - value: decryptSymmetric({ - ciphertext: encryptedSecretVersion.secretValueCiphertext, - iv: encryptedSecretVersion.secretValueIV, - tag: encryptedSecretVersion.secretValueTag, - key: decryptedLatestKey - }) - }) - ); - - setSecretVersions(decryptedSecretVersions); - setIsLoading(false); - } catch (error) { - console.log(error); - } - }; - getSecretVersionHistory(); - }, [secretId]); - - return ( -
-

{t("dashboard.sidebar.version-history")}

-
- {isLoading ? ( -
- -
- ) : ( -
- {secretVersions ? ( - secretVersions - ?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - .map((version: DecryptedSecretVersionListProps, index: number) => ( -
-
-
- -
-
-
-
-
- {new Date(version.createdAt).toLocaleDateString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - })} -
-
-

- - Value: - - {version.value} -

-
-
-
- )) - ) : ( -
- No version history yet. -
- )} -
- )} -
-
- ); -}; - -export default SecretVersionList; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index c24d3b9cf..f0cb8ec4c 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -2,10 +2,10 @@ import crypto from "crypto"; import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets"; +import { uploadWsKey } from "@app/hooks/api/keys/queries"; import { createSecret } from "@app/hooks/api/secrets/queries"; import { fetchUserDetails } from "@app/hooks/api/users/queries"; import { createWorkspace } from "@app/hooks/api/workspace/queries"; -import uploadKeys from "@app/pages/api/workspace/uploadKeys"; const secretsToBeAdded = [ { @@ -111,7 +111,12 @@ const initProjectHelper = async ({ privateKey: PRIVATE_KEY }); - await uploadKeys(workspace._id, user._id, ciphertext, nonce); + await uploadWsKey({ + workspaceId: workspace._id, + userId: user._id, + encryptedKey: ciphertext, + nonce + }); // encrypt and upload secrets to new project const secrets = await encryptSecrets({ diff --git a/frontend/src/hooks/api/keys/queries.tsx b/frontend/src/hooks/api/keys/queries.tsx index c44047351..143266d40 100644 --- a/frontend/src/hooks/api/keys/queries.tsx +++ b/frontend/src/hooks/api/keys/queries.tsx @@ -8,7 +8,7 @@ const encKeyKeys = { getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const }; -const fetchUserWsKey = async (workspaceID: string) => { +export const fetchUserWsKey = async (workspaceID: string) => { const { data } = await apiRequest.get<{ latestKey: UserWsKeyPair }>( `/api/v1/key/${workspaceID}/latest` ); @@ -24,8 +24,23 @@ export const useGetUserWsKey = (workspaceID: string) => }); // mutations +export const uploadWsKey = async ({ + workspaceId, + userId, + encryptedKey, + nonce +}: UploadWsKeyDTO) => { + return apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } }) +} + export const useUploadWsKey = () => useMutation<{}, {}, UploadWsKeyDTO>({ - mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) => - apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } }) + mutationFn: async ({ encryptedKey, nonce, userId, workspaceId }) => { + return uploadWsKey({ + workspaceId, + userId, + encryptedKey, + nonce + }); + } }); diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 13810febd..17f15307b 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -27,12 +27,16 @@ const organizationKeys = { getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const }; +export const fetchOrganizations = async () => { + const { data: { organizations } } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); + return organizations; +} + export const useGetOrganizations = () => { return useQuery({ queryKey: organizationKeys.getUserOrganizations, queryFn: async () => { - const { data: { organizations } } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); - return organizations; + return fetchOrganizations(); } }); } @@ -42,7 +46,6 @@ export const useRenameOrg = () => { return useMutation<{}, {}, RenameOrgDTO>({ mutationFn: ({ newOrgName, orgId }) => { - console.log("useRenameOrg"); return apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }); }, onSuccess: () => { diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index cb421c633..2eb96d45d 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -7,6 +7,7 @@ export { useDeleteOrgMembership, useGetMyAPIKeys, useGetMyIp, + useGetMyOrganizationProjects, useGetMySessions, useGetOrgUsers, useGetUser, diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 48dfdf564..a2d9318e5 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -29,6 +29,7 @@ const userKeys = { myIp: ["ip"] as const, myAPIKeys: ["api-keys"] as const, mySessions: ["sessions"] as const, + myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const }; export const fetchUserDetails = async () => { @@ -147,7 +148,9 @@ export const useAddUserToOrg = () => { } return useMutation({ - mutationFn: (dto) => apiRequest.post("/api/v1/invite-org/signup", dto), + mutationFn: (dto) => { + return apiRequest.post("/api/v1/invite-org/signup", dto); + }, onSuccess: (_, { organizationId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); } @@ -329,4 +332,22 @@ export const useUpdateMfaEnabled = () => { queryClient.invalidateQueries(userKeys.getUser); } }); +} + +export const fetchMyOrganizationProjects = async (orgId: string) => { + const { data: { workspaces } } = await apiRequest.get( + `/api/v1/organization/${orgId}/my-workspaces` + ); + + return workspaces; +} + +export const useGetMyOrganizationProjects = (orgId: string) => { + return useQuery({ + queryKey: userKeys.myOrganizationProjects(orgId), + queryFn: async () => { + return fetchMyOrganizationProjects(orgId); + }, + enabled: true + }); } \ No newline at end of file diff --git a/frontend/src/pages/api/organization/GetOrgUserProjects.ts b/frontend/src/pages/api/organization/GetOrgUserProjects.ts deleted file mode 100644 index d8c87d6c7..000000000 --- a/frontend/src/pages/api/organization/GetOrgUserProjects.ts +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get all the projects of a certain user in an org. - * @param {*} req - * @param {*} res - * @returns - */ -const getOrganizationUserProjects = (req: { orgId: string }) => - SecurityClient.fetchCall(`/api/v1/organization/${req.orgId}/my-workspaces`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res && res.status === 200) { - return (await res.json()).workspaces; - } - console.log("Failed to get projects of a user in an org"); - return undefined; - }); - -export default getOrganizationUserProjects; diff --git a/frontend/src/pages/api/organization/GetOrgUsers.ts b/frontend/src/pages/api/organization/GetOrgUsers.ts deleted file mode 100644 index 9af757e68..000000000 --- a/frontend/src/pages/api/organization/GetOrgUsers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -export interface IMembershipOrg { - _id: string; - user: { - email: string; - firstName: string; - lastName: string; - _id: string; - publicKey: string; - }; - inviteEmail: string; - organization: string; - role: "owner" | "admin" | "member"; - status: "invited" | "accepted"; - deniedPermissions: any[]; -} -/** - * This route lets us get all the users in an org. - * @param {object} obj - * @param {string} obj.orgId - organization Id - * @returns - */ -const getOrganizationUsers = ({ orgId }: { orgId: string }): Promise => - SecurityClient.fetchCall(`/api/v1/organization/${orgId}/users`, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - return (await res.json()).users; - } - console.log("Failed to get org users"); - return undefined; - }); - -export default getOrganizationUsers; diff --git a/frontend/src/pages/api/organization/addUserToOrg.ts b/frontend/src/pages/api/organization/addUserToOrg.ts deleted file mode 100644 index 7dbb65da5..000000000 --- a/frontend/src/pages/api/organization/addUserToOrg.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This function sends an email invite to a user to join an org - * @param {*} email - * @param {*} orgId - * @returns - */ -const addUserToOrg = (email: string, orgId: string) => - SecurityClient.fetchCall("/api/v1/invite-org/signup", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - inviteeEmail: email, - organizationId: orgId - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to add a user to an org"); - return undefined; - }); - -export default addUserToOrg; diff --git a/frontend/src/pages/api/organization/getOrgs.ts b/frontend/src/pages/api/organization/getOrgs.ts deleted file mode 100644 index 09cd0f022..000000000 --- a/frontend/src/pages/api/organization/getOrgs.ts +++ /dev/null @@ -1,23 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route lets us get the all the orgs of a certain user. - * @returns - */ -const getOrganizations = () => { - return SecurityClient.fetchCall("/api/v1/organization", { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }).then(async (res) => { - if (res?.status === 200) { - const {organizations} = await res.json(); - return organizations; - } - console.log("Failed to get orgs of a user"); - return undefined; - }); -} - -export default getOrganizations; diff --git a/frontend/src/pages/api/workspace/getLatestFileKey.ts b/frontend/src/pages/api/workspace/getLatestFileKey.ts deleted file mode 100644 index 4dcd330d2..000000000 --- a/frontend/src/pages/api/workspace/getLatestFileKey.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { apiRequest } from "@app/config/request"; - -/** - * Get the latest key pairs from a certain workspace - * @param {string} workspaceId - * @returns - */ -const getLatestFileKey = async ({ workspaceId }: { workspaceId: string }) => { - const { data } = await apiRequest.get(`/api/v1/key/${workspaceId}/latest`); - return data; -} - -export default getLatestFileKey; diff --git a/frontend/src/pages/api/workspace/uploadKeys.ts b/frontend/src/pages/api/workspace/uploadKeys.ts deleted file mode 100644 index c28fa1fb3..000000000 --- a/frontend/src/pages/api/workspace/uploadKeys.ts +++ /dev/null @@ -1,32 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route uplods the keys in an encrypted format. - * @param {*} workspaceId - * @param {*} userId - * @param {*} encryptedKey - * @param {*} nonce - * @returns - */ -const uploadKeys = (workspaceId: string, userId: string, encryptedKey: string, nonce: string) => - SecurityClient.fetchCall(`/api/v1/key/${workspaceId}`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - key: { - userId, - encryptedKey, - nonce - } - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res; - } - console.log("Failed to upload keys for a new user"); - return undefined; - }); - -export default uploadKeys; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index fd8a78a31..3f24ab101 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -1,10 +1,11 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; -import getOrganizations from "./api/organization/getOrgs"; +import { useGetOrganizations } from "@app/hooks/api"; export default function DashboardRedirect() { const router = useRouter(); + const { data: userOrgs } = useGetOrganizations(); /** * Here we forward to the default workspace if a user opens this url @@ -16,11 +17,10 @@ export default function DashboardRedirect() { try { if (localStorage.getItem("orgData.id")) { router.push(`/org/${localStorage.getItem("orgData.id")}/overview`); - } else { - const userOrgs = await getOrganizations(); - userOrg = userOrgs[0]._id; - router.push(`/org/${userOrg}/overview`); - } + } else if (userOrgs) { + userOrg = userOrgs[0]._id; + router.push(`/org/${userOrg}/overview`); + } } catch (error) { console.log("Error - Not logged in yet"); } diff --git a/frontend/src/pages/project/[id]/members/index.tsx b/frontend/src/pages/project/[id]/members/index.tsx index 5287c0430..997606b1d 100644 --- a/frontend/src/pages/project/[id]/members/index.tsx +++ b/frontend/src/pages/project/[id]/members/index.tsx @@ -11,14 +11,18 @@ import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMembe import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable"; import guidGenerator from "@app/components/utilities/randomId"; import { Input } from "@app/components/v2"; -import { useAddUserToWorkspace,useGetUser , useGetWorkspaceUsers } from "@app/hooks/api"; +import { useOrganization } from "@app/context"; +import { + useAddUserToWorkspace, + useGetOrgUsers, + useGetUser, + useGetWorkspaceUsers} from "@app/hooks/api"; +import { uploadWsKey } from "@app/hooks/api/keys/queries"; import { decryptAssymmetric, encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; -import getOrganizationUsers from "../../../api/organization/GetOrgUsers"; -import uploadKeys from "../../../api/workspace/uploadKeys"; interface UserProps { firstName: string; @@ -44,6 +48,9 @@ export default function Users() { const workspaceId = router.query.id as string; const { data: user } = useGetUser(); + const { currentOrg } = useOrganization(); + const { data: orgUsers } = useGetOrgUsers(currentOrg?._id ?? ""); + const { data: workspaceUsers } = useGetWorkspaceUsers(workspaceId); const { mutateAsync: addUserToWorkspaceMutateAsync } = useAddUserToWorkspace(); @@ -62,7 +69,7 @@ export default function Users() { const [orgUserList, setOrgUserList] = useState([]); useEffect(() => { - if (user && workspaceUsers) { + if (user && workspaceUsers && orgUsers) { (async () => { setPersonalEmail(user.email); @@ -82,10 +89,6 @@ export default function Users() { setIsUserListLoading(false); - // This is needed to know wha users from an org (if any), we are able to add to a certain project - const orgUsers = await getOrganizationUsers({ - orgId: String(localStorage.getItem("orgData.id")) - }); setOrgUserList(orgUsers); setEmail( orgUsers @@ -98,7 +101,7 @@ export default function Users() { ); })(); } - }, [user, workspaceUsers]); + }, [user, workspaceUsers, orgUsers]); const closeAddModal = () => { setIsAddOpen(false); @@ -143,7 +146,12 @@ export default function Users() { privateKey: PRIVATE_KEY }); - uploadKeys(workspaceId, result.invitee._id, ciphertext, nonce); + await uploadWsKey({ + workspaceId, + userId: result.invitee._id, + encryptedKey: ciphertext, + nonce + }); } setEmail(""); setIsAddOpen(false); diff --git a/frontend/src/pages/settings/org/[id]/service-accounts/[serviceAccountId].tsx b/frontend/src/pages/settings/org/[id]/service-accounts/[serviceAccountId].tsx deleted file mode 100644 index 19ef4f768..000000000 --- a/frontend/src/pages/settings/org/[id]/service-accounts/[serviceAccountId].tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import Head from "next/head"; - -import { CreateServiceAccountPage } from "@app/views/Settings/CreateServiceAccountPage"; - -export default function ServiceAccountPage() { - return ( - <> - - Edit Service Account - - - - - ); -} - -ServiceAccountPage.requireAuth = true; diff --git a/frontend/src/pages/signup/index.tsx b/frontend/src/pages/signup/index.tsx index ff8c41e61..3a6963f60 100644 --- a/frontend/src/pages/signup/index.tsx +++ b/frontend/src/pages/signup/index.tsx @@ -12,9 +12,9 @@ import InitialSignupStep from "@app/components/signup/InitialSignupStep"; import TeamInviteStep from "@app/components/signup/TeamInviteStep"; import UserInfoStep from "@app/components/signup/UserInfoStep"; import SecurityClient from "@app/components/utilities/SecurityClient"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import checkEmailVerificationCode from "@app/pages/api/auth/CheckEmailVerificationCode"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; /** * @returns the signup page @@ -37,7 +37,7 @@ export default function SignUp() { useEffect(() => { const tryAuth = async () => { try { - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); router.push(`/org/${userOrgs[0]._id}/overview`); } catch (error) { console.log("Error - Not logged in yet"); diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 3c2b38cb1..18fff01c5 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -26,8 +26,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient"; import { useGetCommonPasswords } from "@app/hooks/api"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; -import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite"; import verifySignupInvite from "./api/auth/VerifySignupInvite"; @@ -169,7 +168,7 @@ export default function SignupInvite() { privateKey }); - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx index d7ddf6078..fb10c7844 100644 --- a/frontend/src/views/Login/Login.tsx +++ b/frontend/src/views/Login/Login.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import axios from "axios" +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchUserDetails } from "@app/hooks/api/users/queries"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; import { getAuthToken, isLoggedIn } from "@app/reactQuery"; import { @@ -24,7 +24,7 @@ export const Login = () => { // TODO(akhilmhdh): workspace will be controlled by a workspace context const redirectToDashboard = async () => { try { - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); // userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id; const userOrg = userOrgs[0] && userOrgs[0]._id; diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 2bee45669..3f7abcba4 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -12,8 +12,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; import attemptLogin from "@app/components/utilities/attemptLogin"; import { Button, Input } from "@app/components/v2"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; type Props = { setStep: (step: number) => void; @@ -90,7 +90,7 @@ export const InitialStep = ({ setIsLoading(false); return; } - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const userOrg = userOrgs[0] && userOrgs[0]._id; // case: login does not require MFA step diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index fe4a0fd01..c607502c9 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -10,7 +10,7 @@ import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa" import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; import { Button } from "@app/components/v2"; import { useSendMfaToken } from "@app/hooks/api/auth"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; // The style for the verification code input const props = { @@ -110,7 +110,7 @@ export const MFAStep = ({ if (isLoginSuccessful) { setIsLoading(false); - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const userOrg = userOrgs[0] && userOrgs[0]._id; // case: login does not require MFA step diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 6076e76bb..dae175eb1 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -8,7 +8,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; import attemptLogin from "@app/components/utilities/attemptLogin"; import { Button, Input } from "@app/components/v2"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; type Props = { providerAuthToken: string; @@ -83,14 +83,14 @@ export const PasswordStep = ({ } // case: login does not require MFA step - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const userOrg = userOrgs[0]._id; setIsLoading(false); createNotification({ text: "Successfully logged in", type: "success" }); - router.push(`/org/${userOrg?._id}/overview`); + router.push(`/org/${userOrg}/overview`); } } } catch (err) { diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx deleted file mode 100644 index b48057685..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useRouter } from "next/router"; - -import NavHeader from "@app/components/navigation/NavHeader"; - -import { SAProjectLevelPermissionsTable } from "./components/SAProjectLevelPermissionsTable"; -import { - CopyServiceAccountPublicKeySection, - ServiceAccountNameChangeSection -} from "./components"; - -export const CreateServiceAccountPage = () => { - const router = useRouter(); - const {serviceAccountId} = router.query; - - return ( -
- -
-

Service Account

-

- A service account represents a machine identity such as a VM or application client. -

-
- {typeof serviceAccountId === "string" && ( -
- -
- -
-
- -
-
- )} -
- ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx deleted file mode 100644 index f1f4e0c60..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect } from "react"; -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { IconButton } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; - -type Props = { - serviceAccountId: string; -} - -export const CopyServiceAccountIDSection = ({ serviceAccountId }: Props): JSX.Element => { - const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false); - - useEffect(() => { - let timer: NodeJS.Timeout; - - if (isServiceAccountIdCopied) { - timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [isServiceAccountIdCopied]); - - const copyServiceAccountIdToClipboard = () => { - navigator.clipboard.writeText(serviceAccountId); - setIsServiceAccountIdCopied.on(); - }; - - return ( -
-

Service Account ID

-
-

{serviceAccountId}

- copyServiceAccountIdToClipboard()} - > - - - Copy - - -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx deleted file mode 100644 index 9efdc2dcd..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CopyServiceAccountIDSection } from "./CopyServiceAccountIDSection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx deleted file mode 100644 index a60dc4589..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect } from "react"; -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { IconButton } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; -import { useGetServiceAccountById } from "@app/hooks/api"; - -type Props = { - serviceAccountId: string; -} - -export const CopyServiceAccountPublicKeySection = ({ serviceAccountId }: Props): JSX.Element => { - const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId); - const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false); - - useEffect(() => { - let timer: NodeJS.Timeout; - - if (isServiceAccountIdCopied) { - timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [isServiceAccountIdCopied]); - - const copyServiceAccountIdToClipboard = () => { - if (!serviceAccount) return; - - navigator.clipboard.writeText(serviceAccount.publicKey); - setIsServiceAccountIdCopied.on(); - }; - - return serviceAccount ? ( -
-

Public Key

-
-

{serviceAccount.publicKey}

- copyServiceAccountIdToClipboard()} - > - - - Copy - - -
-
- ) :
-} \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx deleted file mode 100644 index 2fb93656d..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CopyServiceAccountPublicKeySection } from "./CopyServiceAccountPublicKeySection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx deleted file mode 100644 index dc56cce65..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx +++ /dev/null @@ -1,412 +0,0 @@ -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faKey, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { - decryptAssymmetric, - encryptAssymmetric, - verifyPrivateKey -} from "@app/components/utilities/cryptography/crypto"; -import { - Button, - Checkbox, - DeleteActionModal, - EmptyState, - FormControl, - IconButton, - Input, - Modal, - ModalClose, - ModalContent, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { usePopUp } from "@app/hooks"; -import { - useCreateServiceAccountProjectLevelPermission, - useDeleteServiceAccountProjectLevelPermission, - useGetServiceAccountById, - useGetServiceAccountProjectLevelPermissions, - useGetUserWorkspaces -} from "@app/hooks/api"; -import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; - -const createProjectLevelPermissionSchema = yup.object({ - privateKey: yup.string().required().label("Private Key"), - workspace: yup.string().required().label("Workspace"), - environment: yup.string().required().label("Environment"), - permissions: yup - .object() - .shape({ - read: yup.boolean().required(), - write: yup.boolean().required() - }) - .defined() - .required() -}); - -type CreateProjectLevelPermissionForm = yup.InferType; - -type Props = { - serviceAccountId: string; -}; - -export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => { - const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId); - const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces(); - const [searchPermissions, setSearchPermissions] = useState(""); - - const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = - useGetServiceAccountProjectLevelPermissions(serviceAccountId); - - const createServiceAccountProjectLevelPermission = - useCreateServiceAccountProjectLevelPermission(); - const deleteServiceAccountProjectLevelPermission = - useDeleteServiceAccountProjectLevelPermission(); - - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addProjectLevelPermission", - "removeProjectLevelPermission" - ] as const); - - const [, setSelectedWorkspace] = useState(undefined); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(createProjectLevelPermissionSchema) - }); - - const onAddProjectLevelPermission = async ({ - privateKey, - workspace, - environment, - permissions: { read, write } - }: CreateProjectLevelPermissionForm) => { - // TODO: clean up / modularize this function - - if (!serviceAccount) return; - - const { latestKey } = await getLatestFileKey({ - workspaceId: workspace - }); - - verifyPrivateKey({ - privateKey, - publicKey: serviceAccount.publicKey - }); - - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - const key = decryptAssymmetric({ - ciphertext: latestKey.encryptedKey, - nonce: latestKey.nonce, - publicKey: latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey: serviceAccount.publicKey, - privateKey - }); - - await createServiceAccountProjectLevelPermission.mutateAsync({ - serviceAccountId, - workspaceId: workspace, - environment, - read, - write, - encryptedKey: ciphertext, - nonce - }); - handlePopUpClose("addProjectLevelPermission"); - }; - - const onRemoveProjectLevelPermission = async () => { - const serviceAccountWorkspacePermissionId = ( - popUp?.removeProjectLevelPermission?.data as { _id: string } - )?._id; - await deleteServiceAccountProjectLevelPermission.mutateAsync({ - serviceAccountId, - serviceAccountWorkspacePermissionId - }); - handlePopUpClose("removeProjectLevelPermission"); - }; - - return ( -
-

Project-Level Permissions

-
-
- setSearchPermissions(e.target.value)} - leftIcon={} - placeholder="Search service account project-level permissions..." - /> -
- -
- -
{key} diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx index c685a6dec..9d796d079 100644 --- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx +++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx @@ -61,10 +61,11 @@ type Props = { importedSecrets?: TImportedSecrets; onSecretImportDelete: (env: string, secPath: string) => void; items: { id: string; environment: string; secretPath: string }[]; + searchTerm: string; }; export const SecretImportSection = memo( - ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => { + ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => { const { currentWorkspace } = useWorkspace(); const environments = currentWorkspace?.environments || []; @@ -83,6 +84,7 @@ export const SecretImportSection = memo( )} onDelete={onSecretImportDelete} importedSecPath={impSecPath} + searchTerm={searchTerm} /> ))} diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 780a36225..a7ccec3b4 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -219,7 +219,7 @@ export const SecretInputRow = memo( } return ( -
{index + 1}
- - - - - - - - - - {isPermissionsLoading && ( - - )} - {!isPermissionsLoading && - serviceAccountWorkspacePermissions && - serviceAccountWorkspacePermissions.map( - ({ _id, workspace, environment, read, write }) => { - const environmentName = workspace.environments.find( - (env) => env.slug === environment - )?.name; - return ( - - - - - - - - ); - } - )} - {!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && ( - - - - )} - -
ProjectEnvironmentReadWrite -
{workspace.name}{environmentName} - - {/**/} - - - - {/**/} - - - handlePopUpOpen("removeProjectLevelPermission", { _id })} - > - - -
- -
-
- { - handlePopUpToggle("addProjectLevelPermission", isOpen); - }} - > - -
- {!isUserWorkspacesLoading && userWorkspaces && ( - <> - ( - - - - )} - /> - ( - - - - )} - /> - { - const environments = - userWorkspaces?.find( - /* eslint-disable-next-line no-underscore-dangle */ - (userWorkspace) => userWorkspace._id === control?._formValues?.workspace - )?.environments ?? []; - return ( - - - - ); - }} - /> - - )} - { - const options = [ - { - label: "Read (default)", - value: "read" - }, - { - label: "Write", - value: "write" - } - ]; - - return ( - - <> - {options.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} - - - ); - }} - /> -
- - - - -
- -
-
- handlePopUpToggle("removeProjectLevelPermission", isOpen)} - onDeleteApproved={onRemoveProjectLevelPermission} - /> -
- ); -}; diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx deleted file mode 100644 index fee5544ef..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SAProjectLevelPermissionsTable } from "./SAProjectLevelPermissionsTable"; \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx deleted file mode 100644 index 4c6cd64b7..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheck } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { - Button, - FormControl, - Input} from "@app/components/v2"; -import { - useGetServiceAccountById, - useRenameServiceAccount -} from "@app/hooks/api"; - -const formSchema = yup.object({ - name: yup.string().required().label("Service Account Name") -}); - -type FormData = yup.InferType; - -type Props = { - serviceAccountId: string; -} - -export const ServiceAccountNameChangeSection = ({ - serviceAccountId -}: Props) => { - const { data: serviceAccount, isLoading: isServiceAccountLoading } = useGetServiceAccountById(serviceAccountId); - - const renameServiceAccount = useRenameServiceAccount(); - - const { - handleSubmit, - control, - reset, - formState: { isDirty, isSubmitting } - } = useForm({ resolver: yupResolver(formSchema) }); - - useEffect(() => { - reset({ name: serviceAccount?.name }); - }, [serviceAccount?.name]); - - const onFormSubmit = async ({ name }: FormData) => { - try { - await renameServiceAccount.mutateAsync({ - serviceAccountId, - name - }); - } catch (err) { - console.error(err); - } - } - - return ( -
-

Name

-
- {!isServiceAccountLoading && ( - ( - - - - )} - control={control} - name="name" - /> - )} -
- -
- ); -} diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx deleted file mode 100644 index bedbfa7a4..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ServiceAccountNameChangeSection } from "./ServiceAccountNameChangeSection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx deleted file mode 100644 index 20d12ed5a..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export { CopyServiceAccountIDSection } from "./CopyServiceAccountIDSection"; -export { CopyServiceAccountPublicKeySection } from "./CopyServiceAccountPublicKeySection"; -export { SAProjectLevelPermissionsTable } from "./SAProjectLevelPermissionsTable"; -export { ServiceAccountNameChangeSection } from "./ServiceAccountNameChangeSection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/index.tsx deleted file mode 100644 index 8dfecafb1..000000000 --- a/frontend/src/views/Settings/CreateServiceAccountPage/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CreateServiceAccountPage } from "./CreateServiceAccountPage"; \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx index 8e765f3ff..374681003 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx @@ -4,14 +4,13 @@ import { } from "@app/components/utilities/cryptography/crypto"; import { Checkbox } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api"; - -import getLatestFileKey from "../../../../../pages/api/workspace/getLatestFileKey"; +import { useGetUserWsKey,useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api"; export const E2EESection = () => { const { currentWorkspace } = useWorkspace(); const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? ""); const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus(); + const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); /** * Activate bot for project by performing the following steps: @@ -25,14 +24,12 @@ export const E2EESection = () => { try { if (!currentWorkspace?._id) return; - if (bot) { + if (bot && wsKey) { // case: there is a bot if (!bot.isActive) { // bot is not active -> activate bot - const key = await getLatestFileKey({ - workspaceId: currentWorkspace._id - }); + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); if (!PRIVATE_KEY) { @@ -40,9 +37,9 @@ export const E2EESection = () => { } const WORKSPACE_KEY = decryptAssymmetric({ - ciphertext: key.latestKey.encryptedKey, - nonce: key.latestKey.nonce, - publicKey: key.latestKey.sender.publicKey, + ciphertext: wsKey.encryptedKey, + nonce: wsKey.nonce, + publicKey: wsKey.sender.publicKey, privateKey: PRIVATE_KEY }); diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 032552958..01002c38f 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -17,8 +17,8 @@ import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLo import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input } from "@app/components/v2"; import { useGetCommonPasswords } from "@app/hooks/api"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; import ProjectService from "@app/services/ProjectService"; // eslint-disable-next-line new-cap @@ -188,7 +188,7 @@ export const UserInfoSSOStep = ({ privateKey }); - const userOrgs = await getOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]?._id; const project = await ProjectService.initProject({ organizationId: orgId, From 27f56be4663e31fdc8c1ecc88c5133704e2ac812 Mon Sep 17 00:00:00 2001 From: Hahnbee Lee <55263191+hahnbeelee@users.noreply.github.com> Date: Thu, 10 Aug 2023 14:33:37 -0700 Subject: [PATCH 38/64] cursor-pointer fir Explore button --- frontend/src/pages/org/[id]/overview/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 1957d29f5..15ce5a9f6 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -404,7 +404,7 @@ export default function Organization() { localStorage.setItem("projectData.id", workspace._id); }} > -
+
Explore{" "} Date: Fri, 11 Aug 2023 11:27:33 +0700 Subject: [PATCH 39/64] Remove remaining SecurityClient auth calls in favor of hooks, keep RouteGuard --- backend/src/routes/v1/key.ts | 2 +- backend/src/routes/v1/membership.ts | 2 +- .../src/components/signup/CodeInputStep.tsx | 7 +- .../src/components/signup/EnterEmailStep.tsx | 7 +- .../src/components/signup/UserInfoStep.tsx | 4 +- .../utilities/attemptChangePassword.ts | 9 +- .../components/utilities/attemptCliLogin.ts | 3 +- .../utilities/attemptCliLoginMfa.ts | 6 +- .../src/components/utilities/attemptLogin.ts | 6 +- .../components/utilities/attemptLoginMfa.ts | 5 +- .../utilities/cryptography/changePassword.ts | 147 ------------ .../utilities/cryptography/issueBackupKey.ts | 34 +-- frontend/src/hooks/api/auth/index.tsx | 8 +- frontend/src/hooks/api/auth/queries.tsx | 219 +++++++++++++++++- frontend/src/hooks/api/auth/types.ts | 103 ++++++++ frontend/src/hooks/api/users/queries.tsx | 4 +- .../src/pages/api/auth/ChangePassword2.ts | 46 ---- frontend/src/pages/api/auth/CheckAuth.ts | 5 +- .../api/auth/CheckEmailVerificationCode.ts | 24 -- .../auth/CompleteAccountInformationSignup.ts | 79 ------- .../CompleteAccountInformationSignupInvite.ts | 70 ------ .../api/auth/EmailVerifyOnPasswordReset.ts | 34 --- .../pages/api/auth/IssueBackupPrivateKey.ts | 51 ---- frontend/src/pages/api/auth/Login1.ts | 33 --- frontend/src/pages/api/auth/Login2.ts | 42 ---- frontend/src/pages/api/auth/Logout.ts | 41 ---- frontend/src/pages/api/auth/SRP1.ts | 29 --- .../api/auth/SendEmailOnPasswordReset.ts | 33 --- .../pages/api/auth/SendVerificationEmail.ts | 17 -- frontend/src/pages/api/auth/Token.ts | 16 -- .../src/pages/api/auth/VerifySignupInvite.ts | 27 --- .../api/auth/getBackupEncryptedPrivateKey.ts | 24 -- .../src/pages/api/auth/publicKeyInfisical.ts | 8 - .../auth/resetPasswordOnAccountRecovery.ts | 57 ----- frontend/src/pages/api/auth/verifyMfaToken.ts | 25 -- frontend/src/pages/password-reset.tsx | 39 ++-- frontend/src/pages/signup/index.tsx | 26 ++- frontend/src/pages/signupinvite.tsx | 50 ++-- frontend/src/pages/verify-email.tsx | 7 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 4 +- 40 files changed, 443 insertions(+), 910 deletions(-) delete mode 100644 frontend/src/components/utilities/cryptography/changePassword.ts delete mode 100644 frontend/src/pages/api/auth/ChangePassword2.ts delete mode 100644 frontend/src/pages/api/auth/CheckEmailVerificationCode.ts delete mode 100644 frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts delete mode 100644 frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts delete mode 100644 frontend/src/pages/api/auth/EmailVerifyOnPasswordReset.ts delete mode 100644 frontend/src/pages/api/auth/IssueBackupPrivateKey.ts delete mode 100644 frontend/src/pages/api/auth/Login1.ts delete mode 100644 frontend/src/pages/api/auth/Login2.ts delete mode 100644 frontend/src/pages/api/auth/Logout.ts delete mode 100644 frontend/src/pages/api/auth/SRP1.ts delete mode 100644 frontend/src/pages/api/auth/SendEmailOnPasswordReset.ts delete mode 100644 frontend/src/pages/api/auth/SendVerificationEmail.ts delete mode 100644 frontend/src/pages/api/auth/Token.ts delete mode 100644 frontend/src/pages/api/auth/VerifySignupInvite.ts delete mode 100644 frontend/src/pages/api/auth/getBackupEncryptedPrivateKey.ts delete mode 100644 frontend/src/pages/api/auth/publicKeyInfisical.ts delete mode 100644 frontend/src/pages/api/auth/resetPasswordOnAccountRecovery.ts delete mode 100644 frontend/src/pages/api/auth/verifyMfaToken.ts diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index 2274b3c3f..a72b508b9 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -26,7 +26,7 @@ router.post( keyController.uploadKey ); -router.get( +router.get( // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something) "/:workspaceId/latest", requireAuth({ acceptedAuthModes: [AuthMode.JWT], diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index ff4107022..cf38c7cbc 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -9,7 +9,7 @@ import { AuthMode } from "../../variables"; // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) // TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful -router.get( // used for old CLI (deprecate) +router.get( // TODO endpoint: deprecate - used for old CLI (deprecate) "/:workspaceId/connect", requireAuth({ acceptedAuthModes: [AuthMode.JWT], diff --git a/frontend/src/components/signup/CodeInputStep.tsx b/frontend/src/components/signup/CodeInputStep.tsx index 7f6c75871..de831bbd2 100644 --- a/frontend/src/components/signup/CodeInputStep.tsx +++ b/frontend/src/components/signup/CodeInputStep.tsx @@ -3,7 +3,9 @@ import React, { useState } from "react"; import ReactCodeInput from "react-code-input"; import { useTranslation } from "react-i18next"; -import sendVerificationEmail from "@app/pages/api/auth/SendVerificationEmail"; +import { + useSendVerificationEmail +} from "@app/hooks/api"; import Error from "../basic/Error"; import { Button } from "../v2"; @@ -70,6 +72,7 @@ export default function CodeInputStep({ codeError, isCodeInputCheckLoading }: CodeInputStepProps): JSX.Element { + const { mutateAsync } = useSendVerificationEmail(); const [isLoading, setIsLoading] = useState(false); const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false); const { t } = useTranslation(); @@ -77,7 +80,7 @@ export default function CodeInputStep({ const resendVerificationEmail = async () => { setIsResendingVerificationEmail(true); setIsLoading(true); - sendVerificationEmail(email); + await mutateAsync({ email }); setTimeout(() => { setIsLoading(false); setIsResendingVerificationEmail(false); diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/signup/EnterEmailStep.tsx index 479c88318..e317a4ea5 100644 --- a/frontend/src/components/signup/EnterEmailStep.tsx +++ b/frontend/src/components/signup/EnterEmailStep.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; -import sendVerificationEmail from "@app/pages/api/auth/SendVerificationEmail"; +import { useSendVerificationEmail } from "@app/hooks/api"; import { Button, Input } from "../v2"; @@ -25,13 +25,14 @@ export default function EnterEmailStep({ setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element { + const { mutateAsync } = useSendVerificationEmail(); const [emailError, setEmailError] = useState(false); const { t } = useTranslation(); /** * Verifies if the entered email "looks" correct */ - const emailCheck = () => { + const emailCheck = async () => { let emailCheckBool = false; if (!email) { setEmailError(true); @@ -45,7 +46,7 @@ export default function EnterEmailStep({ // If everything is correct, go to the next step if (!emailCheckBool) { - sendVerificationEmail(email); + await mutateAsync({ email }); incrementStep(); } }; diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index b0a0d420f..db4d040ad 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -9,8 +9,8 @@ import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import { useGetCommonPasswords } from "@app/hooks/api"; +import { completeAccountSignup } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup"; import ProjectService from "@app/services/ProjectService"; import InputField from "../basic/InputField"; @@ -159,7 +159,7 @@ export default function UserInfoStep({ secret: Buffer.from(derivedKey.hash) }); - const response = await completeAccountInformationSignup({ + const response = await completeAccountSignup({ email, firstName: name.split(" ")[0], lastName: name.split(" ").slice(1).join(" "), diff --git a/frontend/src/components/utilities/attemptChangePassword.ts b/frontend/src/components/utilities/attemptChangePassword.ts index 5b42e0190..59363129f 100644 --- a/frontend/src/components/utilities/attemptChangePassword.ts +++ b/frontend/src/components/utilities/attemptChangePassword.ts @@ -3,8 +3,9 @@ import crypto from "crypto"; import jsrp from "jsrp"; -import changePassword2 from "@app/pages/api/auth/ChangePassword2"; -import SRP1 from "@app/pages/api/auth/SRP1"; +import { +changePassword, + srp1} from "@app/hooks/api/auth/queries"; import Aes256Gcm from "./cryptography/aes-256-gcm"; import { deriveArgonKey } from "./cryptography/crypto"; @@ -27,7 +28,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): try { const clientPublicKey = clientOldPassword.getPublicKey(); - const res = await SRP1({ clientPublicKey }); + const res = await srp1({ clientPublicKey }); serverPublicKey = res.serverPublicKey; salt = res.salt; @@ -71,7 +72,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): secret: Buffer.from(derivedKey.hash) }); - await changePassword2({ + await changePassword({ clientProof, protectedKey, protectedKeyIV, diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index 3b9ebc910..b2eeeceea 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -1,10 +1,9 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { login1, login2 } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; -import login1 from "@app/pages/api/auth/Login1"; -import login2 from "@app/pages/api/auth/Login2"; import KeyService from "@app/services/KeyService"; import Telemetry from "./telemetry/Telemetry"; diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts index 2fc6f17b9..6681e8464 100644 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ b/frontend/src/components/utilities/attemptCliLoginMfa.ts @@ -1,10 +1,10 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; -import login1 from "@app/pages/api/auth/Login1"; -import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken"; +// import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken"; import KeyService from "@app/services/KeyService"; import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; @@ -65,7 +65,7 @@ const attemptLoginMfa = async ({ tag } = await verifyMfaToken({ email, - mfaToken + mfaCode: mfaToken }); // unset temporary (MFA) JWT token and set JWT token diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index 29c730e3f..c4fe91b05 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -1,10 +1,9 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { login1, login2 } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; -import login1 from "@app/pages/api/auth/Login1"; -import login2 from "@app/pages/api/auth/Login2"; import KeyService from "@app/services/KeyService"; import Telemetry from "./telemetry/Telemetry"; @@ -46,12 +45,13 @@ const attemptLogin = async ( async () => { try { const clientPublicKey = client.getPublicKey(); + const { serverPublicKey, salt } = await login1({ email, clientPublicKey, providerAuthToken, }); - + client.setSalt(salt); client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts index feb58b596..c588eb965 100644 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ b/frontend/src/components/utilities/attemptLoginMfa.ts @@ -1,10 +1,9 @@ /* eslint-disable prefer-destructuring */ import jsrp from "jsrp"; +import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries"; -import login1 from "@app/pages/api/auth/Login1"; -import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken"; import KeyService from "@app/services/KeyService"; import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; @@ -56,7 +55,7 @@ const attemptLoginMfa = async ({ tag } = await verifyMfaToken({ email, - mfaToken + mfaCode: mfaToken }); // unset temporary (MFA) JWT token and set JWT token diff --git a/frontend/src/components/utilities/cryptography/changePassword.ts b/frontend/src/components/utilities/cryptography/changePassword.ts deleted file mode 100644 index 3e7ad22e0..000000000 --- a/frontend/src/components/utilities/cryptography/changePassword.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import changePassword2 from "@app/pages/api/auth/ChangePassword2"; -import SRP1 from "@app/pages/api/auth/SRP1"; - -import { saveTokenToLocalStorage } from "../saveTokenToLocalStorage"; -import Aes256Gcm from "./aes-256-gcm"; -import { deriveArgonKey } from "./crypto"; - -const clientOldPassword = new jsrp.client(); -const clientNewPassword = new jsrp.client(); - -/** - * This function loggs in the user (whether it's right after signup, or a normal login) - * @param {*} email - * @param {*} password - * @param {*} setErrorLogin - * @param {*} router - * @param {*} isSignUp - * @returns - */ -const changePassword = async ( - email: string, - currentPassword: string, - newPassword: string, - setCurrentPasswordError: (arg: boolean) => void, - setPasswordChanged: (arg: boolean) => void, - setCurrentPassword: (arg: string) => void, - setNewPassword: (arg: string) => void -) => { - try { - setPasswordChanged(false); - setCurrentPasswordError(false); - - clientOldPassword.init( - { - username: email, - password: currentPassword - }, - async () => { - const clientPublicKey = clientOldPassword.getPublicKey(); - - let serverPublicKey; - let salt; - try { - const res = await SRP1({ - clientPublicKey - }); - serverPublicKey = res.serverPublicKey; - salt = res.salt; - } catch (err) { - setCurrentPasswordError(true); - console.log("Wrong current password", err, 1); - } - - clientOldPassword.setSalt(salt); - clientOldPassword.setServerPublicKey(serverPublicKey); - const clientProof = clientOldPassword.getProof(); // called M1 - - clientNewPassword.init( - { - username: email, - password: newPassword - }, - async () => { - clientNewPassword.createVerifier(async (err, result) => { - - const derivedKey = await deriveArgonKey({ - password: newPassword, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - try { - await changePassword2({ - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier - }); - - saveTokenToLocalStorage({ - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - }); - - setPasswordChanged(true); - setCurrentPassword(""); - setNewPassword(""); - - window.location.href = "/login"; - - // move to login page - } catch (error) { - setCurrentPasswordError(true); - console.log(error); - } - }); - } - ); - } - ); - } catch (error) { - console.log("Something went wrong during changing the password"); - } - return true; -}; - -export default changePassword; diff --git a/frontend/src/components/utilities/cryptography/issueBackupKey.ts b/frontend/src/components/utilities/cryptography/issueBackupKey.ts index 9f503e0c9..4391d027f 100644 --- a/frontend/src/components/utilities/cryptography/issueBackupKey.ts +++ b/frontend/src/components/utilities/cryptography/issueBackupKey.ts @@ -3,8 +3,9 @@ import crypto from "crypto"; import jsrp from "jsrp"; -import issueBackupPrivateKey from "@app/pages/api/auth/IssueBackupPrivateKey"; -import SRP1 from "@app/pages/api/auth/SRP1"; +import { issueBackupPrivateKey , + srp1 +} from "@app/hooks/api/auth/queries"; import generateBackupPDF from "../generateBackupPDF"; import Aes256Gcm from "./aes-256-gcm"; @@ -51,7 +52,7 @@ const issueBackupKey = async ({ let serverPublicKey; let salt; try { - const res = await SRP1({ + const res = await srp1({ clientPublicKey }); serverPublicKey = res.serverPublicKey; @@ -61,8 +62,8 @@ const issueBackupKey = async ({ console.log("Wrong current password", err, 1); } - clientPassword.setSalt(salt); - clientPassword.setServerPublicKey(serverPublicKey); + clientPassword.setSalt(salt as string); + clientPassword.setServerPublicKey(serverPublicKey as string); const clientProof = clientPassword.getProof(); // called M1 const generatedKey = crypto.randomBytes(16).toString("hex"); @@ -80,24 +81,25 @@ const issueBackupKey = async ({ secret: generatedKey }); - const res = await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - clientProof - }); + try { + await issueBackupPrivateKey({ + encryptedPrivateKey: ciphertext, + iv, + tag, + salt: result.salt, + verifier: result.verifier, + clientProof + }); - if (res?.status === 400) { - setBackupKeyError(true); - } else if (res?.status === 200) { generateBackupPDF({ personalName, personalEmail: email, generatedKey }); setBackupKeyIssued(true); + + } catch { + setBackupKeyError(true); } } ); diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index a4f967888..dbcc77a5a 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,6 +1,10 @@ export { useGetAuthToken, useGetCommonPasswords, + useResetPassword, useSendMfaToken, - useVerifyMfaToken -} from "./queries" + useSendPasswordResetEmail, + useSendVerificationEmail, + useVerifyEmailVerificationCode, + useVerifyMfaToken, + useVerifyPasswordResetCode} from "./queries" diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 57878d2b3..094fd4b61 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -4,16 +4,86 @@ import { apiRequest } from "@app/config/request"; import { setAuthToken } from "@app/reactQuery"; import { + ChangePasswordDTO, + CompleteAccountDTO, + CompleteAccountSignupDTO, GetAuthTokenAPI, + GetBackupEncryptedPrivateKeyDTO, + IssueBackupPrivateKeyDTO, + Login1DTO, + Login1Res, + Login2DTO, + Login2Res, + ResetPasswordDTO, SendMfaTokenDTO, + SRP1DTO, + SRPR1Res, VerifyMfaTokenDTO, - VerifyMfaTokenRes} from "./types"; + VerifyMfaTokenRes, + VerifySignupInviteDTO} from "./types"; const authKeys = { getAuthToken: ["token"] as const, commonPasswords: ["common-passwords"] as const }; +export const login1 = async (loginDetails: Login1DTO) => { + const { data } = await apiRequest.post("/api/v3/auth/login1", loginDetails); + return data; +} + +export const login2 = async (loginDetails: Login2DTO) => { + const { data } = await apiRequest.post("/api/v3/auth/login2", loginDetails); + return data; +} + +export const useLogin1 = () => { + return useMutation({ + mutationFn: async (details: { + email: string; + clientPublicKey: string; + providerAuthToken?: string; + }) => { + return login1(details); + } + }); +} + +export const useLogin2 = () => { + return useMutation({ + mutationFn: async (details: { + email: string; + clientProof: string; + providerAuthToken?: string; + }) => { + return login2(details); + } + }); +} + +export const srp1 = async (details: SRP1DTO) => { + const { data } = await apiRequest.post("/api/v1/password/srp1", details); + return data; +} + +export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { + const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); + return data; +} + +export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => { + const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details); + return data; +} + +export const useCompleteAccountSignup = () => { + return useMutation({ + mutationFn: async (details: CompleteAccountSignupDTO) => { + return completeAccountSignup(details); + } + }); +} + export const useSendMfaToken = () => { return useMutation<{}, {}, SendMfaTokenDTO>({ mutationFn: async ({ email }) => { @@ -23,18 +93,161 @@ export const useSendMfaToken = () => { }); } +export const verifyMfaToken = async ({ + email, + mfaCode +}: { + email: string; + mfaCode: string; +}) => { + const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { + email, + mfaToken: mfaCode + }); + + return data; +} + export const useVerifyMfaToken = () => { return useMutation({ mutationFn: async ({ email, mfaCode }) => { - const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { + return verifyMfaToken({ email, - mfaToken: mfaCode + mfaCode }); + } + }); +} + +export const verifySignupInvite = async (details: VerifySignupInviteDTO) => { + const { data } = await apiRequest.post("/api/v1/invite-org/verify", details); + return data; +} + +export const useSendVerificationEmail = () => { + return useMutation({ + mutationFn: async ({ + email + }: { + email: string; + }) => { + const { data } = await apiRequest.post("/api/v1/signup/email/signup", { + email + }); + return data; } }); } +export const useVerifyEmailVerificationCode = () => { + return useMutation({ + mutationFn: async ({ + email, + code + }: { + email: string; + code: string; + }) => { + const { data } = await apiRequest.post("/api/v1/signup/email/verify", { + email, + code + }); + + return data; + } + }); +} + +export const useSendPasswordResetEmail = () => { + return useMutation({ + mutationFn: async ({ + email + }: { + email: string; + }) => { + const { data } = await apiRequest.post("/api/v1/password/email/password-reset", { + email + }); + + return data; + } + }); +} + +export const useVerifyPasswordResetCode = () => { + return useMutation({ + mutationFn: async ({ + email, + code + }: { + email: string; + code: string; + }) => { + const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { + email, + code + }); + + return data; + } + }); +} + +export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { + const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); + return data; +} + +export const getBackupEncryptedPrivateKey = async ({ + verificationToken +}: GetBackupEncryptedPrivateKeyDTO) => { + const { data } = await apiRequest.get("/api/v1/password/backup-private-key", { + headers: { + Authorization: `Bearer ${verificationToken}` + } + }); + + return data.backupPrivateKey; +} + +export const useResetPassword = () => { + return useMutation({ + mutationFn: async (details: ResetPasswordDTO) => { + const { data } = await apiRequest.post("/api/v1/password/password-reset", { + protectedKey: details.protectedKey, + protectedKeyIV: details.protectedKeyIV, + protectedKeyTag: details.protectedKeyTag, + encryptedPrivateKey: details.encryptedPrivateKey, + encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, + encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, + salt: details.salt, + verifier: details.verifier + }, { + headers: { + Authorization: `Bearer ${details.verificationToken}` + } + }); + + return data; + } + }); +} + +export const changePassword = async (details: ChangePasswordDTO) => { + const { data } = await apiRequest.post("/api/v1/password/change-password", details); + return data; +} + +export const useChangePassword = () => { + // note: use after srp1 + return useMutation({ + mutationFn: async (details: ChangePasswordDTO) => { + return changePassword(details); + } + }); +} + // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls const fetchAuthToken = async () => { diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index 3d14c19ff..7ed7af566 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -21,4 +21,107 @@ export type VerifyMfaTokenRes = { encryptedPrivateKey: string; iv: string; tag: string; +} + +export type Login1DTO = { + email: string; + clientPublicKey: string; + providerAuthToken?: string; +} + +export type Login2DTO = { + email: string; + clientProof: string; + providerAuthToken?: string; +} + +export type Login1Res = { + serverPublicKey: string; + salt: string; +} + +export type Login2Res = { + mfaEnabled: boolean; + token: string; + encryptionVersion?: number; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; +} + +export type SRP1DTO = { + clientPublicKey: string; +} + +export type SRPR1Res = { + serverPublicKey: string; + salt: string; +} + +export type CompleteAccountDTO = { + email: string; + firstName: string; + lastName: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; +} + +export type CompleteAccountSignupDTO = CompleteAccountDTO & { + providerAuthToken?: string; + attributionSource?: string; + organizationName: string; +} + +export type VerifySignupInviteDTO = { + email: string; + code: string; + organizationId: string; +} + +export type ChangePasswordDTO = { + clientProof: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; +} + +export type ResetPasswordDTO = { + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + verificationToken: string; +} + +export type IssueBackupPrivateKeyDTO = { + encryptedPrivateKey: string; + iv: string; + tag: string; + salt: string; + verifier: string; + clientProof: string; +} + +export type GetBackupEncryptedPrivateKeyDTO = { + verificationToken: string; } \ No newline at end of file diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index a2d9318e5..a0a8ddc2f 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -201,7 +201,9 @@ export const useRegisterUserAction = () => { export const useLogoutUser = () => useMutation({ - mutationFn: () => apiRequest.post("/api/v1/auth/logout"), + mutationFn: async () => { + await apiRequest.post("/api/v1/auth/logout"); + }, onSuccess: () => { setAuthToken(""); // Delete the cookie by not setting a value; Alternatively clear the local storage diff --git a/frontend/src/pages/api/auth/ChangePassword2.ts b/frontend/src/pages/api/auth/ChangePassword2.ts deleted file mode 100644 index 8381c13ad..000000000 --- a/frontend/src/pages/api/auth/ChangePassword2.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { apiRequest } from "@app/config/request"; - -interface Props { - clientProof: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; -} - -/** - * This is the second step of the change password process (pake) - * @param {*} clientPublicKey - * @returns - */ -const changePassword2 = async ({ - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier -}: Props) => { - const { data } = await apiRequest.post("/api/v1/password/change-password", { - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - }); - - return data; -} - -export default changePassword2; diff --git a/frontend/src/pages/api/auth/CheckAuth.ts b/frontend/src/pages/api/auth/CheckAuth.ts index 9b39c6933..f1b98f087 100644 --- a/frontend/src/pages/api/auth/CheckAuth.ts +++ b/frontend/src/pages/api/auth/CheckAuth.ts @@ -4,12 +4,13 @@ import SecurityClient from "@app/components/utilities/SecurityClient"; * This function is used to check if the user is authenticated. * To do that, we get their tokens from cookies, and verify if they are good. */ -const checkAuth = async () => - SecurityClient.fetchCall("/api/v1/auth/checkAuth", { +const checkAuth = async () => { + return SecurityClient.fetchCall("/api/v1/auth/checkAuth", { method: "POST", headers: { "Content-Type": "application/json" } }).then((res) => res); +} export default checkAuth; diff --git a/frontend/src/pages/api/auth/CheckEmailVerificationCode.ts b/frontend/src/pages/api/auth/CheckEmailVerificationCode.ts deleted file mode 100644 index 5cbd9dc2b..000000000 --- a/frontend/src/pages/api/auth/CheckEmailVerificationCode.ts +++ /dev/null @@ -1,24 +0,0 @@ -interface Props { - email: string; - code: string; -} - -/** - * This route check the verification code from the email that user just recieved - * @param {object} obj - * @param {string} obj.email - * @param {string} obj.code - * @returns - */ -const checkEmailVerificationCode = ({ email, code }: Props) => fetch("/api/v1/signup/email/verify", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email, - code - }) - }); - -export default checkEmailVerificationCode; diff --git a/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts b/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts deleted file mode 100644 index 37ceb4e3a..000000000 --- a/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts +++ /dev/null @@ -1,79 +0,0 @@ - -import { apiRequest } from "@app/config/request"; - -interface Props { - email: string; - firstName: string; - lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - providerAuthToken?: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - organizationName: string; - salt: string; - verifier: string; - attributionSource?: string; -} - -/** - * This function is called in the end of the signup process. - * It sends all the necessary nformation to the server. - * @param {object} obj - * @param {string} obj.email - email of the user completing signup - * @param {string} obj.firstName - first name of the user completing signup - * @param {string} obj.lastName - last name of the user completing sign up - * @param {string} obj.protectedKey - protected key in encryption version 2 - * @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2 - * @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2 - * @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization) - * @param {string} obj.publicKey - public key of the user completing signup - * @param {string} obj.ciphertext - * @param {string} obj.iv - * @param {string} obj.tag - * @param {string} obj.salt - * @param {string} obj.verifier - * @returns - */ -const completeAccountInformationSignup = async ({ - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - organizationName, - providerAuthToken, - attributionSource -}: Props) => { - const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", { - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - organizationName, - providerAuthToken, - ...(attributionSource ? { attributionSource } : {}) - }); - - return data; -} - -export default completeAccountInformationSignup; diff --git a/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts b/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts deleted file mode 100644 index e2264b3d6..000000000 --- a/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { apiRequest } from "@app/config/request"; - -interface Props { - email: string; - firstName: string; - lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; -} - -// missing token? -// TODO: add to SecurityClient - - -/** - * This function is called in the end of the signup process. - * It sends all the necessary nformation to the server. - * @param {object} obj - * @param {string} obj.email - email of the user completing signupinvite flow - * @param {string} obj.firstName - first name of the user completing signupinvite flow - * @param {string} obj.lastName - last name of the user completing signupinvite flow - * @param {string} obj.publicKey - public key of the user completing signupinvite flow - * @param {string} obj.ciphertext - * @param {string} obj.iv - * @param {string} obj.tag - * @param {string} obj.salt - * @param {string} obj.verifier - * @param {string} obj.token - token that confirms a user's identity - * @returns - */ -const completeAccountInformationSignupInvite = async ({ - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier -}: Props) => { - const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", { - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - }); - - return data; -} - -export default completeAccountInformationSignupInvite; diff --git a/frontend/src/pages/api/auth/EmailVerifyOnPasswordReset.ts b/frontend/src/pages/api/auth/EmailVerifyOnPasswordReset.ts deleted file mode 100644 index 8e601c6a4..000000000 --- a/frontend/src/pages/api/auth/EmailVerifyOnPasswordReset.ts +++ /dev/null @@ -1,34 +0,0 @@ -interface Props { - email: string; - code: string; -} - -/** - * This is the second part of the account recovery step (a user needs to verify their email). - * A user need to click on a button in a magic link page - * @param {object} obj - * @param {object} obj.email - email of a user that is trying to recover access to their account - * @param {object} obj.code - token that a use received via the magic link - * @returns - */ -const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => { - const response = await fetch("/api/v1/password/email/password-reset-verify", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email, - code - }) - }); - if (response?.status === 200) { - return response; - } - - throw new Error( - "Something went wrong during email verification on password reset." - ); -}; - -export default EmailVerifyOnPasswordReset; diff --git a/frontend/src/pages/api/auth/IssueBackupPrivateKey.ts b/frontend/src/pages/api/auth/IssueBackupPrivateKey.ts deleted file mode 100644 index e5fe18359..000000000 --- a/frontend/src/pages/api/auth/IssueBackupPrivateKey.ts +++ /dev/null @@ -1,51 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - encryptedPrivateKey: string; - iv: string; - tag: string; - salt: string; - verifier: string; - clientProof: string; -} - -/** - * This is the route that issues a backup private key that will afterwards be added into a pdf - * @param {object} obj - * @param {string} obj.encryptedPrivateKey - * @param {string} obj.iv - * @param {string} obj.tag - * @param {string} obj.salt - * @param {string} obj.verifier - * @param {string} obj.clientProof - * @returns - */ -const issueBackupPrivateKey = ({ - encryptedPrivateKey, - iv, - tag, - salt, - verifier, - clientProof -}: Props) => - SecurityClient.fetchCall("/api/v1/password/backup-private-key", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - clientProof, - encryptedPrivateKey, - iv, - tag, - salt, - verifier - }) - }).then((res) => { - if (res?.status !== 200) { - console.log("Failed to issue the backup key"); - } - return res; - }); - -export default issueBackupPrivateKey; diff --git a/frontend/src/pages/api/auth/Login1.ts b/frontend/src/pages/api/auth/Login1.ts deleted file mode 100644 index 85377d34c..000000000 --- a/frontend/src/pages/api/auth/Login1.ts +++ /dev/null @@ -1,33 +0,0 @@ -interface Login1 { - serverPublicKey: string; - salt: string; -} - -/** - * This is the first step of the login process (pake) - * @param {*} email - * @param {*} clientPublicKey - * @returns - */ -const login1 = async (loginDetails: { - email: string; - clientPublicKey: string; - providerAuthToken?: string; -}) => { - const response = await fetch("/api/v3/auth/login1", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(loginDetails), - }); - // need precise error handling about the status code - if (response?.status === 200) { - const data = (await response.json()) as unknown as Login1; - return data; - } - - throw new Error("Wrong password"); -}; - -export default login1; diff --git a/frontend/src/pages/api/auth/Login2.ts b/frontend/src/pages/api/auth/Login2.ts deleted file mode 100644 index ea9df262b..000000000 --- a/frontend/src/pages/api/auth/Login2.ts +++ /dev/null @@ -1,42 +0,0 @@ -interface Login2Response { - mfaEnabled: boolean; - token: string; - encryptionVersion?: number; - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; -} - -/** - * This is the second step of the login process - * @param {*} email - * @param {*} clientPublicKey - * @returns - */ -const login2 = async (loginDetails: { - email: string; - clientProof: string; - providerAuthToken?: string; -}) => { - const response = await fetch("/api/v3/auth/login2", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(loginDetails), - credentials: "include" - }); - // need precise error handling about the status code - if (response.status === 200) { - const data = (await response.json()) as unknown as Login2Response; - return data; - } - - throw new Error("Password verification failed"); -}; - -export default login2; diff --git a/frontend/src/pages/api/auth/Logout.ts b/frontend/src/pages/api/auth/Logout.ts deleted file mode 100644 index 343e093ed..000000000 --- a/frontend/src/pages/api/auth/Logout.ts +++ /dev/null @@ -1,41 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * This route logs the user out. Note: the user should authorized to do this. - * We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry - */ -const logout = async () => { - try { - const res = await SecurityClient.fetchCall("/api/v1/auth/logout", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - credentials: "include" - }); - - if (res?.status === 200) { - SecurityClient.setToken(""); - // Delete the cookie by not setting a value; Alternatively clear the local storage - localStorage.removeItem("protectedKey"); - localStorage.removeItem("protectedKeyIV"); - localStorage.removeItem("protectedKeyTag"); - localStorage.removeItem("publicKey"); - localStorage.removeItem("encryptedPrivateKey"); - localStorage.removeItem("iv"); - localStorage.removeItem("tag"); - localStorage.removeItem("PRIVATE_KEY"); - localStorage.removeItem("orgData.id"); - localStorage.removeItem("projectData.id"); - - return res; - } - - } catch (error) { - console.log("Error logging out", error); - } - - return undefined; -}; - -export default logout; diff --git a/frontend/src/pages/api/auth/SRP1.ts b/frontend/src/pages/api/auth/SRP1.ts deleted file mode 100644 index 142df3d7c..000000000 --- a/frontend/src/pages/api/auth/SRP1.ts +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -interface Props { - clientPublicKey: string; -} - -/** - * This is the first step of the change password process (pake) - * @param {string} clientPublicKey - * @returns - */ -const SRP1 = ({ clientPublicKey }: Props) => - SecurityClient.fetchCall("/api/v1/password/srp1", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - clientPublicKey - }) - }).then(async (res) => { - if (res && res.status === 200) { - return res.json(); - } - console.log("Failed to do the first step of SRP"); - return undefined; - }); - -export default SRP1; diff --git a/frontend/src/pages/api/auth/SendEmailOnPasswordReset.ts b/frontend/src/pages/api/auth/SendEmailOnPasswordReset.ts deleted file mode 100644 index 37c6d14d2..000000000 --- a/frontend/src/pages/api/auth/SendEmailOnPasswordReset.ts +++ /dev/null @@ -1,33 +0,0 @@ -interface Props { - email: string; -} - -/** - * This is the first of the account recovery step (a user needs to verify their email). - * It will send an email containing a magic link to start the account recovery flow. - * @param {object} obj - * @param {object} obj.email - email of a user that is trying to recover access to their account - * @returns - */ -const SendEmailOnPasswordReset = async ({ email }: Props) => { - const response = await fetch("/api/v1/password/email/password-reset", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email - }) - }); - // need precise error handling about the status code - if (response?.status === 200) { - const data = await response.json(); - return data; - } - - throw new Error( - "Something went wrong while sending the email verification for password reset." - ); -}; - -export default SendEmailOnPasswordReset; diff --git a/frontend/src/pages/api/auth/SendVerificationEmail.ts b/frontend/src/pages/api/auth/SendVerificationEmail.ts deleted file mode 100644 index 92180cc04..000000000 --- a/frontend/src/pages/api/auth/SendVerificationEmail.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * This route send the verification email to the user's email (contains a 6-digit verification code) - * @param {*} email - */ -const sendVerificationEmail = (email: string) => { - fetch("/api/v1/signup/email/signup", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email - }) - }); -}; - -export default sendVerificationEmail; diff --git a/frontend/src/pages/api/auth/Token.ts b/frontend/src/pages/api/auth/Token.ts deleted file mode 100644 index 6bd5b5097..000000000 --- a/frontend/src/pages/api/auth/Token.ts +++ /dev/null @@ -1,16 +0,0 @@ -const token = async () => - fetch("/api/v1/auth/token", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - credentials: "include" - }).then(async (res) => { - if (res.status === 200) { - return (await res.json()).token; - } - console.log("Getting a new token failed"); - return undefined; - }); - -export default token; diff --git a/frontend/src/pages/api/auth/VerifySignupInvite.ts b/frontend/src/pages/api/auth/VerifySignupInvite.ts deleted file mode 100644 index 89dbb4db8..000000000 --- a/frontend/src/pages/api/auth/VerifySignupInvite.ts +++ /dev/null @@ -1,27 +0,0 @@ -interface Props { - email: string; - code: string; - organizationId: string; -} - -/** - * This route verifies the signup invite link - * @param {object} obj - * @param {string} obj.email - email that a user is trying to verify - * @param {string} obj.organizationId - id of organization that a user is trying to verify for - * @param {string} obj.code - code that a user received to the abovementioned email - * @returns - */ -const verifySignupInvite = ({ email, organizationId, code }: Props) => fetch("/api/v1/invite-org/verify", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - email, - organizationId, - code - }) - }); - -export default verifySignupInvite; diff --git a/frontend/src/pages/api/auth/getBackupEncryptedPrivateKey.ts b/frontend/src/pages/api/auth/getBackupEncryptedPrivateKey.ts deleted file mode 100644 index f344b575e..000000000 --- a/frontend/src/pages/api/auth/getBackupEncryptedPrivateKey.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * This is the route that get an encrypted private key (will be decrypted with a backup key) - * @param {object} obj - * @param {object} obj.verificationToken - this is the token that confirms that a user is the right one - * @returns - */ -const getBackupEncryptedPrivateKey = ({ - verificationToken -}: { - verificationToken: string; -}) => fetch("/api/v1/password/backup-private-key", { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${ verificationToken}` - } - }).then(async (res) => { - if (res?.status !== 200) { - console.log("Failed to get the backup key"); - } - return (await res?.json())?.backupPrivateKey; - }); - -export default getBackupEncryptedPrivateKey; diff --git a/frontend/src/pages/api/auth/publicKeyInfisical.ts b/frontend/src/pages/api/auth/publicKeyInfisical.ts deleted file mode 100644 index 60caa411a..000000000 --- a/frontend/src/pages/api/auth/publicKeyInfisical.ts +++ /dev/null @@ -1,8 +0,0 @@ -const publicKeyInfisical = () => fetch("/api/v1/key/publicKey/infisical", { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }); - -export default publicKeyInfisical; diff --git a/frontend/src/pages/api/auth/resetPasswordOnAccountRecovery.ts b/frontend/src/pages/api/auth/resetPasswordOnAccountRecovery.ts deleted file mode 100644 index aa93b03ed..000000000 --- a/frontend/src/pages/api/auth/resetPasswordOnAccountRecovery.ts +++ /dev/null @@ -1,57 +0,0 @@ -interface Props { - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; - verificationToken: string; -} - -/** - * This is the route that resets the account password if all the previus steps were passed - * @param {object} obj - * @param {object} obj.verificationToken - this is the token that confirms that a user is the right one - * @param {object} obj.encryptedPrivateKey - the new encrypted private key (encrypted using the new password) - * @param {object} obj.iv - * @param {object} obj.tag - * @param {object} obj.salt - * @param {object} obj.verifier - * @returns - */ -const resetPasswordOnAccountRecovery = ({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - verificationToken, -}: Props) => fetch("/api/v1/password/password-reset", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${verificationToken}` - }, - body: JSON.stringify({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - }) - }).then(async (res) => { - if (res?.status !== 200) { - console.log("Failed to get the backup key"); - } - return res; - }); - -export default resetPasswordOnAccountRecovery; diff --git a/frontend/src/pages/api/auth/verifyMfaToken.ts b/frontend/src/pages/api/auth/verifyMfaToken.ts deleted file mode 100644 index fc298d0fe..000000000 --- a/frontend/src/pages/api/auth/verifyMfaToken.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { apiRequest } from "@app/config/request"; - -/** - * Verify MFA token [mfaToken] for user with email [email] - * @param {object} obj - * @param {string} obj.email - email of user - * @param {string} obj.mfaToken - MFA cod/token to verify - * @returns - */ -const verifyMfaToken = async ({ - email, - mfaToken -}: { - email: string; - mfaToken: string; -}) => { - const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { - email, - mfaToken - }); - - return data; -} - -export default verifyMfaToken; diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 30efc2c3b..b38f67c01 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -12,11 +12,10 @@ import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; import passwordCheck from "@app/components/utilities/checks/PasswordCheck"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; +import { useResetPassword,useVerifyPasswordResetCode } from "@app/hooks/api"; +import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries"; import { deriveArgonKey } from "../components/utilities/cryptography/crypto"; -import EmailVerifyOnPasswordReset from "./api/auth/EmailVerifyOnPasswordReset"; -import getBackupEncryptedPrivateKey from "./api/auth/getBackupEncryptedPrivateKey"; -import resetPasswordOnAccountRecovery from "./api/auth/resetPasswordOnAccountRecovery"; // eslint-disable-next-line new-cap const client = new jsrp.client(); @@ -34,6 +33,10 @@ export default function PasswordReset() { const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const router = useRouter(); + + const { mutateAsync: verifyPasswordResetCodeMutateAsync } = useVerifyPasswordResetCode(); + const { mutateAsync: resetPasswordMutateAsync } = useResetPassword(); + const parsedUrl = queryString.parse(router.asPath.split("?")[1]); const token = parsedUrl.token as string; const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); @@ -43,7 +46,7 @@ export default function PasswordReset() { e.preventDefault(); try { const result = await getBackupEncryptedPrivateKey({ verificationToken }); - + setPrivateKey( Aes256Gcm.decrypt({ ciphertext: result.encryptedPrivateKey, @@ -53,7 +56,8 @@ export default function PasswordReset() { }) ); setStep(3); - } catch { + } catch(err) { + console.error(err); setBackupKeyError(true); } }; @@ -112,7 +116,7 @@ export default function PasswordReset() { secret: Buffer.from(derivedKey.hash) }); - const response = await resetPasswordOnAccountRecovery({ + await resetPasswordMutateAsync({ protectedKey, protectedKeyIV, protectedKeyTag, @@ -123,11 +127,9 @@ export default function PasswordReset() { verifier: result.verifier, verificationToken }); + + router.push("/login"); - // if everything works, go the main dashboard page. - if (response?.status === 200) { - router.push("/login"); - } setLoading(false) }); } @@ -146,15 +148,16 @@ export default function PasswordReset() { - +
); } From c0f3aecad3a6e1aecb255eb0d3b568c2d30a178c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 13 Aug 2023 11:12:07 +0700 Subject: [PATCH 43/64] Fix lint issues --- .../AuthMethodSection/AuthMethodSection.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx index 0f3647350..42917aae6 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx @@ -119,15 +119,16 @@ export const AuthMethodSection = () => { {user && authMethodOpts.map((authMethodOpt) => { return (
-
+
-

{authMethodOpt.label}

onAuthMethodToggle(value, authMethodOpt)} isChecked={authMethods?.includes(authMethodOpt.value) ?? false} - /> + > +

{authMethodOpt.label}

+
); })} From 95d25b114eafe7060993682233cca5abae6063f1 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 13 Aug 2023 14:08:26 +0700 Subject: [PATCH 44/64] Fix incorrect field in validateProviderAuthToken --- backend/src/controllers/v1/signupController.ts | 5 +++-- backend/src/controllers/v3/signupController.ts | 12 +++++++++++- backend/src/helpers/auth.ts | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index b545320a6..ce464e214 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import { User } from "../../models"; +import { AuthMethod, User } from "../../models"; import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup"; import { createToken } from "../../helpers/auth"; import { BadRequestError } from "../../utils/errors"; @@ -81,7 +81,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { if (!user) { user = await new User({ - email + email, + authMethods: [AuthMethod.EMAIL] }).save(); } diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index 12f225c4e..f17384189 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -117,7 +117,17 @@ export const completeAccountSignup = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null - if (!user.authMethods?.includes(AuthMethod.OKTA_SAML)) { + const hasSamlEnabled = user.authMethods + .some( + (authMethod: AuthMethod) => + [ + AuthMethod.OKTA_SAML, + AuthMethod.AZURE_SAML, + AuthMethod.JUMPCLOUD_SAML + ].includes(authMethod) + ); + + if (!hasSamlEnabled) { // TODO: modify this part // initialize default organization and workspace await initializeDefaultOrg({ organizationName, diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index e15283c75..f24c54859 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -408,7 +408,7 @@ export const validateProviderAuthToken = async ({ ); if ( - !user.authMethods.includes(decodedToken.authProvider) || + !user.authMethods.includes(decodedToken.authMethod) || decodedToken.email !== email ) { throw new Error("Invalid authentication credentials.") From dd8f55804c9882131b3829fc63f94d1b17cb76c1 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 13 Aug 2023 16:18:11 +0800 Subject: [PATCH 45/64] finalized sso controller --- backend/src/ee/controllers/v1/ssoController.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/backend/src/ee/controllers/v1/ssoController.ts b/backend/src/ee/controllers/v1/ssoController.ts index 55c518192..d75cf25d2 100644 --- a/backend/src/ee/controllers/v1/ssoController.ts +++ b/backend/src/ee/controllers/v1/ssoController.ts @@ -157,10 +157,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => { } }, { - authProviders: [ssoConfig.authProvider], - $unset: { - authProvider: 1 - } + authMethods: [ssoConfig.authProvider], } ); } else { @@ -171,10 +168,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => { } }, { - authProviders: [AuthMethod.EMAIL], - $unset: { - authProvider: 1, - } + authMethods: [AuthMethod.EMAIL], } ); } From 05be5910d0a4a735f66d877d1e44e1a9e8f992db Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 13 Aug 2023 17:18:18 +0700 Subject: [PATCH 46/64] Update changelog --- docs/changelog/overview.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 9ea4cdf13..773642c12 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -6,7 +6,9 @@ The changelog below reflects new product developments and updates on a monthly b ## August 2023 +- Release Audit Logs V2. - Add support for GitHub SSO. +- Enable users to opt in for multiple authentication methods. ## July 2023 From bc108a82b6977183daf007e81f18013e9f05d63b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 13 Aug 2023 22:47:29 +0700 Subject: [PATCH 47/64] Add SSO linking feature for existing users --- backend/src/utils/auth.ts | 8 +++- frontend/src/hooks/api/users/queries.tsx | 3 +- frontend/src/views/Login/LoginSSO.tsx | 5 +-- .../Login/components/MFAStep/MFAStep.tsx | 32 ++++++++++++++-- .../components/PasswordStep/PasswordStep.tsx | 38 ++++++++++++++++--- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 2516337af..f7a1a504c 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -106,8 +106,9 @@ const initializePassport = async () => { }).save(); } + let isLinkingRequired = false; if (!user.authMethods.includes(AuthMethod.GOOGLE)) { - done(InternalServerError()); + isLinkingRequired = true; } const isUserCompleted = !!user.publicKey; @@ -119,6 +120,7 @@ const initializePassport = async () => { lastName: user.lastName, authMethod: AuthMethod.GOOGLE, isUserCompleted, + isLinkingRequired, ...(req.query.state ? { callbackPort: req.query.state as string } : {}) @@ -159,8 +161,9 @@ const initializePassport = async () => { }).save(); } + let isLinkingRequired = false; if (!user.authMethods.includes(AuthMethod.GITHUB)) { - done(InternalServerError()); + isLinkingRequired = true; } const isUserCompleted = !!user.publicKey; @@ -172,6 +175,7 @@ const initializePassport = async () => { lastName: user.lastName, authMethod: AuthMethod.GITHUB, isUserCompleted, + isLinkingRequired, ...(req.query.state ? { callbackPort: req.query.state as string } : {}) diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index e8a7a45f6..90314a1e4 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -20,7 +20,8 @@ import { RenameUserDTO, TokenVersion, UpdateOrgUserRoleDTO, - User} from "./types"; + User +} from "./types"; const userKeys = { getUser: ["user"] as const, diff --git a/frontend/src/views/Login/LoginSSO.tsx b/frontend/src/views/Login/LoginSSO.tsx index 548bee3ae..8140b872f 100644 --- a/frontend/src/views/Login/LoginSSO.tsx +++ b/frontend/src/views/Login/LoginSSO.tsx @@ -16,8 +16,7 @@ export const LoginSSO = ({ providerAuthToken }: Props) => { const { email, - isUserCompleted, - callbackPort + isUserCompleted } = jwt_decode(providerAuthToken) as any; useEffect(() => { @@ -36,7 +35,6 @@ export const LoginSSO = ({ providerAuthToken }: Props) => { return ( { return ( diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index c607502c9..571d7ee56 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -3,14 +3,19 @@ import ReactCodeInput from "react-code-input"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import axios from "axios" +import jwt_decode from "jwt-decode"; import Error from "@app/components/basic/Error"; // which to notification import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa" import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; import { Button } from "@app/components/v2"; +import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { useSendMfaToken } from "@app/hooks/api/auth"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchUserDetails } from "@app/hooks/api/users/queries"; +import { AuthMethod } from "@app/hooks/api/users/types"; + // The style for the verification code input const props = { @@ -54,8 +59,7 @@ interface VerifyMfaTokenError { export const MFAStep = ({ email, password, - providerAuthToken, - callbackPort + providerAuthToken }: Props) => { const { createNotification } = useNotificationContext(); const router = useRouter(); @@ -67,9 +71,22 @@ export const MFAStep = ({ const { t } = useTranslation(); const sendMfaToken = useSendMfaToken(); + const { mutateAsync: updateUserAuthMethodsMutateAsync } = useUpdateUserAuthMethods(); const handleLoginMfa = async () => { try { + let isLinkingRequired: undefined | boolean; + let callbackPort: undefined | string; + let authMethod: undefined | AuthMethod; + + if (providerAuthToken) { + const decodedToken = jwt_decode(providerAuthToken) as any; + + isLinkingRequired = decodedToken.isLinkingRequired; + callbackPort = decodedToken.callbackPort; + authMethod = decodedToken.authMethod; + } + if (mfaCode.length !== 6) { createNotification({ text: "Please enter a 6-digit MFA code and try again", @@ -79,7 +96,7 @@ export const MFAStep = ({ } setIsLoading(true); - if (callbackPort){ + if (callbackPort) { // attemptCliLogin const isCliLoginSuccessful = await attemptCliLoginMfa({ @@ -118,6 +135,15 @@ export const MFAStep = ({ text: "Successfully logged in", type: "success" }); + + if (isLinkingRequired && authMethod) { + const user = await fetchUserDetails(); + const newAuthMethods = [...user.authMethods, authMethod] + await updateUserAuthMethodsMutateAsync({ + authMethods: newAuthMethods + }); + } + router.push(`/org/${userOrg}/overview`); } else { createNotification({ diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index dae175eb1..5a5e7c99d 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -3,16 +3,18 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router" import axios from "axios" +import jwt_decode from "jwt-decode"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; import attemptLogin from "@app/components/utilities/attemptLogin"; import { Button, Input } from "@app/components/v2"; +import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchUserDetails } from "@app/hooks/api/users/queries"; type Props = { providerAuthToken: string; - callbackPort?: string; email: string; password: string; setPassword: (password: string) => void; @@ -21,16 +23,22 @@ type Props = { export const PasswordStep = ({ providerAuthToken, - callbackPort, email, password, setPassword, - setStep + setStep, }: Props) => { const { createNotification } = useNotificationContext(); const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const router = useRouter(); + const { mutateAsync } = useUpdateUserAuthMethods(); + + const { + callbackPort, + isLinkingRequired, + authMethod + } = jwt_decode(providerAuthToken) as any; const handleLogin = async () => { try { @@ -90,6 +98,15 @@ export const PasswordStep = ({ text: "Successfully logged in", type: "success" }); + + if (isLinkingRequired) { + const user = await fetchUserDetails(); + const newAuthMethods = [...user.authMethods, authMethod] + await mutateAsync({ + authMethods: newAuthMethods + }); + } + router.push(`/org/${userOrg}/overview`); } } @@ -108,9 +125,18 @@ export const PasswordStep = ({ onSubmit={(e) => e.preventDefault()} className="h-full mx-auto w-full max-w-md px-6 pt-8" > -

- What’s your Infisical Password? -

+
+

+ {isLinkingRequired ? "Link your account" : "What's your Infisical password?"} +

+ {isLinkingRequired && ( +
+ + An existing account without this SSO authentication method enabled was found under the same email. Login with your password to link the account. + +
+ )} +
Date: Sun, 13 Aug 2023 18:26:41 -0700 Subject: [PATCH 48/64] added a check for signup events --- backend/src/controllers/v3/secretsController.ts | 6 ++++-- backend/src/helpers/secrets.ts | 5 +++-- backend/src/interfaces/services/SecretService/index.ts | 1 + frontend/src/helpers/project.ts | 3 ++- frontend/src/hooks/api/secrets/types.ts | 1 + 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 4b6c1adab..bbfc74a52 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -362,7 +362,8 @@ export const createSecret = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, - secretPath = "/" + secretPath = "/", + source } = req.body; const secret = await SecretService.createSecret({ @@ -380,7 +381,8 @@ export const createSecret = async (req: Request, res: Response) => { secretPath, secretCommentCiphertext, secretCommentIV, - secretCommentTag + secretCommentTag, + source }); await EventService.handleEvent({ diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 880e7cee3..a37da47bb 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -326,7 +326,8 @@ export const createSecretHelper = async ({ secretCommentCiphertext, secretCommentIV, secretCommentTag, - secretPath = "/" + secretPath = "/", + source }: CreateSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, @@ -463,7 +464,7 @@ export const createSecretHelper = async ({ const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { + if (postHogClient && source !== "signup") { postHogClient.capture({ event: "secrets added", distinctId: await TelemetryService.getDistinctId({ diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index 678c67711..3fd19fd7a 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -17,6 +17,7 @@ export interface CreateSecretParams { secretCommentIV?: string; secretCommentTag?: string; secretPath: string; + source?: string; } export interface GetSecretsParams { diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index f0cb8ec4c..938d571de 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -140,7 +140,8 @@ const initProjectHelper = async ({ secretCommentCiphertext: secret.secretCommentCiphertext, secretCommentIV: secret.secretCommentIV, secretCommentTag: secret.secretCommentTag, - secretPath: "/" + secretPath: "/", + source: "signup" }); }); diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 964bac6c2..a9ad18f65 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -165,4 +165,5 @@ export type CreateSecretDTO = { secretCommentIV: string; secretCommentTag: string; secretPath: string; + source?: string; } \ No newline at end of file From baa907dbb690f8f3ac9a3837e2b99dfcc7a084f5 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 14 Aug 2023 11:09:22 +0700 Subject: [PATCH 49/64] Update source to metadata.source --- backend/src/controllers/v3/secretsController.ts | 4 ++-- backend/src/helpers/secrets.ts | 6 +++--- backend/src/interfaces/services/SecretService/index.ts | 4 +++- backend/src/routes/v3/secrets.ts | 2 ++ frontend/src/helpers/project.ts | 4 +++- frontend/src/hooks/api/secrets/types.ts | 4 +++- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index bbfc74a52..cd00f0429 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -363,7 +363,7 @@ export const createSecret = async (req: Request, res: Response) => { secretCommentIV, secretCommentTag, secretPath = "/", - source + metadata } = req.body; const secret = await SecretService.createSecret({ @@ -382,7 +382,7 @@ export const createSecret = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, - source + metadata }); await EventService.handleEvent({ diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index a37da47bb..31341d647 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -327,7 +327,7 @@ export const createSecretHelper = async ({ secretCommentIV, secretCommentTag, secretPath = "/", - source + metadata }: CreateSecretParams) => { const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, @@ -463,8 +463,8 @@ export const createSecretHelper = async ({ }); const postHogClient = await TelemetryService.getPostHogClient(); - - if (postHogClient && source !== "signup") { + + if (postHogClient && (metadata?.source !== "signup")) { postHogClient.capture({ event: "secrets added", distinctId: await TelemetryService.getDistinctId({ diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index 3fd19fd7a..fb3e4442c 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -17,7 +17,9 @@ export interface CreateSecretParams { secretCommentIV?: string; secretCommentTag?: string; secretPath: string; - source?: string; + metadata?: { + source?: string; + } } export interface GetSecretsParams { diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 224123445..8f8fac9c4 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -180,6 +180,8 @@ router.post( body("secretCommentIV").optional().isString().trim(), body("secretCommentTag").optional().isString().trim(), body("secretPath").default("/").isString().trim(), + body("metadata").optional().isObject().withMessage("Metadata should be an object"), + body("metadata.source").optional().isString().withMessage("Source should be a string"), validateRequest, requireAuth({ acceptedAuthModes: [ diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 938d571de..6a1efc135 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -141,7 +141,9 @@ const initProjectHelper = async ({ secretCommentIV: secret.secretCommentIV, secretCommentTag: secret.secretCommentTag, secretPath: "/", - source: "signup" + metadata: { + source: "signup" + } }); }); diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index a9ad18f65..2bf304f4e 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -165,5 +165,7 @@ export type CreateSecretDTO = { secretCommentIV: string; secretCommentTag: string; secretPath: string; - source?: string; + metadata?: { + source?: string; + } } \ No newline at end of file From 11bb0d648fe820326c9573712a0b619a3c2d1a34 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Mon, 14 Aug 2023 18:36:44 -0700 Subject: [PATCH 50/64] fixed capitalization --- frontend/src/pages/cli-redirect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx index ab308ef6d..977222cd7 100644 --- a/frontend/src/pages/cli-redirect.tsx +++ b/frontend/src/pages/cli-redirect.tsx @@ -5,7 +5,7 @@ export default function CliRedirect() { return (
- Infisical Cli | Login Successful! + Infisical CLI | Login Successful!
From b000a78f74e1e24c18497e0c5a587fb864d9adc7 Mon Sep 17 00:00:00 2001 From: Daniel Inge Date: Tue, 15 Aug 2023 18:39:15 -0400 Subject: [PATCH 51/64] Change tag color assignments and sorting --- .../SecretInputRow/SecretInputRow.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index a7ccec3b4..37b9edabb 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -110,6 +110,11 @@ export const SecretInputRow = memo( append } = useFieldArray({ control, name: `secrets.${index}.tags` }); + const colorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) + + // display the tags in alphabetical order + secretTags.sort((a, b) => a.name.localeCompare(b.name)) + // to get details on a secret const overrideAction = useWatch({ control, @@ -321,19 +326,22 @@ export const SecretInputRow = memo(
- {secretTags.map(({ id, slug }, i) => ( - remove(i)} - key={id} - > - {slug} - - ))} + {secretTags.map(({ id, _id, slug }, i) => { + // This map lookup shouldn't ever fail, but if it does we default to the first color + const tagColor = colorByTagId.get(_id) || tagColors[0] + return ( + remove(i)} + key={id} + > + {slug} + ) + })}
{}} + onCheckedChange={() => { }} > - {} + { } } key={wsTag._id} From 3436e6be0ebe37708b3e0e8baabf55834ce4e468 Mon Sep 17 00:00:00 2001 From: Daniel Inge Date: Tue, 15 Aug 2023 18:46:17 -0400 Subject: [PATCH 52/64] Small formatting changes --- .../components/SecretInputRow/SecretInputRow.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 37b9edabb..65e5436b7 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -110,7 +110,7 @@ export const SecretInputRow = memo( append } = useFieldArray({ control, name: `secrets.${index}.tags` }); - const colorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) + const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) // display the tags in alphabetical order secretTags.sort((a, b) => a.name.localeCompare(b.name)) @@ -328,7 +328,7 @@ export const SecretInputRow = memo(
{secretTags.map(({ id, _id, slug }, i) => { // This map lookup shouldn't ever fail, but if it does we default to the first color - const tagColor = colorByTagId.get(_id) || tagColors[0] + const tagColor = tagColorByTagId.get(_id) || tagColors[0] return ( { }} + onCheckedChange={() => {}} > - { } + {} } key={wsTag._id} From 5ac18163922f3a7a705a3636a84dad128d03fd67 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 17 Aug 2023 01:24:41 +0700 Subject: [PATCH 53/64] Correct SSO linking case and uncomment audit logs v2 --- backend/src/controllers/v3/authController.ts | 6 ++---- backend/src/helpers/auth.ts | 7 +------ frontend/src/layouts/AppLayout/AppLayout.tsx | 10 +++++----- frontend/src/pages/project/[id]/audit-logs/index.tsx | 2 +- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index ebbcb61b2..413bcefa0 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -47,17 +47,16 @@ export const login1 = async (req: Request, res: Response) => { clientPublicKey: string, providerAuthToken?: string; } = req.body; - + const user = await User.findOne({ email, }).select("+salt +verifier"); if (!user) throw new Error("Failed to find user"); - + if (!user.authMethods.includes(AuthMethod.EMAIL)) { await validateProviderAuthToken({ email, - user, providerAuthToken, }); } @@ -109,7 +108,6 @@ export const login2 = async (req: Request, res: Response) => { if (!user.authMethods.includes(AuthMethod.EMAIL)) { await validateProviderAuthToken({ email, - user, providerAuthToken, }) } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index f24c54859..74094cba6 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -392,11 +392,9 @@ export const createToken = ({ export const validateProviderAuthToken = async ({ email, - user, providerAuthToken, }: { email: string; - user: IUser, providerAuthToken?: string; }) => { if (!providerAuthToken) { @@ -407,10 +405,7 @@ export const validateProviderAuthToken = async ({ jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) ); - if ( - !user.authMethods.includes(decodedToken.authMethod) || - decodedToken.email !== email - ) { + if (decodedToken.email !== email) { throw new Error("Invalid authentication credentials.") } } diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index bb08e849d..1391fe298 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -483,7 +483,7 @@ export const AppLayout = ({ children }: LayoutProps) => { - {/* + { } icon="system-outline-168-view-headline" > - Audit Logs V2 + Audit Logs - */} - + + {/* { Audit Logs - + */} {/* { return (
- {t("common.head-title", { title: t("billing.title") })} + {t("common.head-title", { title: t("settings.project.title") })} From 767abe51ef8b6ca1de9287d6edf272fa06ecf675 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 17 Aug 2023 01:34:24 +0700 Subject: [PATCH 54/64] Fix lint errors --- backend/src/controllers/v3/signupController.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index f17384189..449df4e11 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -71,8 +71,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { if (providerAuthToken) { await validateProviderAuthToken({ email, - providerAuthToken, - user, + providerAuthToken }); } else { const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] @@ -117,16 +116,8 @@ export const completeAccountSignup = async (req: Request, res: Response) => { if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null - const hasSamlEnabled = user.authMethods - .some( - (authMethod: AuthMethod) => - [ - AuthMethod.OKTA_SAML, - AuthMethod.AZURE_SAML, - AuthMethod.JUMPCLOUD_SAML - ].includes(authMethod) - ); - + const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)); + if (!hasSamlEnabled) { // TODO: modify this part // initialize default organization and workspace await initializeDefaultOrg({ From d7b26cbf049a846771a4e527e809e76b3481a1c0 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 17 Aug 2023 01:51:44 +0700 Subject: [PATCH 55/64] Fix Select placeholder in audit logs v2 --- .../src/views/Project/AuditLogsPage/components/LogsFilter.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx index d97eefc81..43bb1e38f 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx @@ -74,8 +74,7 @@ export const LogsFilter = ({ className="w-40 mr-4" >