feat: feedback changes added
This commit is contained in:
committed by
Andrew Bastin
parent
a0d40c8776
commit
3c2b48a635
@@ -27,7 +27,7 @@ import {
|
||||
} from './input-types.args';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { UserDeleteData } from 'src/user/user.model';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Admin)
|
||||
@@ -71,20 +71,20 @@ export class AdminResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Revoke a user invite by invitee email',
|
||||
description: 'Revoke a user invites by invitee emails',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async revokeUserInvitationByAdmin(
|
||||
async revokeUserInvitationsByAdmin(
|
||||
@GqlAdmin() adminUser: Admin,
|
||||
@Args({
|
||||
name: 'inviteeEmail',
|
||||
description: 'Invite Email',
|
||||
type: () => ID,
|
||||
name: 'inviteeEmails',
|
||||
description: 'Invitee Emails',
|
||||
type: () => [String],
|
||||
})
|
||||
inviteeEmail: string,
|
||||
inviteeEmails: string[],
|
||||
): Promise<boolean> {
|
||||
const invite = await this.adminService.revokeUserInvite(
|
||||
inviteeEmail,
|
||||
const invite = await this.adminService.revokeUserInvites(
|
||||
inviteeEmails,
|
||||
adminUser.uid,
|
||||
);
|
||||
if (E.isLeft(invite)) throwErr(invite.left);
|
||||
@@ -93,6 +93,7 @@ export class AdminResolver {
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete an user account from infra',
|
||||
deprecationReason: 'Use removeUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserByAdmin(
|
||||
@@ -108,7 +109,7 @@ export class AdminResolver {
|
||||
return removedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => [UserDeleteData], {
|
||||
@Mutation(() => [UserDeletionResult], {
|
||||
description: 'Delete user accounts from infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
@@ -119,7 +120,7 @@ export class AdminResolver {
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<UserDeleteData[]> {
|
||||
): Promise<UserDeletionResult[]> {
|
||||
const deletionResults = await this.adminService.removeUserAccounts(
|
||||
userUIDs,
|
||||
);
|
||||
@@ -129,6 +130,7 @@ export class AdminResolver {
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Make user an admin',
|
||||
deprecationReason: 'Use makeUsersAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUserAdmin(
|
||||
@@ -188,6 +190,7 @@ export class AdminResolver {
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove user as admin',
|
||||
deprecationReason: 'Use demoteUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserAsAdmin(
|
||||
@@ -207,7 +210,7 @@ export class AdminResolver {
|
||||
description: 'Remove users as admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUsersAsAdmin(
|
||||
async demoteUsersByAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
@@ -215,7 +218,7 @@ export class AdminResolver {
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.removeUsersAsAdmin(userUIDs);
|
||||
const isUpdated = await this.adminService.demoteUsersByAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
@@ -372,14 +375,4 @@ export class AdminResolver {
|
||||
userInvited(@GqlUser() admin: AuthUser) {
|
||||
return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`);
|
||||
}
|
||||
|
||||
@Subscription(() => InvitedUser, {
|
||||
description: 'Listen for User Invite Revocation',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@SkipThrottle()
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
userRevoked(@GqlUser() admin: AuthUser) {
|
||||
return this.pubsub.asyncIterator(`admin/${admin.uid}/invitation_revoked`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('AdminService', () => {
|
||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||
|
||||
return expect(
|
||||
await adminService.removeUsersAsAdmin(['123456']),
|
||||
await adminService.demoteUsersByAdmin(['123456']),
|
||||
).toEqualRight(true);
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ describe('AdminService', () => {
|
||||
]);
|
||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||
|
||||
return expect(await adminService.removeUsersAsAdmin(['123'])).toEqualLeft(
|
||||
return expect(await adminService.demoteUsersByAdmin(['123'])).toEqualLeft(
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
USERS_NOT_FOUND,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_NOT_FOUND,
|
||||
USER_IS_ADMIN,
|
||||
@@ -29,7 +30,7 @@ import { TeamMemberRole } from '../team/team.model';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
import { UserDeleteData } from 'src/user/user.model';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -139,35 +140,28 @@ export class AdminService {
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async updateUserDisplayName(userUid: string, displayName: string) {
|
||||
const updatedUser = await this.userService.updateUser(userUid, displayName);
|
||||
const updatedUser = await this.userService.updateUserDisplayName(
|
||||
userUid,
|
||||
displayName,
|
||||
);
|
||||
if (E.isLeft(updatedUser)) return E.left(updatedUser.left);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke infra level user invitation
|
||||
* @param inviteeEmail Invitee's email
|
||||
* Revoke infra level user invitations
|
||||
* @param inviteeEmails Invitee's emails
|
||||
* @param adminUid Admin Uid
|
||||
* @returns an Either of array of `InvitedUser` object or error string
|
||||
* @returns an Either of boolean or error string
|
||||
*/
|
||||
async revokeUserInvite(inviteeEmail: string, adminUid: string) {
|
||||
async revokeUserInvites(inviteeEmails: string[], adminUid: string) {
|
||||
try {
|
||||
const deletedInvitee = await this.prisma.invitedUsers.delete({
|
||||
await this.prisma.invitedUsers.deleteMany({
|
||||
where: {
|
||||
inviteeEmail,
|
||||
inviteeEmail: { in: inviteeEmails },
|
||||
},
|
||||
});
|
||||
|
||||
const invitedUser = <InvitedUser>{
|
||||
adminEmail: deletedInvitee.adminEmail,
|
||||
adminUid: deletedInvitee.adminUid,
|
||||
inviteeEmail: deletedInvitee.inviteeEmail,
|
||||
invitedOn: deletedInvitee.invitedOn,
|
||||
};
|
||||
|
||||
this.pubsub.publish(`admin/${adminUid}/invitation_revoked`, invitedUser);
|
||||
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(USER_INVITATION_NOT_FOUND);
|
||||
@@ -434,44 +428,44 @@ export class AdminService {
|
||||
|
||||
/**
|
||||
* Remove user (not Admin) accounts by UIDs
|
||||
* @param userUid User UIDs
|
||||
* @param userUIDs User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUserAccounts(userUIDs: string[]) {
|
||||
const allUsers = await this.userService.findUsersByIds(userUIDs);
|
||||
if (allUsers.length === 0) return E.left(USER_NOT_FOUND);
|
||||
const allUsersList = await this.userService.findUsersByIds(userUIDs);
|
||||
if (allUsersList.length === 0) return E.left(USERS_NOT_FOUND);
|
||||
|
||||
const userDeleteResult: UserDeleteData[] = [];
|
||||
const userDeleteResult: UserDeletionResult[] = [];
|
||||
|
||||
// Admin user can not be deleted without removing admin status/role
|
||||
allUsers.forEach((user) => {
|
||||
allUsersList.forEach((user) => {
|
||||
if (user.isAdmin) {
|
||||
userDeleteResult.push({
|
||||
userUID: user.uid,
|
||||
success: false,
|
||||
isDeleted: false,
|
||||
errorMessage: ADMIN_CAN_NOT_BE_DELETED,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const normalUsers = allUsers.filter((user) => !user.isAdmin);
|
||||
const nonAdminUsers = allUsersList.filter((user) => !user.isAdmin);
|
||||
|
||||
const deletionPromises = normalUsers.map((user) => {
|
||||
const deletionPromises = nonAdminUsers.map((user) => {
|
||||
return this.userService
|
||||
.deleteUserByUID(user)()
|
||||
.then((res) => {
|
||||
if (E.isLeft(res)) {
|
||||
return {
|
||||
userUID: user.uid,
|
||||
success: false,
|
||||
isDeleted: false,
|
||||
errorMessage: res.left,
|
||||
} as UserDeleteData;
|
||||
} as UserDeletionResult;
|
||||
}
|
||||
return {
|
||||
userUID: user.uid,
|
||||
success: true,
|
||||
isDeleted: true,
|
||||
errorMessage: null,
|
||||
} as UserDeleteData;
|
||||
} as UserDeletionResult;
|
||||
});
|
||||
});
|
||||
const promiseResult = await Promise.allSettled(deletionPromises);
|
||||
@@ -526,7 +520,7 @@ export class AdminService {
|
||||
* @param userUIDs User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUsersAsAdmin(userUIDs: string[]) {
|
||||
async demoteUsersByAdmin(userUIDs: string[]) {
|
||||
const adminUsers = await this.userService.fetchAdminUsers();
|
||||
|
||||
const remainingAdmins = adminUsers.filter(
|
||||
|
||||
@@ -31,7 +31,7 @@ import { Shortcode } from 'src/shortcode/shortcode.model';
|
||||
// A custom message type that defines the topic and the corresponding payload.
|
||||
// For every module that publishes a subscription add its type def and the possible subscription type.
|
||||
export type TopicDef = {
|
||||
[topic: `admin/${string}/${'invited' | 'invitation_revoked'}`]: InvitedUser;
|
||||
[topic: `admin/${string}/${'invited'}`]: InvitedUser;
|
||||
[topic: `user/${string}/${'updated' | 'deleted'}`]: User;
|
||||
[topic: `user_settings/${string}/${'created' | 'updated'}`]: UserSettings;
|
||||
[
|
||||
|
||||
@@ -58,7 +58,7 @@ registerEnumType(SessionType, {
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class UserDeleteData {
|
||||
export class UserDeletionResult {
|
||||
@Field(() => ID, {
|
||||
description: 'UID of the user',
|
||||
})
|
||||
@@ -67,7 +67,7 @@ export class UserDeleteData {
|
||||
@Field(() => Boolean, {
|
||||
description: 'Flag to determine if user deletion was successful or not',
|
||||
})
|
||||
success: Boolean;
|
||||
isDeleted: Boolean;
|
||||
|
||||
@Field({
|
||||
nullable: true,
|
||||
|
||||
@@ -438,7 +438,7 @@ describe('UserService', () => {
|
||||
test('should resolve right and update user', async () => {
|
||||
mockPrisma.user.update.mockResolvedValueOnce(user);
|
||||
|
||||
const result = await userService.updateUser(user.uid, user.displayName);
|
||||
const result = await userService.updateUserDisplayName(user.uid, user.displayName);
|
||||
expect(result).toEqualRight({
|
||||
...user,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
@@ -448,7 +448,7 @@ describe('UserService', () => {
|
||||
test('should resolve right and publish user updated subscription', async () => {
|
||||
mockPrisma.user.update.mockResolvedValueOnce(user);
|
||||
|
||||
await userService.updateUser(user.uid, user.displayName);
|
||||
await userService.updateUserDisplayName(user.uid, user.displayName);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`user/${user.uid}/updated`,
|
||||
{
|
||||
@@ -461,7 +461,7 @@ describe('UserService', () => {
|
||||
test('should resolve left and error when invalid user uid is passed', async () => {
|
||||
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
||||
|
||||
const result = await userService.updateUser(
|
||||
const result = await userService.updateUserDisplayName(
|
||||
'invalidUserUid',
|
||||
user.displayName,
|
||||
);
|
||||
|
||||
@@ -90,9 +90,9 @@ export class UserService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Non-Admin Users with given IDs
|
||||
* Find users with given IDs
|
||||
* @param userUIDs User IDs
|
||||
* @returns Option of found Users
|
||||
* @returns Array of found Users
|
||||
*/
|
||||
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
|
||||
const users = await this.prisma.user.findMany({
|
||||
@@ -290,7 +290,7 @@ export class UserService {
|
||||
* @param displayName User's displayName
|
||||
* @returns a Either of User or error
|
||||
*/
|
||||
async updateUser(userUID: string, displayName: string) {
|
||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
||||
try {
|
||||
const dbUpdatedUser = await this.prisma.user.update({
|
||||
where: { uid: userUID },
|
||||
|
||||
Reference in New Issue
Block a user