1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-22 11:45:48 +00:00

Merge pull request from Infisical/delete-org

Delete user, organization, project capabilities feature/update
This commit is contained in:
BlackMagiq
2023-10-11 19:26:19 +01:00
committed by GitHub
54 changed files with 1285 additions and 305 deletions

@ -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";

@ -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

@ -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

@ -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
});
};

@ -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
});
}

@ -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
});
}

@ -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({

@ -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,
{

@ -29,6 +29,4 @@ const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
});
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
export default GitAppInstallationSession;
export const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);

@ -26,6 +26,4 @@ const gitAppOrganizationInstallation = new Schema<Installation>({
});
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
export default GitAppOrganizationInstallation;
export const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);

@ -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<GitRisks>({
const gitRisks = new Schema<IGitRisks>({
id: {
type: String,
},
@ -147,6 +147,4 @@ const gitRisks = new Schema<GitRisks>({
}
}, { timestamps: true });
const GitRisks = model<GitRisks>("GitRisks", gitRisks);
export default GitRisks;
export const GitRisks = model<IGitRisks>("GitRisks", gitRisks);

@ -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";

@ -50,6 +50,4 @@ const roleSchema = new Schema<IRole>(
roleSchema.index({ organization: 1, workspace: 1 });
const Role = model<IRole>("Role", roleSchema);
export default Role;
export const Role = model<IRole>("Role", roleSchema);

@ -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) => {

@ -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.

@ -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();
}
}

@ -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();
}
};

@ -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";

@ -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";

@ -13,15 +13,6 @@ router.get(
organizationController.getOrganizations
);
router.post(
// not used on frontend
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationController.createOrganization
);
router.get(
"/:organizationId",
requireAuth({

@ -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;

@ -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;

@ -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

@ -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() })
});

@ -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);

@ -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:{

@ -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);

@ -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);

@ -2,6 +2,8 @@ export {
useAddOrgPmtMethod,
useAddOrgTaxId,
useCreateCustomerPortalSession,
useCreateOrg,
useDeleteOrgById,
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetOrganizations,

@ -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));
}
});
}

@ -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<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
}>(`/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<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });

@ -5,6 +5,7 @@ export {
useCreateAPIKey,
useDeleteAPIKey,
useDeleteOrgMembership,
useDeleteUser,
useGetMyAPIKeys,
useGetMyIp,
useGetMyOrganizationProjects,

@ -42,6 +42,20 @@ 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.clear();
}
});
};
export const fetchUserAction = async (action: string) => {
const { data } = await apiRequest.get<{ userAction: string }>("/api/v1/user-action", {
params: {
@ -208,21 +222,31 @@ export const useRegisterUserAction = () => {
});
};
export const useLogoutUser = () =>
useMutation({
export const useLogoutUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
await apiRequest.post("/api/v1/auth/logout");
},
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");
queryClient.clear();
}
});
}
export const useGetMyIp = () => {
return useQuery({

@ -115,6 +115,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 || "";
@ -157,16 +158,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);

@ -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<GitRisks[]> =>
const getRisksByOrganization = (oranizationId: string): Promise<IGitRisks[]> =>
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, {
method: "GET",
headers: {

@ -18,4 +18,4 @@ export default function SettingsOrg() {
);
}
SettingsOrg.requireAuth = true;
SettingsOrg.requireAuth = true;

@ -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 (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<NonePage />
</>
);
}
NoneOrganization.requireAuth = true;

@ -2,7 +2,6 @@ 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 { getAuthToken, isLoggedIn } from "@app/reactQuery";
@ -11,6 +10,8 @@ import {
MFAStep,
SAMLSSOStep
} from "./components";
// import { navigateUserToOrg } from "../../Login.utils";
import { navigateUserToOrg } from "./Login.utils";
export const Login = () => {
const router = useRouter();
@ -24,10 +25,6 @@ export const Login = () => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
try {
const userOrgs = await fetchOrganizations();
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrg = userOrgs[0] && userOrgs[0]._id;
// user details
const userDetails = await fetchUserDetails()
// send details back to client
@ -40,7 +37,8 @@ export const Login = () => {
const instance = axios.create()
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
}
router.push(`/org/${userOrg}/overview`);
await navigateUserToOrg(router);
} catch (error) {
console.log("Error - Not logged in yet");
}

@ -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");
}
}

@ -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) {

@ -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",

@ -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 (
<form

@ -0,0 +1,112 @@
import { useEffect } from "react";
import { Controller, 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,
Input,
Modal,
ModalContent} from "@app/components/v2";
import { useCreateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
const schema = yup.object({
name: yup.string().required("Organization name is required"),
}).required();
export type FormData = yup.InferType<typeof schema>;
export const NonePage = () => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"createOrg",
] as const);
const { mutateAsync } = useCreateOrg();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
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 (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<Modal
isOpen={popUp?.createOrg?.isOpen}
>
<ModalContent
title="Create Organization"
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="Acme Corp"
/>
</FormControl>
)}
/>
<Button
className=""
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</form>
</ModalContent>
</Modal>
</div>
);
}

@ -0,0 +1 @@
export { NonePage } from "./NonePage";

@ -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<GitRisks[]>([]);
const [gitRisks, setGitRisks] = useState<IGitRisks[]>([]);
useEffect(() => {
const fetchRisks = async () => {

@ -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 (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<Button
isLoading={isLoading}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteOrg")}
isDisabled={(membershipOrg && membershipOrg.role !== "admin")}
>
{`Delete ${currentOrg?.name}`}
</Button>
<DeleteActionModal
isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?"
subTitle={`Permanently remove ${currentOrg?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteOrg", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteOrgSubmit}
/>
</div>
);
}

@ -0,0 +1 @@
export { OrgDeleteSection } from "./OrgDeleteSection";

@ -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 (
<div>
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{(membershipOrg && membershipOrg.role === "admin") && (
<OrgDeleteSection />
)}
</div>
);
};

@ -18,7 +18,7 @@ export const OrgIncidentContactsSection = () => {
const permission = useOrgPermission();
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>

@ -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"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""

@ -0,0 +1,64 @@
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useDeleteUser } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteAccountSection = () => {
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 (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<Button
isLoading={isLoading}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteAccount")}
>
Delete my account
</Button>
<DeleteActionModal
isOpen={popUp.deleteAccount.isOpen}
title="Are you sure want to delete your account?"
subTitle="Permanently remove this account and all of its data. This action is not reversible, so please be careful."
onChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteAccountSubmit}
/>
</div>
);
}

@ -0,0 +1 @@
export { DeleteAccountSection } from "./DeleteAccountSection";

@ -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 = () => {
<ChangeLanguageSection />
<SessionsSection />
<EmergencyKitSection />
<DeleteAccountSection />
</div>
);
}

@ -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 (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
<FormControl
label={
<div className="mb-0.5 text-sm font-normal text-gray-400">
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
workspace
</div>
}
>
<Input
onChange={(e) => setDeleteProjectInput(e.target.value)}
value={deleteProjectInput}
placeholder="Type the project name to delete"
className="bg-mineshaft-800"
/>
</FormControl>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
colorSchema="danger"
onClick={onDeleteWorkspace}
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
isLoading={isDeleting}
>
{t("settings.project.delete-project")}
</Button>
)}
</ProjectPermissionCan>
<p className="mt-3 ml-0.5 text-xs text-gray-500">
{t("settings.project.delete-project-note")}
</p>
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently remove ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
</div>
);
};