Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refresh invite link #664

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/modules/invite/invite.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
HttpException,
HttpStatus,
NotFoundException,
Post,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { InviteService } from './invite.service';
import { isUUID } from 'class-validator';
import { CustomHttpException } from '../../helpers/custom-http-filter';

@ApiBearerAuth()
@ApiTags('Organisation Invites')
Expand Down Expand Up @@ -41,4 +44,29 @@ export class InviteController {
}
}
}

@Post(':orgId/invite/:inviteId/refresh')
async refreshInvitation(@Param('orgId') orgId: string, @Param('inviteId') inviteId: string) {
if (isUUID(orgId, 4)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation is best handled in the controllers. Service layer is for your bussiness logic

throw new CustomHttpException(
{
status_code: 400,
error: 'Bad Request',
message: 'Invalid organisation ID format',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use constants where possible to make code cleaner and maintainable. Hardcoded strings just leaves room for errors

},
400
);
} else if (isUUID(inviteId, 4)) {
throw new CustomHttpException(
{
status_code: 400,
error: 'Bad Request',
message: 'Invalid organisation ID format',
},
400
);
}

return await this.inviteService.refreshInvite(orgId, inviteId);
}
}
71 changes: 69 additions & 2 deletions src/modules/invite/invite.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import {
ForbiddenException,
HttpStatus,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { InviteDto } from './dto/invite.dto';
import { Invite } from './entities/invite.entity';
import { Organisation } from '../../modules/organisations/entities/organisations.entity';
import { Repository } from 'typeorm';
import { Not, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { v4 as uuidv4 } from 'uuid';
import { CustomHttpException } from '../../helpers/custom-http-filter';

@Injectable()
export class InviteService {
Expand Down Expand Up @@ -66,4 +73,64 @@ export class InviteService {

return responseData;
}

async refreshInvite(orgId: string, inviteId) {
const organisation = await this.organisationRepository.findOne({ where: { id: orgId } });

if (!organisation) {
throw new CustomHttpException(
{
status_code: 404,
error: 'Not Found',
message: `The organization with ID ${orgId} does not exist`,
},
HttpStatus.NOT_FOUND
);
}

const invitation = await this.inviteRepository.findOne({
where: {
id: inviteId,
organisation: organisation,
},
});

if (!invitation) {
throw new CustomHttpException(
{
status_code: 404,
error: 'Not Found',
message: `The invitation with ID ${inviteId} does not exist in this organization`,
},
HttpStatus.NOT_FOUND
);
}

if (invitation.isAccepted) {
throw new CustomHttpException(
{
status_code: 403,
message: `The invitation cannot be refreshed because it has been accepted`,
},
HttpStatus.FORBIDDEN
);
}

const newToken = uuidv4();

const updatedInvitation = this.inviteRepository.save({
...invitation,
token: newToken,
});
const link = `${process.env.FRONTEND_URL}/invite?token=${newToken}`;

return {
status_code: 200,
data: {
...updatedInvitation,
link,
},
message: 'Invitation link refreshed successfully',
};
}
}
54 changes: 53 additions & 1 deletion src/modules/invite/tests/invite.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { HttpException, HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
Expand Down Expand Up @@ -92,4 +92,56 @@ describe('InviteService', () => {
await expect(service.createInvite('1')).rejects.toThrow(NotFoundException);
});
});

describe('refreshInvite', () => {
it('should refresh an invitation link successfully', async () => {
const orgId = uuidv4();
const inviteId = uuidv4();
const organisation = { id: orgId };
const invitation = { id: inviteId, isAccepted: false };
const newToken = uuidv4();
const updatedInvitation = { ...invitation, token: newToken };

jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any);
jest.spyOn(repository, 'findOne').mockResolvedValue(invitation as any);
jest.spyOn(repository, 'save').mockResolvedValue(updatedInvitation as any);
(uuidv4 as jest.Mock).mockReturnValue(newToken);

const result = await service.refreshInvite(orgId, inviteId);

expect(result).toHaveProperty('data');
});

it('should throw an exception if the organisation does not exist', async () => {
const orgId = uuidv4();
const inviteId = uuidv4();

jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(null);

await expect(service.refreshInvite(orgId, inviteId)).rejects.toThrow(HttpException);
});

it('should throw an exception if the invitation does not exist', async () => {
const orgId = uuidv4();
const inviteId = uuidv4();
const organisation = { id: orgId };

jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any);
jest.spyOn(repository, 'findOne').mockResolvedValue(null);

await expect(service.refreshInvite(orgId, inviteId)).rejects.toThrow(HttpException);
});

it('should throw an exception if the invitation has already been accepted', async () => {
const orgId = uuidv4();
const inviteId = uuidv4();
const organisation = { id: orgId };
const invitation = { id: inviteId, isAccepted: true };

jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any);
jest.spyOn(repository, 'findOne').mockResolvedValue(invitation as any);

await expect(service.refreshInvite(orgId, inviteId)).rejects.toThrow(HttpException);
});
});
});
Loading