feat: admin user management (backend) (#3786)
This commit is contained in:
@@ -56,3 +56,22 @@ export enum SessionType {
|
||||
registerEnumType(SessionType, {
|
||||
name: 'SessionType',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class UserDeletionResult {
|
||||
@Field(() => ID, {
|
||||
description: 'UID of the user',
|
||||
})
|
||||
userUID: string;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Flag to determine if user deletion was successful or not',
|
||||
})
|
||||
isDeleted: Boolean;
|
||||
|
||||
@Field({
|
||||
nullable: true,
|
||||
description: 'Error message if user deletion was not successful',
|
||||
})
|
||||
errorMessage: String;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSON_INVALID, USER_NOT_FOUND } from 'src/errors';
|
||||
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
@@ -176,6 +176,26 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUsersByIds', () => {
|
||||
test('should successfully return users given valid user UIDs', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.findUsersByIds([
|
||||
'123344',
|
||||
'5555',
|
||||
'6666',
|
||||
]);
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
|
||||
test('should return empty array of users given a invalid user UIDs', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userService.findUsersByIds(['sdcvbdbr']);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUserViaMagicLink', () => {
|
||||
test('should successfully create user and account for magic-link given valid inputs', async () => {
|
||||
mockPrisma.user.create.mockResolvedValueOnce(user);
|
||||
@@ -414,6 +434,54 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserDisplayName', () => {
|
||||
test('should resolve right and update user display name', async () => {
|
||||
const newDisplayName = 'New Name';
|
||||
mockPrisma.user.update.mockResolvedValueOnce({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
});
|
||||
|
||||
const result = await userService.updateUserDisplayName(
|
||||
user.uid,
|
||||
newDisplayName,
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
||||
});
|
||||
});
|
||||
test('should resolve right and publish user updated subscription', async () => {
|
||||
const newDisplayName = 'New Name';
|
||||
mockPrisma.user.update.mockResolvedValueOnce({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
});
|
||||
|
||||
await userService.updateUserDisplayName(user.uid, user.displayName);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`user/${user.uid}/updated`,
|
||||
{
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
||||
},
|
||||
);
|
||||
});
|
||||
test('should resolve left and error when invalid user uid is passed', async () => {
|
||||
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
||||
|
||||
const result = await userService.updateUserDisplayName(
|
||||
'invalidUserUid',
|
||||
user.displayName,
|
||||
);
|
||||
expect(result).toEqualLeft(USER_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllUsers', () => {
|
||||
test('should resolve right and return 20 users when cursor is null', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
@@ -435,6 +503,36 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllUsersV2', () => {
|
||||
test('should resolve right and return first 20 users when searchString is null', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.fetchAllUsersV2(null, {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
test('should resolve right and return next 20 users when searchString is provided', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.fetchAllUsersV2('.com', {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
test('should resolve left and return an empty array when users not found', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userService.fetchAllUsersV2('Unknown entry', {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAdminUsers', () => {
|
||||
test('should return a list of admin users', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
|
||||
@@ -556,4 +654,17 @@ describe('UserService', () => {
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUsersAsAdmin', () => {
|
||||
test('should resolve right and return true for valid user UIDs', async () => {
|
||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 1 });
|
||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
test('should resolve right and return false for invalid user UIDs', async () => {
|
||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 0 });
|
||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
||||
expect(result).toEqualLeft(USERS_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,14 @@ import * as T from 'fp-ts/Task';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { pipe, constVoid } from 'fp-ts/function';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { USER_NOT_FOUND } from 'src/errors';
|
||||
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import { SessionType, User } from './user.model';
|
||||
import { USER_UPDATE_FAILED } from 'src/errors';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
||||
import { UserDataHandler } from './user.data.handler';
|
||||
import { User as DbUser } from '@prisma/client';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -88,6 +89,20 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users with given IDs
|
||||
* @param userUIDs User IDs
|
||||
* @returns Array of found Users
|
||||
*/
|
||||
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: {
|
||||
uid: { in: userUIDs },
|
||||
},
|
||||
});
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update User with new generated hashed refresh token
|
||||
*
|
||||
@@ -269,6 +284,30 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's data
|
||||
* @param userUID User UID
|
||||
* @param displayName User's displayName
|
||||
* @returns a Either of User or error
|
||||
*/
|
||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
||||
try {
|
||||
const dbUpdatedUser = await this.prisma.user.update({
|
||||
where: { uid: userUID },
|
||||
data: { displayName },
|
||||
});
|
||||
|
||||
const updatedUser = this.convertDbUserToUser(dbUpdatedUser);
|
||||
|
||||
// Publish subscription for user updates
|
||||
await this.pubsub.publish(`user/${updatedUser.uid}/updated`, updatedUser);
|
||||
|
||||
return E.right(updatedUser);
|
||||
} catch (error) {
|
||||
return E.left(USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse currentRESTSession and currentGQLSession
|
||||
* @param sessionData string of the session
|
||||
@@ -286,6 +325,7 @@ export class UserService {
|
||||
* @param cursorID string of userUID or null
|
||||
* @param take number of users to query
|
||||
* @returns an array of `User` object
|
||||
* @deprecated use fetchAllUsersV2 instead
|
||||
*/
|
||||
async fetchAllUsers(cursorID: string, take: number) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
@@ -296,6 +336,43 @@ export class UserService {
|
||||
return fetchedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the users in the `User` table based on cursor
|
||||
* @param searchString search on user's displayName or email
|
||||
* @param paginationOption pagination options
|
||||
* @returns an array of `User` object
|
||||
*/
|
||||
async fetchAllUsersV2(
|
||||
searchString: string,
|
||||
paginationOption: OffsetPaginationArgs,
|
||||
) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
skip: paginationOption.skip,
|
||||
take: paginationOption.take,
|
||||
where: searchString
|
||||
? {
|
||||
OR: [
|
||||
{
|
||||
displayName: {
|
||||
contains: searchString,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
contains: searchString,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
orderBy: [{ isAdmin: 'desc' }, { displayName: 'asc' }],
|
||||
});
|
||||
|
||||
return fetchedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the number of users in db
|
||||
* @returns a count (Int) of user records in DB
|
||||
@@ -326,6 +403,23 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change users to admins by toggling isAdmin param to true
|
||||
* @param userUID user UIDs
|
||||
* @returns a Either of true or error
|
||||
*/
|
||||
async makeAdmins(userUIDs: string[]) {
|
||||
try {
|
||||
await this.prisma.user.updateMany({
|
||||
where: { uid: { in: userUIDs } },
|
||||
data: { isAdmin: true },
|
||||
});
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(USER_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the admin users
|
||||
* @returns an array of admin users
|
||||
@@ -444,4 +538,22 @@ export class UserService {
|
||||
return E.left(USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change users from an admin by toggling isAdmin param to false
|
||||
* @param userUIDs user UIDs
|
||||
* @returns a Either of true or error
|
||||
*/
|
||||
async removeUsersAsAdmin(userUIDs: string[]) {
|
||||
const data = await this.prisma.user.updateMany({
|
||||
where: { uid: { in: userUIDs } },
|
||||
data: { isAdmin: false },
|
||||
});
|
||||
|
||||
if (data.count === 0) {
|
||||
return E.left(USERS_NOT_FOUND);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user