Compare commits
5 Commits
feat/cli-a
...
feat/user-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3387d06e3 | ||
|
|
ac0f3babbb | ||
|
|
20e8b50524 | ||
|
|
5fc9b5660a | ||
|
|
b9c36faaa4 |
@@ -269,6 +269,25 @@ export class AdminResolver {
|
|||||||
return invitedUser.right;
|
return invitedUser.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, { description: 'Revoke a user invite by ID' })
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async revokeUserInvitationByAdmin(
|
||||||
|
@GqlAdmin() adminUser: Admin,
|
||||||
|
@Args({
|
||||||
|
name: 'inviteeEmail',
|
||||||
|
description: 'Invite Email',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
inviteeEmail: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const invite = await this.adminService.revokeUserInvite(
|
||||||
|
inviteeEmail,
|
||||||
|
adminUser.uid,
|
||||||
|
);
|
||||||
|
if (E.isLeft(invite)) throwErr(invite.left);
|
||||||
|
return invite.right;
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Delete an user account from infra',
|
description: 'Delete an user account from infra',
|
||||||
})
|
})
|
||||||
@@ -471,4 +490,14 @@ export class AdminResolver {
|
|||||||
userInvited(@GqlUser() admin: AuthUser) {
|
userInvited(@GqlUser() admin: AuthUser) {
|
||||||
return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`);
|
return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscription(() => InvitedUser, {
|
||||||
|
description: 'Listen for User Revocation',
|
||||||
|
resolve: (value) => value,
|
||||||
|
})
|
||||||
|
@SkipThrottle()
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
userRevoked(@GqlUser() admin: AuthUser) {
|
||||||
|
return this.pubsub.asyncIterator(`admin/${admin.uid}/revoked`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { mockDeep } from 'jest-mock-extended';
|
import { mockDeep } from 'jest-mock-extended';
|
||||||
import { InvitedUsers } from '@prisma/client';
|
import { InvitedUsers as DbInvitedUser } from '@prisma/client';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { TeamService } from '../team/team.service';
|
import { TeamService } from '../team/team.service';
|
||||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
DUPLICATE_EMAIL,
|
DUPLICATE_EMAIL,
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
|
USER_NOT_INVITED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { InvitedUser } from './invited-user.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -44,7 +46,7 @@ const adminService = new AdminService(
|
|||||||
mockConfigService,
|
mockConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const invitedUsers: InvitedUsers[] = [
|
const invitedUsers: DbInvitedUser[] = [
|
||||||
{
|
{
|
||||||
adminUid: 'uid1',
|
adminUid: 'uid1',
|
||||||
adminEmail: 'admin1@example.com',
|
adminEmail: 'admin1@example.com',
|
||||||
@@ -64,9 +66,19 @@ describe('AdminService', () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
mockPrisma.user.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const expectedResults: InvitedUser[] = invitedUsers.map(
|
||||||
|
(invitedUser) => ({
|
||||||
|
...invitedUser,
|
||||||
|
isInvitationAccepted: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const results = await adminService.fetchInvitedUsers();
|
const results = await adminService.fetchInvitedUsers();
|
||||||
expect(results).toEqual(invitedUsers);
|
expect(results).toEqual(expectedResults);
|
||||||
});
|
});
|
||||||
test('should resolve left and return an empty array if invited users not found', async () => {
|
test('should resolve left and return an empty array if invited users not found', async () => {
|
||||||
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
||||||
@@ -76,6 +88,51 @@ describe('AdminService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('revokeUserInvite', () => {
|
||||||
|
test('should resolve left and return error if email not invited', async () => {
|
||||||
|
mockPrisma.invitedUsers.delete.mockRejectedValueOnce(new Error());
|
||||||
|
|
||||||
|
const result = await adminService.revokeUserInvite(
|
||||||
|
'test@gmail.com',
|
||||||
|
'adminUid',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(USER_NOT_INVITED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve right and return deleted invitee email', async () => {
|
||||||
|
const adminUid = 'adminUid';
|
||||||
|
mockPrisma.invitedUsers.delete.mockResolvedValueOnce(invitedUsers[0]);
|
||||||
|
|
||||||
|
const result = await adminService.revokeUserInvite(
|
||||||
|
invitedUsers[0].inviteeEmail,
|
||||||
|
adminUid,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockPrisma.invitedUsers.delete).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
inviteeEmail: invitedUsers[0].inviteeEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve right, delete invitee email and publish a subscription', async () => {
|
||||||
|
const adminUid = 'adminUid';
|
||||||
|
mockPrisma.invitedUsers.delete.mockResolvedValueOnce(invitedUsers[0]);
|
||||||
|
|
||||||
|
await adminService.revokeUserInvite(
|
||||||
|
invitedUsers[0].inviteeEmail,
|
||||||
|
adminUid,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
|
`admin/${adminUid}/revoked`,
|
||||||
|
invitedUsers[0],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('inviteUserToSignInViaEmail', () => {
|
describe('inviteUserToSignInViaEmail', () => {
|
||||||
test('should resolve right and create a invited user', async () => {
|
test('should resolve right and create a invited user', async () => {
|
||||||
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);
|
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import {
|
|||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
|
USER_NOT_INVITED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { MailerService } from '../mailer/mailer.service';
|
import { MailerService } from '../mailer/mailer.service';
|
||||||
import { InvitedUser } from './invited-user.model';
|
import { InvitedUser } from './invited-user.model';
|
||||||
@@ -26,6 +26,7 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
|||||||
import { TeamMemberRole } from '../team/team.model';
|
import { TeamMemberRole } from '../team/team.model';
|
||||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Admin } from './admin.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@@ -110,18 +111,69 @@ export class AdminService {
|
|||||||
return E.right(invitedUser);
|
return E.right(invitedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke the invitation of a user to join infra.
|
||||||
|
* @param inviteeEmail Invitee's email
|
||||||
|
* @param adminUser Admin object
|
||||||
|
* @returns an Either of array of `InvitedUser` object or error string
|
||||||
|
*/
|
||||||
|
async revokeUserInvite(inviteeEmail: string, adminUid: string) {
|
||||||
|
try {
|
||||||
|
const deletedInvitee = await this.prisma.invitedUsers.delete({
|
||||||
|
where: {
|
||||||
|
inviteeEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitedUser = <InvitedUser>{
|
||||||
|
adminEmail: deletedInvitee.adminEmail,
|
||||||
|
adminUid: deletedInvitee.adminUid,
|
||||||
|
inviteeEmail: deletedInvitee.inviteeEmail,
|
||||||
|
invitedOn: deletedInvitee.invitedOn,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pubsub.publish(`admin/${adminUid}/revoked`, invitedUser);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(USER_NOT_INVITED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the list of invited users by the admin.
|
* Fetch the list of invited users by the admin.
|
||||||
* @returns an Either of array of `InvitedUser` object or error
|
* @returns an Either of array of `InvitedUser` object or error
|
||||||
*/
|
*/
|
||||||
async fetchInvitedUsers() {
|
async fetchInvitedUsers() {
|
||||||
const invitedUsers = await this.prisma.invitedUsers.findMany();
|
const dbInvitedUsers = await this.prisma.invitedUsers.findMany();
|
||||||
|
|
||||||
const users: InvitedUser[] = invitedUsers.map(
|
const invitationAcceptedUsers = await this.prisma.user.findMany({
|
||||||
(user) => <InvitedUser>{ ...user },
|
where: {
|
||||||
);
|
email: {
|
||||||
|
in: dbInvitedUsers.map((user) => user.inviteeEmail),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return users;
|
let invitedUsers: InvitedUser[] = [];
|
||||||
|
|
||||||
|
dbInvitedUsers.forEach((dbInvitedUser) => {
|
||||||
|
const isUserAccepts = invitationAcceptedUsers.find(
|
||||||
|
(user) => user.email === dbInvitedUser.inviteeEmail,
|
||||||
|
);
|
||||||
|
|
||||||
|
const invitedUser: InvitedUser = {
|
||||||
|
adminEmail: dbInvitedUser.adminEmail,
|
||||||
|
adminUid: dbInvitedUser.adminUid,
|
||||||
|
inviteeEmail: dbInvitedUser.inviteeEmail,
|
||||||
|
invitedOn: dbInvitedUser.invitedOn,
|
||||||
|
isInvitationAccepted: isUserAccepts ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
|
invitedUsers.push(invitedUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
return invitedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,4 +21,10 @@ export class InvitedUser {
|
|||||||
description: 'Date when the user invitation was sent',
|
description: 'Date when the user invitation was sent',
|
||||||
})
|
})
|
||||||
invitedOn: Date;
|
invitedOn: Date;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'Boolean status if invitation was accepted or not',
|
||||||
|
defaultValue: false,
|
||||||
|
})
|
||||||
|
isInvitationAccepted: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ export const USER_FB_DOCUMENT_DELETION_FAILED =
|
|||||||
*/
|
*/
|
||||||
export const USER_NOT_FOUND = 'user/not_found' as const;
|
export const USER_NOT_FOUND = 'user/not_found' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User is not invited by admin
|
||||||
|
*/
|
||||||
|
export const USER_NOT_INVITED = 'admin/user_not_invited' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User is already invited by admin
|
* User is already invited by admin
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { Shortcode } from 'src/shortcode/shortcode.model';
|
|||||||
// A custom message type that defines the topic and the corresponding payload.
|
// 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.
|
// For every module that publishes a subscription add its type def and the possible subscription type.
|
||||||
export type TopicDef = {
|
export type TopicDef = {
|
||||||
[topic: `admin/${string}/${'invited'}`]: InvitedUser;
|
[topic: `admin/${string}/${'invited' | 'revoked'}`]: InvitedUser;
|
||||||
[topic: `user/${string}/${'updated' | 'deleted'}`]: User;
|
[topic: `user/${string}/${'updated' | 'deleted'}`]: User;
|
||||||
[topic: `user_settings/${string}/${'created' | 'updated'}`]: UserSettings;
|
[topic: `user_settings/${string}/${'created' | 'updated'}`]: UserSettings;
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user