diff --git a/package.json b/package.json index 996acd1d..91016128 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.0.22", + "version": "1.0.23", "main": "index.ts", "license": "UNLICENSED", "scripts": { diff --git a/src/dataLoaders.ts b/src/dataLoaders.ts index 6c696049..6f7abc72 100644 --- a/src/dataLoaders.ts +++ b/src/dataLoaders.ts @@ -83,6 +83,9 @@ export default class DataLoaders { const queryResult = await this.dbConnection.collection(collectionName) .find({ [fieldName]: { $in: values }, + isRemoved: { + $ne: true, + }, }) .toArray(); diff --git a/src/models/project.ts b/src/models/project.ts index 98fb71e9..e8f04356 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -194,6 +194,7 @@ export default class ProjectModel extends AbstractModel impleme /** * Generates integration ID that's used in collector URL for sending events + * @returns integration ID as string. */ public static generateIntegrationId(): string { return uuid(); @@ -203,6 +204,7 @@ export default class ProjectModel extends AbstractModel impleme * Generates new integration token with integration id field * * @param integrationId - integration id for using in collector URL + * @returns generated integration token. */ public static generateIntegrationToken(integrationId: string): string { const secret = uuid(); @@ -383,4 +385,24 @@ export default class ProjectModel extends AbstractModel impleme console.log(error); } } + + /** + * Mark project as removed. + */ + public async markProjectAsRemoved(): Promise { + await this.collection.updateOne({ _id: this._id }, { + $set: { isRemoved: true }, + }); + + try { + /** + * Remove users in project collection + */ + await this.dbConnection.collection('users-in-project:' + this._id) + .drop(); + } catch (error) { + console.log(`Can't remove collection "users-in-project:${this._id}" because it doesn't exist.`); + console.log(error); + } + } } diff --git a/src/models/projectToWorkspace.js b/src/models/projectToWorkspace.js index 02e2384d..ae50e596 100644 --- a/src/models/projectToWorkspace.js +++ b/src/models/projectToWorkspace.js @@ -15,7 +15,7 @@ const { ObjectID } = require('mongodb'); class ProjectToWorkspace { /** * Creates an instance of ProjectToWorkspace - * @param {string|ObjectID} workspaceId + * @param {string|ObjectID} workspaceId workspace. */ constructor(workspaceId) { this.workspaceId = new ObjectID(workspaceId); @@ -53,6 +53,9 @@ class ProjectToWorkspace { async findById(projectWorkspaceId) { const projectWorkspace = await this.collection.findOne({ _id: new ObjectID(projectWorkspaceId), + isRemoved: { + $ne: true, + }, }); if (!projectWorkspace) { diff --git a/src/models/user.ts b/src/models/user.ts index b6e9cb8c..f9707643 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -62,7 +62,7 @@ export interface UserNotificationsDBScheme { /** * Types of notifications to receive */ - whatToReceive: {[key in UserNotificationType]: boolean}; + whatToReceive: { [key in UserNotificationType]: boolean }; } /** @@ -165,6 +165,7 @@ export default class UserModel extends AbstractModel implements Us /** * Generate 16bytes password + * @returns generated password. */ public static generatePassword(): Promise { return new Promise((resolve, reject) => { @@ -172,7 +173,7 @@ export default class UserModel extends AbstractModel implements Us if (err) { return reject(err); } - + console.log(buff.toString('hex')); resolve(buff.toString('hex')); }); }); @@ -182,6 +183,7 @@ export default class UserModel extends AbstractModel implements Us * Compose default notifications settings for new users. * * @param email - user email from the sign-up form will be used as email-channel endpoint + * @returns default notification settings for new users. */ public static generateDefaultNotificationsSettings(email: string): UserNotificationsDBScheme { return { @@ -212,7 +214,6 @@ export default class UserModel extends AbstractModel implements Us * Change user's password * Hashes new password and updates the document * - * @param userId - user ID * @param newPassword - new user password */ public async changePassword(newPassword: string): Promise { @@ -318,7 +319,7 @@ export default class UserModel extends AbstractModel implements Us * Remove workspace from membership collection * @param workspaceId - id of workspace to remove */ - public async removeWorkspace(workspaceId: string): Promise<{workspaceId: string}> { + public async removeWorkspace(workspaceId: string): Promise<{ workspaceId: string }> { await this.membershipCollection.deleteOne({ workspaceId: new ObjectId(workspaceId), }); @@ -328,6 +329,22 @@ export default class UserModel extends AbstractModel implements Us }; } + /** + * Mark workspace as removed. + * @param workspaceId - id of workspace to remove + */ + public async markWorkspaceAsRemoved(workspaceId: string): Promise<{ workspaceId: string }> { + await this.membershipCollection.updateOne({ + workspaceId: new ObjectId(workspaceId), + }, { + $set: { isRemoved: true }, + }); + + return { + workspaceId, + }; + } + /** * Confirm membership of workspace by id * @param workspaceId - workspace id to confirm @@ -352,10 +369,16 @@ export default class UserModel extends AbstractModel implements Us workspaceId: { $in: idsAsObjectId, }, + isRemoved: { + $ne: true, + }, isPending: { $ne: true, }, } : { + isRemoved: { + $ne: true, + }, isPending: { $ne: true, }, diff --git a/src/models/workspace.ts b/src/models/workspace.ts index 6ceebd30..65263dca 100644 --- a/src/models/workspace.ts +++ b/src/models/workspace.ts @@ -94,6 +94,7 @@ export default class WorkspaceModel extends AbstractModel imp /** * Generates SHA-256 hash that used as invite hash + * @returns invitation hash as string. */ public static generateInviteHash(): string { return crypto @@ -106,6 +107,7 @@ export default class WorkspaceModel extends AbstractModel imp * Checks is provided document represents pending member * * @param doc - doc to check + * @returns status of is current member pending. */ public static isPendingMember(doc: MemberDBScheme): doc is PendingMemberDBScheme { return !!(doc as PendingMemberDBScheme).userEmail && !(doc as ConfirmedMemberDBScheme).userId; @@ -127,6 +129,20 @@ export default class WorkspaceModel extends AbstractModel imp } } + /** + * Mark workspace as removed. + */ + public async markWorkspaceAsRemoved(): Promise { + /** + * Delete the workspace data. + */ + await this.collection.updateOne({ + _id: new ObjectId(this._id), + }, { + $set: { isRemoved: true }, + }); + } + /** * Update invite hash of workspace * @param inviteHash - new invite hash @@ -407,6 +423,7 @@ export default class WorkspaceModel extends AbstractModel imp /** * Due date of the current workspace tariff plan + * @returns Date object of due date. */ public getTariffPlanDueDate(): Date { const lastChargeDate = new Date(this.lastChargeDate); @@ -415,7 +432,8 @@ export default class WorkspaceModel extends AbstractModel imp } /** - * Is tariff plan expired or not + * Is tariff plan expired or not. + * @returns current status of tariff plan. */ public isTariffPlanExpired(): boolean { const date = new Date(); diff --git a/src/models/workspacesFactory.ts b/src/models/workspacesFactory.ts index 1f1cb495..9ddd7baf 100644 --- a/src/models/workspacesFactory.ts +++ b/src/models/workspacesFactory.ts @@ -106,7 +106,16 @@ export default class WorkspacesFactory extends AbstractModelFactory { - const workspaceData = await this.collection.findOne({ inviteHash }); + const workspaceData = await this.collection.findOne({ + inviteHash, + isRemoved: { + $ne: true, + }, + }); + + if (!workspaceData) { + return null; + } return workspaceData && new WorkspaceModel(workspaceData); } diff --git a/src/resolvers/workspace.js b/src/resolvers/workspace.js index bcbfad6a..cb914825 100644 --- a/src/resolvers/workspace.js +++ b/src/resolvers/workspace.js @@ -9,7 +9,7 @@ import ProjectToWorkspace from '../models/projectToWorkspace'; import Validator from '../utils/validator'; import { dateFromObjectId } from '../utils/dates'; -const { ApolloError, UserInputError, ForbiddenError } = require('apollo-server-express'); +const { ApolloError, UserInputError } = require('apollo-server-express'); const crypto = require('crypto'); /** @@ -145,6 +145,10 @@ module.exports = { const currentUser = await factories.usersFactory.findById(user.id); const workspace = await factories.workspacesFactory.findByInviteHash(inviteHash); + if (!workspace) { + throw new ApolloError('There is no workspace with provided id'); + } + if (await workspace.getMemberInfo(user.id)) { throw new ApolloError('You are already member of this workspace'); } @@ -175,6 +179,10 @@ module.exports = { const currentUser = await factories.usersFactory.findById(user.id); const workspace = await factories.workspacesFactory.findById(workspaceId); + if (!workspace) { + throw new ApolloError('There is no workspace with provided id'); + } + const hash = crypto .createHash('sha256') .update(`${workspaceId}:${currentUser.email}:${process.env.INVITE_LINK_HASH_SALT}`) @@ -288,7 +296,7 @@ module.exports = { * @param {string} workspaceId - id of the workspace where the user should be removed * @param {UserInContext} user - current authorized user {@see ../index.js} * @param {ContextFactories} factories - factories for working with models - * @return {Promise} - true if operation is successful + * @return {Promise} - true if operation is successful, false if there is no other admin left. */ async leaveWorkspace(_obj, { workspaceId }, { user, factories }) { const userModel = await factories.usersFactory.findById(user.id); @@ -307,7 +315,7 @@ module.exports = { ); if (!isThereOtherAdmins) { - throw new ForbiddenError('You can\'t leave this workspace because you are the last admin'); + return false; } } await workspaceModel.removeMember(userModel); @@ -315,6 +323,72 @@ module.exports = { return true; }, + /** + * Mutation in order to leave workspace + * @param {ResolverObj} _obj - object that contains the result returned from the resolver on the parent field + * @param {string} workspaceId - id of the workspace where the user should be removed + * @param {UserInContext} user - current authorized user {@see ../index.js} + * @param {ContextFactories} factories - factories for working with models + * @return {Promise} - true if operation is successful + */ + async deleteWorkspace(_obj, { workspaceId }, { user, factories }) { + const workspaceModel = await factories.workspacesFactory.findById(workspaceId); + + if (!workspaceModel) { + throw new UserInputError('There is no workspace with provided id'); + } + + const membersInfo = (await workspaceModel.getMembers()); + + for (const member of membersInfo) { + /** + * remove members from workspace. + */ + if (member.userId) { + const userModel = await factories.usersFactory.findById(member.userId.toString()); + + await userModel.markWorkspaceAsRemoved(workspaceId.toString()); + } + /** + * remove the members who's invitation is pending. + */ + if (member.userEmail) { + const invitedUser = await factories.usersFactory.findByEmail(member.userEmail); + + /** + * If user is already uses Hawk + */ + if (invitedUser) { + await invitedUser.removeWorkspace(workspaceId); + } + + /** + * Remove User's invitation from workspace. + */ + await workspaceModel.removeMemberByEmail(member.userEmail); + } + } + + const projectToWorkspace = new ProjectToWorkspace(workspaceId.toString()); + + const projectsInfo = await projectToWorkspace.getProjects(); + + if (projectsInfo.length) { + for (const project of projectsInfo) { + /** + * Remove project + */ + const projectModel = await factories.projectsFactory.findById(project.id.toString()); + + await projectModel.markProjectAsRemoved(); + } + } + + await workspaceModel.markWorkspaceAsRemoved(); + + return true; + }, + /** * Change workspace plan for default plan mutation implementation * @@ -379,6 +453,7 @@ module.exports = { /** * Return empty object to call resolver for specific mutation + * @returns Empty object. */ workspace: () => ({}), }, @@ -481,6 +556,7 @@ module.exports = { /** * Returns type of the team member * @param {MemberDBScheme} memberData - result from resolver above + * @returns type of member. */ __resolveType(memberData) { return WorkspaceModel.isPendingMember(memberData) ? 'PendingMember' : 'ConfirmedMember'; @@ -496,6 +572,7 @@ module.exports = { * @param {ConfirmedMemberDBScheme} memberData - result from resolver above * @param _args - empty list of args * @param {ContextFactories} factories - factories for working with models + * @returns user data of the workspace */ user(memberData, _args, { factories }) { return factories.usersFactory.findById(memberData.userId.toString()); @@ -504,6 +581,7 @@ module.exports = { /** * True if user has admin permissions * @param {ConfirmedMemberDBScheme} memberData - result from resolver above + * @returns status of is user admin */ isAdmin(memberData) { return !WorkspaceModel.isPendingMember(memberData) && (memberData.isAdmin || false); diff --git a/src/typeDefs/workspace.ts b/src/typeDefs/workspace.ts index d6e91803..25dae331 100644 --- a/src/typeDefs/workspace.ts +++ b/src/typeDefs/workspace.ts @@ -268,6 +268,7 @@ export default gql` """ Mutation in order to leave workspace Returns true if operation is successful + Returns false if there is no other admin lefts """ leaveWorkspace( """ @@ -275,5 +276,16 @@ export default gql` """ workspaceId: ID! ): Boolean! @requireAuth + + """ + Mutation in order to delete workspace + Returns true if operation is successful + """ + deleteWorkspace( + """ + Workspace ID + """ + workspaceId: ID! + ): Boolean! @requireAdmin } `;