diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index b022ac5de..928d5b50b 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models"; -import { EventType } from "../../ee/models"; +import { EventType, Role } from "../../ee/models"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { sendMail } from "../../helpers/nodemailer"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; @@ -15,7 +15,6 @@ import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; -import Role from "../../ee/models/role"; import { BadRequestError } from "../../utils/errors"; import { InviteUserToWorkspaceV1 } from "../../validation/workspace"; diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index 757d1aed7..2d216b6c5 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -6,13 +6,11 @@ import { Organization, Workspace } from "../../models"; -import { createOrganization as create } from "../../helpers/organization"; -import { addMembershipsOrg } from "../../helpers/membershipOrg"; -import { ACCEPTED, ADMIN } from "../../variables"; import { getLicenseServerUrl, getSiteURL } from "../../config"; import { licenseServerKeyRequest } from "../../config/request"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/organization"; +import { ACCEPTED } from "../../variables"; import { OrgPermissionActions, OrgPermissionSubjects, @@ -34,36 +32,6 @@ export const getOrganizations = async (req: Request, res: Response) => { }); }; -/** - * Create new organization named [organizationName] - * and add user as owner - * @param req - * @param res - * @returns - */ -export const createOrganization = async (req: Request, res: Response) => { - const { - body: { organizationName } - } = await validateRequest(reqValidator.CreateOrgv1, req); - - // create organization and add user as member - const organization = await create({ - email: req.user.email, - name: organizationName - }); - - await addMembershipsOrg({ - userIds: [req.user._id.toString()], - organizationId: organization._id.toString(), - roles: [ADMIN], - statuses: [ACCEPTED] - }); - - return res.status(200).send({ - organization - }); -}; - /** * Return organization with id [organizationId] * @param req diff --git a/backend/src/controllers/v1/secretScanningController.ts b/backend/src/controllers/v1/secretScanningController.ts index ff1e5f3f0..581a168b1 100644 --- a/backend/src/controllers/v1/secretScanningController.ts +++ b/backend/src/controllers/v1/secretScanningController.ts @@ -1,12 +1,15 @@ import { Request, Response } from "express"; -import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession"; +import { + GitAppInstallationSession, + GitAppOrganizationInstallation, + GitRisks +} from "../../ee/models"; import crypto from "crypto"; import { Types } from "mongoose"; import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors"; -import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation"; import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; -import GitRisks, { +import { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index cbefc98a7..1b3a9fd3f 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -202,12 +202,12 @@ export const deleteWorkspace = async (req: Request, res: Response) => { ); // delete workspace - await deleteWork({ - id: workspaceId + const workspace = await deleteWork({ + workspaceId: new Types.ObjectId(workspaceId) }); return res.status(200).send({ - message: "Successfully deleted workspace" + workspace }); }; diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index ad776864c..384765f68 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,11 +1,25 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models"; +import { + Membership, + MembershipOrg, + ServiceAccount, + Workspace +} from "../../models"; +import { Role } from "../../ee/models"; import { deleteMembershipOrg } from "../../helpers/membershipOrg"; -import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; -import Role from "../../ee/models/role"; -import { BadRequestError } from "../../utils/errors"; -import { CUSTOM } from "../../variables"; +import { + createOrganization as create, + deleteOrganization, + updateSubscriptionOrgQuantity +} from "../../helpers/organization"; +import { addMembershipsOrg } from "../../helpers/membershipOrg"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { + ACCEPTED, + ADMIN, + CUSTOM +} from "../../variables"; import * as reqValidator from "../../validation/organization"; import { validateRequest } from "../../helpers/validation"; import { @@ -332,3 +346,60 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response serviceAccounts }); }; + +/** + * Create new organization named [organizationName] + * and add user as owner + * @param req + * @param res + * @returns + */ + export const createOrganization = async (req: Request, res: Response) => { + const { + body: { name } + } = await validateRequest(reqValidator.CreateOrgv2, req); + + // create organization and add user as member + const organization = await create({ + email: req.user.email, + name + }); + + await addMembershipsOrg({ + userIds: [req.user._id.toString()], + organizationId: organization._id.toString(), + roles: [ADMIN], + statuses: [ACCEPTED] + }); + + return res.status(200).send({ + organization + }); +}; + +/** + * Delete organization with id [organizationId] + * @param req + * @param res + */ +export const deleteOrganizationById = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.DeleteOrgv2, req); + + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: new Types.ObjectId(organizationId), + role: ADMIN + }); + + if (!membershipOrg) throw UnauthorizedRequestError(); + + const organization = await deleteOrganization({ + organizationId: new Types.ObjectId(organizationId) + }); + + return res.status(200).send({ + organization + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 0b859f749..3998956f7 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -5,49 +5,9 @@ import bcrypt from "bcrypt"; import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models"; import { getSaltRounds } from "../../config"; import { validateRequest } from "../../helpers/validation"; +import { deleteUser } from "../../helpers/user"; import * as reqValidator from "../../validation"; -/** - * Return the current user. - * @param req - * @param res - * @returns - */ -export const getMe = async (req: Request, res: Response) => { - /* - #swagger.summary = "Retrieve the current user on the request" - #swagger.description = "Retrieve the current user on the request" - - #swagger.security = [{ - "apiKeyAuth": [] - }] - - #swagger.responses[200] = { - content: { - "application/json": { - "schema": { - "type": "object", - "properties": { - "user": { - "type": "object", - $ref: "#/components/schemas/CurrentUser", - "description": "Current user on request" - } - } - } - } - } - } - */ - const user = await User.findById(req.user._id).select( - "+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag" - ); - - return res.status(200).send({ - user - }); -}; - /** * Update the current user's MFA-enabled status [isMfaEnabled]. * Note: Infisical currently only supports email-based 2FA only; this will expand to @@ -296,3 +256,59 @@ export const deleteMySessions = async (req: Request, res: Response) => { message: "Successfully revoked all sessions" }); }; + +/** + * Return the current user. + * @param req + * @param res + * @returns + */ + export const getMe = async (req: Request, res: Response) => { + /* + #swagger.summary = "Retrieve the current user on the request" + #swagger.description = "Retrieve the current user on the request" + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + $ref: "#/components/schemas/CurrentUser", + "description": "Current user on request" + } + } + } + } + } + } + */ + const user = await User.findById(req.user._id).select( + "+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag" + ); + + return res.status(200).send({ + user + }); +}; + +/** + * Delete the current user. + * @param req + * @param res + */ +export const deleteMe = async (req: Request, res: Response) => { + const user = await deleteUser({ + userId: req.user._id + }); + + return res.status(200).send({ + user + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts index 0d5f105ea..d80ef2817 100644 --- a/backend/src/ee/controllers/v1/roleController.ts +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -23,7 +23,7 @@ import { memberPermissions } from "../../services/RoleService"; import { BadRequestError } from "../../../utils/errors"; -import Role from "../../models/role"; +import { Role } from "../../models"; import { validateRequest } from "../../../helpers/validation"; import { packRules } from "@casl/ability/extra"; @@ -212,6 +212,7 @@ export const getUserPermissions = async (req: Request, res: Response) => { const { params: { orgId } } = await validateRequest(GetUserPermission, req); + const { permission } = await getUserOrgPermissions(req.user._id, orgId); res.status(200).json({ diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index 3f57602f9..6b6507cd0 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -298,6 +298,10 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { message: "Failed to delete service token" }); + await ServiceTokenDataV3Key.findOneAndDelete({ + serviceTokenData: serviceTokenData._id + }); + await EEAuditLogService.createAuditLog( req.authData, { diff --git a/backend/src/ee/models/gitAppInstallationSession.ts b/backend/src/ee/models/gitAppInstallationSession.ts index a78629186..0cdf8df3c 100644 --- a/backend/src/ee/models/gitAppInstallationSession.ts +++ b/backend/src/ee/models/gitAppInstallationSession.ts @@ -29,6 +29,4 @@ const gitAppInstallationSession = new Schema({ }); -const GitAppInstallationSession = model("git_app_installation_session", gitAppInstallationSession); - -export default GitAppInstallationSession; +export const GitAppInstallationSession = model("git_app_installation_session", gitAppInstallationSession); \ No newline at end of file diff --git a/backend/src/ee/models/gitAppOrganizationInstallation.ts b/backend/src/ee/models/gitAppOrganizationInstallation.ts index e742c0cf1..4ce55b0cb 100644 --- a/backend/src/ee/models/gitAppOrganizationInstallation.ts +++ b/backend/src/ee/models/gitAppOrganizationInstallation.ts @@ -26,6 +26,4 @@ const gitAppOrganizationInstallation = new Schema({ }); -const GitAppOrganizationInstallation = model("git_app_organization_installation", gitAppOrganizationInstallation); - -export default GitAppOrganizationInstallation; +export const GitAppOrganizationInstallation = model("git_app_organization_installation", gitAppOrganizationInstallation); \ No newline at end of file diff --git a/backend/src/ee/models/gitRisks.ts b/backend/src/ee/models/gitRisks.ts index 0fd1ffd38..8d3f59208 100644 --- a/backend/src/ee/models/gitRisks.ts +++ b/backend/src/ee/models/gitRisks.ts @@ -5,7 +5,7 @@ export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED"; export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED"; export const STATUS_UNRESOLVED = "UNRESOLVED"; -export type GitRisks = { +export type IGitRisks = { id: string; description: string; startLine: string; @@ -42,7 +42,7 @@ export type GitRisks = { organization: Schema.Types.ObjectId, } -const gitRisks = new Schema({ +const gitRisks = new Schema({ id: { type: String, }, @@ -147,6 +147,4 @@ const gitRisks = new Schema({ } }, { timestamps: true }); -const GitRisks = model("GitRisks", gitRisks); - -export default GitRisks; +export const GitRisks = model("GitRisks", gitRisks); \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts index 6e9ec34a9..74abad3e4 100644 --- a/backend/src/ee/models/index.ts +++ b/backend/src/ee/models/index.ts @@ -2,6 +2,7 @@ export * from "./secretSnapshot"; export * from "./secretVersion"; export * from "./folderVersion"; export * from "./log"; +export * from "./role"; export * from "./action"; export * from "./ssoConfig"; export * from "./trustedIp"; @@ -9,3 +10,5 @@ export * from "./auditLog"; export * from "./gitRisks"; export * from "./gitAppOrganizationInstallation"; export * from "./gitAppInstallationSession"; +export * from "./secretApprovalPolicy"; +export * from "./secretApprovalRequest"; diff --git a/backend/src/ee/models/role.ts b/backend/src/ee/models/role.ts index 643acedcd..d3de1d3ae 100644 --- a/backend/src/ee/models/role.ts +++ b/backend/src/ee/models/role.ts @@ -50,6 +50,4 @@ const roleSchema = new Schema( roleSchema.index({ organization: 1, workspace: 1 }); -const Role = model("Role", roleSchema); - -export default Role; +export const Role = model("Role", roleSchema); \ No newline at end of file diff --git a/backend/src/ee/services/GithubSecretScanning/GithubSecretScanningService.ts b/backend/src/ee/services/GithubSecretScanning/GithubSecretScanningService.ts index e9226b762..e0a45215c 100644 --- a/backend/src/ee/services/GithubSecretScanning/GithubSecretScanningService.ts +++ b/backend/src/ee/services/GithubSecretScanning/GithubSecretScanningService.ts @@ -1,6 +1,8 @@ import { Probot } from "probot"; -import GitRisks from "../../models/gitRisks"; -import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation"; +import { + GitAppOrganizationInstallation, + GitRisks +} from "../../models"; import { scanGithubPushEventForSecretLeaks } from "../../../queues/secret-scanning/githubScanPushEvent"; export default async (app: Probot) => { app.on("installation.deleted", async (context) => { diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 1ba5c7abc..3e8598de3 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,5 +1,43 @@ -import { Types } from "mongoose"; -import { MembershipOrg, Organization } from "../models"; +import mongoose, { Types } from "mongoose"; +import { + Bot, + BotKey, + BotOrg, + Folder, + IncidentContactOrg, + Integration, + IntegrationAuth, + Key, + Membership, + MembershipOrg, + Organization, + Secret, + SecretBlindIndexData, + SecretImport, + ServiceToken, + ServiceTokenData, + ServiceTokenDataV3, + ServiceTokenDataV3Key, + Tag, + Webhook, + Workspace +} from "../models"; +import { + Action, + AuditLog, + FolderVersion, + GitAppInstallationSession, + GitAppOrganizationInstallation, + GitRisks, + Log, + Role, + SSOConfig, + SecretApprovalPolicy, + SecretApprovalRequest, + SecretSnapshot, + SecretVersion, + TrustedIP +} from "../ee/models"; import { ACCEPTED, } from "../variables"; @@ -17,6 +55,7 @@ import { import { createBotOrg } from "./botOrg"; +import { InternalServerError, ResourceNotFoundError } from "../utils/errors"; /** * Create an organization with name [name] @@ -65,6 +104,227 @@ export const createOrganization = async ({ return organization; }; +/** + * Delete organization with id [organizationId] + * @param {Object} obj + * @param {Types.ObjectId} obj.organizationId - id of organization to delete + * @returns + */ +export const deleteOrganization = async ({ + organizationId +}: { + organizationId: Types.ObjectId; +}) => { + const session = await mongoose.startSession(); + session.startTransaction(); + + try { + const organization = await Organization.findByIdAndDelete(organizationId); + + if (!organization) throw ResourceNotFoundError(); + + await MembershipOrg.deleteMany({ + organization: organization._id + }); + + await BotOrg.deleteMany({ + organization: organization._id + }); + + await SSOConfig.deleteMany({ + organization: organization._id + }); + + await Role.deleteMany({ + organization: organization._id + }); + + await IncidentContactOrg.deleteMany({ + organization: organization._id + }); + + await GitRisks.deleteMany({ + organization: organization._id + }); + + await GitAppInstallationSession.deleteMany({ + organization: organization._id + }); + + await GitAppOrganizationInstallation.deleteMany({ + organization: organization._id + }); + + const workspaceIds = await Workspace.distinct("_id", { + organization: organization._id + }); + + await Workspace.deleteMany({ + organization: organization._id + }); + + await Membership.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Key.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Bot.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await BotKey.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretBlindIndexData.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Secret.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretVersion.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretSnapshot.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + + await SecretImport.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Folder.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await FolderVersion.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Webhook.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await TrustedIP.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Tag.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await IntegrationAuth.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Integration.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceToken.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceTokenData.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceTokenDataV3.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await ServiceTokenDataV3Key.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await AuditLog.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Log.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await Action.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretApprovalPolicy.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + await SecretApprovalRequest.deleteMany({ + workspace: { + $in: workspaceIds + } + }); + + return organization; + } catch (err) { + await session.abortTransaction(); + throw InternalServerError({ + message: "Failed to delete organization" + }); + } finally { + session.endSession(); + } +} + /** * Update organization subscription quantity to reflect number of members in * the organization. diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 728c7d089..263c5c736 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,5 +1,27 @@ -import { IUser, User } from "../models"; +import mongoose, { Types } from "mongoose"; +import { + APIKeyData, + BackupPrivateKey, + IUser, + Key, + Membership, + MembershipOrg, + TokenVersion, + User, + UserAction +} from "../models"; +import { + Action, + Log +} from "../ee/models"; import { sendMail } from "./nodemailer"; +import { + InternalServerError, + ResourceNotFoundError +} from "../utils/errors"; +import { ADMIN } from "../variables"; +import { deleteOrganization } from "../helpers/organization"; +import { deleteWorkspace } from "../helpers/workspace"; /** * Initialize a user under email [email] @@ -134,3 +156,169 @@ export const checkUserDevice = async ({ }); } }; + +/** + * Check that if we delete user with id [userId] then + * there won't be any admin-less organizations or projects + * @param {Object} obj + * @param {String} obj.userId - id of user to check deletion conditions for + */ +const checkDeleteUserConditions = async ({ + userId +}: { + userId: Types.ObjectId; +}) => { + const memberships = await Membership.find({ + user: userId + }); + + const membershipOrgs = await MembershipOrg.find({ + user: userId + }); + + // delete organizations where user is only member + for await (const membershipOrg of membershipOrgs) { + const orgMemberCount = await MembershipOrg.countDocuments({ + organization: membershipOrg.organization, + }); + + const otherOrgAdminCount = await MembershipOrg.countDocuments({ + organization: membershipOrg.organization, + user: { $ne: userId }, + role: ADMIN + }); + + if (orgMemberCount > 1 && otherOrgAdminCount === 0) { + throw InternalServerError({ + message: "Failed to delete account because an org would be admin-less" + }); + } + } + + // delete workspaces where user is only member + for await (const membership of memberships) { + const workspaceMemberCount = await Membership.countDocuments({ + workspace: membership.workspace + }); + + const otherWorkspaceAdminCount = await Membership.countDocuments({ + workspace: membership.workspace, + user: { $ne: userId }, + role: ADMIN + }); + + if (workspaceMemberCount > 1 && otherWorkspaceAdminCount === 0) { + throw InternalServerError({ + message: "Failed to delete account because a workspace would be admin-less" + }); + } + } +} + +/** + * Delete account with id [userId] + * @param {Object} obj + * @param {Types.ObjectId} obj.userId - id of user to delete + * @returns {User} user - deleted user + */ +export const deleteUser = async ({ + userId +}: { + userId: Types.ObjectId +}) => { + const session = await mongoose.startSession(); + session.startTransaction(); + + try { + const user = await User.findByIdAndDelete(userId); + + if (!user) throw ResourceNotFoundError(); + + await checkDeleteUserConditions({ + userId: user._id + }); + + await UserAction.deleteMany({ + user: user._id + }); + + await BackupPrivateKey.deleteMany({ + user: user._id + }); + + await APIKeyData.deleteMany({ + user: user._id + }); + + await Action.deleteMany({ + user: user._id + }); + + await Log.deleteMany({ + user: user._id + }); + + await TokenVersion.deleteMany({ + user: user._id + }); + + await Key.deleteMany({ + receiver: user._id + }); + + const membershipOrgs = await MembershipOrg.find({ + user: userId + }); + + // delete organizations where user is only member + for await (const membershipOrg of membershipOrgs) { + const memberCount = await MembershipOrg.countDocuments({ + organization: membershipOrg.organization + }); + + if (memberCount === 1) { + // organization only has 1 member (the current user) + + await deleteOrganization({ + organizationId: membershipOrg.organization + }); + } + } + + const memberships = await Membership.find({ + user: userId + }); + + // delete workspaces where user is only member + for await (const membership of memberships) { + const memberCount = await Membership.countDocuments({ + workspace: membership.workspace + }); + + if (memberCount === 1) { + // workspace only has 1 member (the current user) -> delete workspace + + await deleteWorkspace({ + workspaceId: membership.workspace + }); + } + } + + await MembershipOrg.deleteMany({ + user: userId + }); + + await Membership.deleteMany({ + user: userId + }); + + return user; + } catch (err) { + await session.abortTransaction(); + throw InternalServerError({ + message: "Failed to delete account" + }) + } finally { + session.endSession(); + } +} \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index b65a4543b..73f83b061 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,18 +1,42 @@ -import { Types } from "mongoose"; +import mongoose, { Types } from "mongoose"; import { Bot, + BotKey, + Folder, + Integration, + IntegrationAuth, Key, Membership, Secret, - Workspace, + SecretBlindIndexData, + SecretImport, + ServiceToken, + ServiceTokenData, + ServiceTokenDataV3, + ServiceTokenDataV3Key, + Tag, + Webhook, + Workspace } from "../models"; import { + Action, + AuditLog, + FolderVersion, IPType, + Log, + SecretApprovalPolicy, + SecretApprovalRequest, + SecretSnapshot, + SecretVersion, TrustedIP } from "../ee/models"; import { createBot } from "../helpers/bot"; import { EELicenseService } from "../ee/services"; import { SecretService } from "../services"; +import { + InternalServerError, + ResourceNotFoundError +} from "../utils/errors"; /** * Create a workspace with name [name] in organization with id [organizationId] @@ -77,18 +101,126 @@ export const createWorkspace = async ({ * @param {Object} obj * @param {String} obj.id - id of workspace to delete */ -export const deleteWorkspace = async ({ id }: { id: string }) => { - await Workspace.deleteOne({ _id: id }); - await Bot.deleteOne({ - workspace: id, - }); - await Membership.deleteMany({ - workspace: id, - }); - await Secret.deleteMany({ - workspace: id, - }); - await Key.deleteMany({ - workspace: id, - }); +export const deleteWorkspace = async ({ + workspaceId +}: { + workspaceId: Types.ObjectId; +}) => { + const session = await mongoose.startSession(); + session.startTransaction(); + + try { + const workspace = await Workspace.findByIdAndDelete(workspaceId); + + if (!workspace) throw ResourceNotFoundError(); + + await Membership.deleteMany({ + workspace: workspace._id + }); + + await Key.deleteMany({ + workspace: workspace._id + }); + + await Bot.deleteMany({ + workspace: workspace._id + }); + + await BotKey.deleteMany({ + workspace: workspace._id + }); + + await SecretBlindIndexData.deleteMany({ + workspace: workspace._id + }); + + await Secret.deleteMany({ + workspace: workspace._id + }); + + await SecretVersion.deleteMany({ + workspace: workspace._id + }); + + await SecretSnapshot.deleteMany({ + workspace: workspace._id + }); + + await SecretImport.deleteMany({ + workspace: workspace._id + }); + + await Folder.deleteMany({ + workspace: workspace._id + }); + + await FolderVersion.deleteMany({ + workspace: workspace._id + }); + + await Webhook.deleteMany({ + workspace: workspace._id + }); + + await TrustedIP.deleteMany({ + workspace: workspace._id + }); + + await Tag.deleteMany({ + workspace: workspace._id + }); + + await IntegrationAuth.deleteMany({ + workspace: workspace._id + }); + + await Integration.deleteMany({ + workspace: workspace._id + }); + + await ServiceToken.deleteMany({ + workspace: workspace._id + }); + + await ServiceTokenData.deleteMany({ + workspace: workspace._id + }); + + await ServiceTokenDataV3.deleteMany({ + workspace: workspace._id + }); + + await ServiceTokenDataV3Key.deleteMany({ + workspace: workspace._id + }); + + await AuditLog.deleteMany({ + workspace: workspace._id + }); + + await Log.deleteMany({ + workspace: workspace._id + }); + + await Action.deleteMany({ + workspace: workspace._id + }); + + await SecretApprovalPolicy.deleteMany({ + workspace: workspace._id + }); + + await SecretApprovalRequest.deleteMany({ + workspace: workspace._id + }); + + return workspace; + } catch (err) { + await session.abortTransaction(); + throw InternalServerError({ + message: "Failed to delete organization" + }); + } finally { + session.endSession(); + } }; diff --git a/backend/src/queues/secret-scanning/githubScanFullRepository.ts b/backend/src/queues/secret-scanning/githubScanFullRepository.ts index c66bf2a2f..ece43bd97 100644 --- a/backend/src/queues/secret-scanning/githubScanFullRepository.ts +++ b/backend/src/queues/secret-scanning/githubScanFullRepository.ts @@ -2,7 +2,7 @@ import Queue, { Job } from "bull"; import { ProbotOctokit } from "probot" import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers"; -import GitRisks from "../../ee/models/gitRisks"; +import { GitRisks } from "../../ee/models"; import { MembershipOrg, User } from "../../models"; import { ADMIN } from "../../variables"; import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; diff --git a/backend/src/queues/secret-scanning/githubScanPushEvent.ts b/backend/src/queues/secret-scanning/githubScanPushEvent.ts index 09261e235..af2d88f1f 100644 --- a/backend/src/queues/secret-scanning/githubScanPushEvent.ts +++ b/backend/src/queues/secret-scanning/githubScanPushEvent.ts @@ -3,7 +3,7 @@ import { ProbotOctokit } from "probot" import { Commit } from "@octokit/webhooks-types"; import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers"; -import GitRisks from "../../ee/models/gitRisks"; +import { GitRisks } from "../../ee/models"; import { MembershipOrg, User } from "../../models"; import { ADMIN } from "../../variables"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 011d5fed6..3dfe2689b 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -13,15 +13,6 @@ router.get( organizationController.getOrganizations ); -router.post( - // not used on frontend - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - organizationController.createOrganization -); - router.get( "/:organizationId", requireAuth({ diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index d1387bd42..537449273 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -54,4 +54,20 @@ router.get( organizationsController.getOrganizationServiceAccounts ); +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.createOrganization +); + +router.delete( + "/:organizationId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.deleteOrganizationById +); + export default router; diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index 41d4c8e6b..cc7a7ec0d 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -4,14 +4,6 @@ import { requireAuth } from "../../middleware"; import { usersController } from "../../controllers/v2"; import { AuthMode } from "../../variables"; -router.get( - "/me", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] - }), - usersController.getMe -); - router.patch( "/me/mfa", requireAuth({ @@ -84,4 +76,20 @@ router.delete( usersController.deleteMySessions ); +router.get( + "/me", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.getMe +); + +router.delete( + "/me", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.deleteMe +); + export default router; diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index cc481b011..3339d8927 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -4,7 +4,14 @@ import { Types } from "mongoose"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { EESecretService } from "../../ee/services"; import { redisClient } from "../../services/RedisService" -import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models"; +import { + IPType, + ISecretVersion, + Role, + SecretSnapshot, + SecretVersion, + TrustedIP +} from "../../ee/models"; import { AuthMethod, BackupPrivateKey, @@ -34,14 +41,12 @@ import { MEMBER, OWNER } from "../../variables"; - import { InternalServerError } from "../errors"; import { ProjectPermissionActions, ProjectPermissionSub, memberProjectPermissions } from "../../ee/services/ProjectRoleService"; -import Role from "../../ee/models/role"; /** * Backfill secrets to ensure that they're all versioned and have diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index ce93db9b0..bddf76cb5 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -136,12 +136,6 @@ export const GetOrgLicencesv1 = z.object({ params: z.object({ organizationId: z.string().trim() }) }); -export const CreateOrgv1 = z.object({ - body: z.object({ - organizationName: z.string().trim() - }) -}); - export const GetOrgv1 = z.object({ params: z.object({ organizationId: z.string().trim() @@ -209,3 +203,13 @@ export const VerfiyUserToOrganizationV1 = z.object({ code: z.string().trim() }) }); + +export const CreateOrgv2 = z.object({ + body: z.object({ + name: z.string().trim() + }) +}); + +export const DeleteOrgv2 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); \ No newline at end of file diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index b2eeeceea..b8812918a 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -2,8 +2,6 @@ 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 KeyService from "@app/services/KeyService"; import Telemetry from "./telemetry/Telemetry"; @@ -122,17 +120,7 @@ const attemptLogin = async ( iv, tag, privateKey - }); - - const userOrgs = await fetchOrganizations(); - const orgId = userOrgs[0]._id; - localStorage.setItem("orgData.id", orgId); - - const orgUserProjects = await fetchMyOrganizationProjects(orgId); - - if (orgUserProjects.length > 0) { - localStorage.setItem("projectData.id", orgUserProjects[0]._id); - } + }); if (email) { telemetry.identify(email, email); diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts index 6681e8464..3a99e6e8e 100644 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ b/frontend/src/components/utilities/attemptCliLoginMfa.ts @@ -2,9 +2,6 @@ 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 verifyMfaToken from "@app/pages/api/auth/verifyMfaToken"; import KeyService from "@app/services/KeyService"; import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; @@ -19,7 +16,6 @@ interface IsMfaLoginSuccessful { privateKey: string; JTWToken: string; } - } /** @@ -93,16 +89,6 @@ const attemptLoginMfa = async ({ privateKey }); - // 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 fetchOrganizations(); - const orgId = userOrgs[0]._id; - localStorage.setItem("orgData.id", orgId); - - const orgUserProjects = await fetchMyOrganizationProjects(orgId); - localStorage.setItem("projectData.id", orgUserProjects[0]._id); - resolve({ success: true, loginResponse:{ diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index c4fe91b05..c9cedbf68 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -2,8 +2,6 @@ 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 KeyService from "@app/services/KeyService"; import Telemetry from "./telemetry/Telemetry"; @@ -119,20 +117,6 @@ const attemptLogin = async ( tag, privateKey }); - - // 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 fetchOrganizations(); - - const orgId = userOrgs[0]._id; - localStorage.setItem("orgData.id", orgId); - - const orgUserProjects = await fetchMyOrganizationProjects(orgId); - - if (orgUserProjects.length > 0) { - localStorage.setItem("projectData.id", orgUserProjects[0]._id); - } if (email) { telemetry.identify(email, email); diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts index c588eb965..fdbf495c1 100644 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ b/frontend/src/components/utilities/attemptLoginMfa.ts @@ -2,8 +2,6 @@ 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 KeyService from "@app/services/KeyService"; import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; @@ -83,16 +81,6 @@ const attemptLoginMfa = async ({ privateKey }); - // 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 fetchOrganizations(); - const orgId = userOrgs[0]._id; - localStorage.setItem("orgData.id", orgId); - - const orgUserProjects = await fetchMyOrganizationProjects(orgId); - localStorage.setItem("projectData.id", orgUserProjects[0]._id); - resolve(true); } catch (err) { reject(err); diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 7134d0501..e167de290 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -2,6 +2,8 @@ export { useAddOrgPmtMethod, useAddOrgTaxId, useCreateCustomerPortalSession, + useCreateOrg, + useDeleteOrgById, useDeleteOrgPmtMethod, useDeleteOrgTaxId, useGetOrganizations, diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 17f15307b..697daede5 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -41,6 +41,25 @@ export const useGetOrganizations = () => { }); } +export const useCreateOrg = () => { + return useMutation({ + mutationFn: async ({ + name + }: { + name: string; + }) => { + const { data: { organization } } = await apiRequest.post( + "/api/v2/organizations", + { + name + } + ); + + return organization; + } + }); +}; + export const useRenameOrg = () => { const queryClient = useQueryClient(); @@ -333,4 +352,33 @@ export const useGetOrgLicenses = (organizationId: string) => { }, enabled: true }); +} + +export const useDeleteOrgById = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + organizationId, + }: { + organizationId: string; + }) => { + const { data: { organization } } = await apiRequest.delete<{ organization: Organization }>( + `/api/v2/organizations/${organizationId}` + ); + return organization; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); + queryClient.invalidateQueries(organizationKeys.getOrgPlanBillingInfo(dto.organizationId)); + queryClient.invalidateQueries(organizationKeys.getOrgPlanTable(dto.organizationId)); + queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "monthly")); // You might need to invalidate for 'yearly' as well. + queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "yearly")); + queryClient.invalidateQueries(organizationKeys.getOrgBillingDetails(dto.organizationId)); + queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId)); + queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId)); + queryClient.invalidateQueries(organizationKeys.getOrgInvoices(dto.organizationId)); + queryClient.invalidateQueries(organizationKeys.getOrgLicenses(dto.organizationId)); + } + }); } \ No newline at end of file diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 8af9fe6f3..3e68aa57d 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -63,10 +63,12 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) => }); const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { + if (orgId === "") return []; + const { data } = await apiRequest.get<{ data: { permissions: PackRule>>[] }; }>(`/api/v1/roles/organization/${orgId}/permissions`, {}); - + return data.data.permissions; }; @@ -74,7 +76,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = useQuery({ queryKey: roleQueryKeys.getUserOrgPermissions({ orgId }), queryFn: () => getUserOrgPermissions({ orgId }), - enabled: Boolean(orgId), + // enabled: Boolean(orgId), select: (data) => { const rule = unpackRules>>(data); const ability = createMongoAbility(rule, { conditionsMatcher }); diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index e4d94d041..338c3b069 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -5,6 +5,7 @@ export { useCreateAPIKey, useDeleteAPIKey, useDeleteOrgMembership, + useDeleteUser, useGetMyAPIKeys, useGetMyIp, useGetMyOrganizationProjects, @@ -17,5 +18,4 @@ export { useRevokeMySessions, useUpdateMfaEnabled, useUpdateOrgUserRole, - useUpdateUserAuthMethods -} from "./queries"; \ No newline at end of file + useUpdateUserAuthMethods} from "./queries"; \ 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 c940c8d61..04e620a9e 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -42,6 +42,24 @@ export const fetchUserDetails = async () => { export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails); +export const useDeleteUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { + const { data: { user } } = await apiRequest.delete<{ user: User }>("/api/v2/users/me"); + return user; + }, + onSuccess: () => { + queryClient.invalidateQueries(userKeys.getUser); + queryClient.invalidateQueries(userKeys.userAction); + queryClient.invalidateQueries(userKeys.myIp); + queryClient.invalidateQueries(userKeys.myAPIKeys); + queryClient.invalidateQueries(userKeys.mySessions); + } + }); +}; + export const fetchUserAction = async (action: string) => { const { data } = await apiRequest.get<{ userAction: string }>("/api/v1/user-action", { params: { @@ -216,11 +234,16 @@ export const useLogoutUser = () => onSuccess: () => { setAuthToken(""); // Delete the cookie by not setting a value; Alternatively clear the local storage - localStorage.setItem("publicKey", ""); - localStorage.setItem("encryptedPrivateKey", ""); - localStorage.setItem("iv", ""); - localStorage.setItem("tag", ""); - localStorage.setItem("PRIVATE_KEY", ""); + 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"); } }); diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 8c1391687..22c4f1fad 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -113,6 +113,7 @@ export const AppLayout = ({ children }: LayoutProps) => { // eslint-disable-next-line prefer-const const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); + const { user } = useUser(); const { subscription } = useSubscription(); const workspaceId = currentWorkspace?._id || ""; @@ -148,16 +149,6 @@ export const AppLayout = ({ children }: LayoutProps) => { try { console.log("Logging out..."); await logout.mutateAsync(); - 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"); router.push("/login"); } catch (error) { console.error(error); diff --git a/frontend/src/pages/api/secret-scanning/getRisksByOrganization.ts b/frontend/src/pages/api/secret-scanning/getRisksByOrganization.ts index 822086b21..a2072f2ed 100644 --- a/frontend/src/pages/api/secret-scanning/getRisksByOrganization.ts +++ b/frontend/src/pages/api/secret-scanning/getRisksByOrganization.ts @@ -1,6 +1,6 @@ import SecurityClient from "@app/components/utilities/SecurityClient"; -export type GitRisks = { +export type IGitRisks = { _id: string; description: string; startLine: string; @@ -41,7 +41,7 @@ export type GitRisks = { * Will create a new integration session and return it for the given org * @returns */ -const getRisksByOrganization = (oranizationId: string): Promise => +const getRisksByOrganization = (oranizationId: string): Promise => SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, { method: "GET", headers: { diff --git a/frontend/src/pages/org/[id]/members/index.tsx b/frontend/src/pages/org/[id]/members/index.tsx index e14e675ed..d287400ae 100644 --- a/frontend/src/pages/org/[id]/members/index.tsx +++ b/frontend/src/pages/org/[id]/members/index.tsx @@ -18,4 +18,4 @@ export default function SettingsOrg() { ); } -SettingsOrg.requireAuth = true; +SettingsOrg.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/pages/org/none/index.tsx b/frontend/src/pages/org/none/index.tsx new file mode 100644 index 000000000..0dacc3c03 --- /dev/null +++ b/frontend/src/pages/org/none/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { NonePage } from "@app/views/Org/NonePage"; + +export default function NoneOrganization() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +NoneOrganization.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx new file mode 100644 index 000000000..44bf24ce8 --- /dev/null +++ b/frontend/src/views/Login/Login.utils.tsx @@ -0,0 +1,17 @@ +import { NextRouter } from "next/router"; + +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; + +export const navigateUserToOrg = async (router: NextRouter) => { + const userOrgs = await fetchOrganizations(); + + if (userOrgs.length > 0) { + // user is part of at least 1 org + const userOrg = userOrgs[0] && userOrgs[0]._id; + localStorage.setItem("orgData.id", userOrg); + router.push(`/org/${userOrg}/overview`); + } else { + // user is not part of any org + router.push("/org/none"); + } +} diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 3cc26fb4e..c8400248b 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -12,9 +12,10 @@ 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 { navigateUserToOrg } from "../../Login.utils"; + type Props = { setStep: (step: number) => void; email: string; @@ -73,6 +74,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: email: email.toLowerCase(), password }); + if (isLoginSuccessful && isLoginSuccessful.success) { // case: login was successful @@ -82,15 +84,14 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: setIsLoading(false); return; } - const userOrgs = await fetchOrganizations(); - const userOrg = userOrgs[0] && userOrgs[0]._id; + + await navigateUserToOrg(router); // case: login does not require MFA step createNotification({ text: "Successfully logged in", type: "success" }); - router.push(`/org/${userOrg}/overview`); } } } catch (err) { diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index 6759e85aa..df8e7b0d7 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -12,10 +12,10 @@ 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"; +import { navigateUserToOrg } from "../../Login.utils"; // The style for the verification code input const props = { @@ -127,8 +127,6 @@ export const MFAStep = ({ if (isLoginSuccessful) { setIsLoading(false); - const userOrgs = await fetchOrganizations(); - const userOrg = userOrgs[0] && userOrgs[0]._id; // case: login does not require MFA step createNotification({ @@ -144,7 +142,7 @@ export const MFAStep = ({ }); } - router.push(`/org/${userOrg}/overview`); + await navigateUserToOrg(router); } else { createNotification({ text: "Failed to log in", diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index d3c34a209..b0994a0aa 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -10,9 +10,10 @@ 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"; +import { navigateUserToOrg } from "../../Login.utils"; + type Props = { providerAuthToken: string; email: string; @@ -92,8 +93,6 @@ export const PasswordStep = ({ } // case: login does not require MFA step - const userOrgs = await fetchOrganizations(); - const userOrg = userOrgs[0]._id; setIsLoading(false); createNotification({ text: "Successfully logged in", @@ -108,7 +107,7 @@ export const PasswordStep = ({ }); } - router.push(`/org/${userOrg}/overview`); + await navigateUserToOrg(router); } } } catch (err) { @@ -120,8 +119,6 @@ export const PasswordStep = ({ console.error(err); } }; - - return (
; + +export const NonePage = () => { + const { createNotification } = useNotificationContext(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "createOrg", + ] as const); + + const { mutateAsync } = useCreateOrg(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + name: "" + } + }); + + useEffect(() => { + handlePopUpOpen("createOrg"); + }, []); + + const onFormSubmit = async ({ name }: FormData) => { + try { + + const organization = await mutateAsync({ + name + }); + + createNotification({ + text: "Successfully created organization", + type: "success" + }); + + window.location.href = `/org/${organization._id}/overview`; + + reset(); + handlePopUpToggle("createOrg", false); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to created organization", + type: "error" + }); + } + } + + return ( +
+ + + + ( + + + + )} + /> + + + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/NonePage/index.tsx b/frontend/src/views/Org/NonePage/index.tsx new file mode 100644 index 000000000..42505f072 --- /dev/null +++ b/frontend/src/views/Org/NonePage/index.tsx @@ -0,0 +1 @@ +export { NonePage } from "./NonePage"; \ No newline at end of file diff --git a/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx b/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx index 28f0313b2..4add11835 100644 --- a/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx +++ b/frontend/src/views/SecretScanning/components/SecretScanningLogsTable.tsx @@ -14,14 +14,14 @@ import { } from "@app/components/v2"; import timeSince from "@app/ee/utilities/timeSince"; import getRisksByOrganization, { - GitRisks + IGitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization"; import { RiskStatusSelection } from "./RiskStatusSelection"; export const SecretScanningLogsTable = () => { const [isLoading, setIsLoading] = useState(false); - const [gitRisks, setGitRisks] = useState([]); + const [gitRisks, setGitRisks] = useState([]); useEffect(() => { const fetchRisks = async () => { diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx new file mode 100644 index 000000000..dd810f1ac --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -0,0 +1,81 @@ +import { useRouter } from "next/router"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + DeleteActionModal +} from "@app/components/v2"; +import { useOrganization, useUser } from "@app/context"; +import { + useDeleteOrgById, + useGetOrgUsers +} from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; +import { navigateUserToOrg } from "@app/views/Login/Login.utils"; + +export const OrgDeleteSection = () => { + const router = useRouter(); + const { currentOrg } = useOrganization(); + const { user } = useUser(); + const { createNotification } = useNotificationContext(); + const { data: members } = useGetOrgUsers(currentOrg?._id ?? ""); + + const membershipOrg = members?.find((member) => member.user._id === user._id); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteOrg" + ] as const); + + const { mutateAsync, isLoading } = useDeleteOrgById(); + + const handleDeleteOrgSubmit = async () => { + try { + if (!currentOrg?._id) return; + + await mutateAsync({ + organizationId: currentOrg?._id + }); + + createNotification({ + text: "Successfully deleted organization", + type: "success" + }); + + await navigateUserToOrg(router); + + handlePopUpClose("deleteOrg"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete organization", + type: "error" + }); + } + } + + return ( +
+

+ Danger Zone +

+ + handlePopUpToggle("deleteOrg", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteOrgSubmit} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/index.tsx new file mode 100644 index 000000000..b55aa296e --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgDeleteSection/index.tsx @@ -0,0 +1 @@ +export { OrgDeleteSection } from "./OrgDeleteSection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx index 178992f7f..e4e219e0f 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx @@ -1,11 +1,24 @@ +import { useOrganization, useUser } from "@app/context"; +import { useGetOrgUsers } from "@app/hooks/api"; + +import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection"; export const OrgGeneralTab = () => { + const { currentOrg } = useOrganization(); + const { user } = useUser(); + const { data: members } = useGetOrgUsers(currentOrg?._id ?? ""); + + const membershipOrg = members?.find((member) => member.user._id === user._id); + return (
+ {(membershipOrg && membershipOrg.role === "admin") && ( + + )}
); }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx index 9ef12bf13..bc2547528 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsSection/OrgIncidentContactsSection.tsx @@ -18,7 +18,7 @@ export const OrgIncidentContactsSection = () => { const permission = useOrgPermission(); return ( -
+

{t("section.incident.incident-contacts")}

diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx index f20271210..ec433f34e 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx @@ -53,7 +53,7 @@ export const OrgNameChangeSection = (): JSX.Element => { onSubmit={handleSubmit(onFormSubmit)} className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" > -

Organization name

+

Name

{ + const router = useRouter(); + const { createNotification } = useNotificationContext(); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteAccount" + ] as const); + + const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteUser(); + + const handleDeleteAccountSubmit = async () => { + try { + await deleteUserMutateAsync(); + + createNotification({ + text: "Successfully deleted account", + type: "success" + }); + + router.push("/login"); + handlePopUpClose("deleteAccount"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete account", + type: "error" + }); + } + } + + return ( +
+

+ Danger Zone +

+ + handlePopUpToggle("deleteAccount", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteAccountSubmit} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/DeleteAccountSection/index.tsx b/frontend/src/views/Settings/PersonalSettingsPage/DeleteAccountSection/index.tsx new file mode 100644 index 000000000..d54d3d4ac --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/DeleteAccountSection/index.tsx @@ -0,0 +1 @@ +export { DeleteAccountSection } from "./DeleteAccountSection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalGeneralTab/PersonalGeneralTab.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalGeneralTab/PersonalGeneralTab.tsx index 531776751..e5dced524 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalGeneralTab/PersonalGeneralTab.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalGeneralTab/PersonalGeneralTab.tsx @@ -1,4 +1,5 @@ import { ChangeLanguageSection } from "../ChangeLanguageSection"; +import { DeleteAccountSection } from "../DeleteAccountSection"; import { EmergencyKitSection } from "../EmergencyKitSection"; import { SessionsSection } from "../SessionsSection"; import { UserNameSection } from "../UserNameSection"; @@ -10,6 +11,7 @@ export const PersonalGeneralTab = () => { +
); } \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx index 18f27d8d5..8b62b1a08 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -1,10 +1,11 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, FormControl, Input } from "@app/components/v2"; +import { + Button, + DeleteActionModal, +} from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -13,78 +14,74 @@ import { } from "@app/context"; import { useToggle } from "@app/hooks"; import { useDeleteWorkspace } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; export const DeleteProjectSection = () => { - const { t } = useTranslation(); const router = useRouter(); const { createNotification } = useNotificationContext(); - const { currentWorkspace } = useWorkspace(); - const { currentOrg } = useOrganization(); - const [isDeleting, setIsDeleting] = useToggle(); - const [deleteProjectInput, setDeleteProjectInput] = useState(""); - const deleteWorkspace = useDeleteWorkspace(); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteWorkspace" +] as const); - const onDeleteWorkspace = async () => { + const { currentOrg } = useOrganization(); + const { currentWorkspace } = useWorkspace(); + const [isDeleting, setIsDeleting] = useToggle(); + const deleteWorkspace = useDeleteWorkspace(); + + const handleDeleteWorkspaceSubmit = async () => { setIsDeleting.on(); try { if (!currentWorkspace?._id) return; + await deleteWorkspace.mutateAsync({ workspaceID: currentWorkspace?._id }); - // redirect user to the org overview - router.push(`/org/${currentOrg?._id}/overview`); - + createNotification({ - text: "Successfully deleted workspace", + text: "Successfully deleted project", type: "success" }); - } catch (error) { - console.error(error); + + router.push(`/org/${currentOrg?._id}/overview`); + handlePopUpClose("deleteWorkspace"); + } catch (err) { + console.error(err); createNotification({ - text: "Failed to delete workspace", + text: "Failed to delete project", type: "error" }); } finally { setIsDeleting.off(); } - }; + } return ( -
-

{t("settings.project.danger-zone")}

-

{t("settings.project.danger-zone-note")}

-
- - Type {currentWorkspace?.name} to delete the - workspace -
- } - > - setDeleteProjectInput(e.target.value)} - value={deleteProjectInput} - placeholder="Type the project name to delete" - className="bg-mineshaft-800" - /> - -
- - {(isAllowed) => ( - - )} - -

- {t("settings.project.delete-project-note")} -

+
+

+ Danger Zone +

+ + {(isAllowed) => ( + + )} + + handlePopUpToggle("deleteWorkspace", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteWorkspaceSubmit} + />
); };