Skip to content

Commit

Permalink
Merge pull request #20 from vishnuvinay89/all-saas-0.2-dev
Browse files Browse the repository at this point in the history
TaskId #234721 task : Create 'Send invitation API' with invitation module.
  • Loading branch information
sudeeppr1998 authored Feb 7, 2025
2 parents 5f8ac45 + 6d58570 commit 5954fe1
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/adapters/postgres/postgres-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PostgresFieldsService } from "./fields-adapter";
import { Cohort } from "src/cohort/entities/cohort.entity";
import { UserTenantMapping } from "src/userTenantMapping/entities/user-tenant-mapping.entity";
import { Tenants } from "src/userTenantMapping/entities/tenant.entity";
import { Invitations } from "src/invitation/entities/invitation.entity";
import { UserRoleMapping } from "src/rbac/assign-role/entities/assign-role.entity";
import { Role } from "src/rbac/role/entities/role.entity";
import { PostgresRoleService } from "./rbac/role-adapter";
Expand All @@ -35,6 +36,7 @@ import { JwtService } from "@nestjs/jwt";
Cohort,
UserTenantMapping,
Tenants,
Invitations,
UserRoleMapping,
Role,
RolePrivilegeMapping,
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { CoursePlannerModule } from './course-planner/course-planner.module';
import { HttpService } from "@utils/http-service";
import { TenantModule } from "./tenant/tenant.module";
import { AcademicyearsModule } from './academicyears/academicyears.module';
import { InvitationModule } from './invitation/invitation.module';

@Module({
imports: [
Expand All @@ -45,6 +46,7 @@ import { AcademicyearsModule } from './academicyears/academicyears.module';
CoursePlannerModule,
TenantModule,
AcademicyearsModule,
InvitationModule,
],
controllers: [AppController],
providers: [AppService,HttpService]
Expand Down
3 changes: 2 additions & 1 deletion src/common/utils/api-id.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ export const APIID = {
TENANT_LIST: "api.tenant.list",
ACADEMICYEAR_CREATE: 'api.academicyear.create',
ACADEMICYEAR_LIST: 'api.academicyear.list',
ACADEMICYEAR_GET: 'api.academicyear.get'
ACADEMICYEAR_GET: 'api.academicyear.get',
SEND_INVITATION:'api.invitation.send'
}
3 changes: 3 additions & 0 deletions src/common/utils/response.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export const API_RESPONSES = {
TENANT_GET: 'Tenant fetched successfully.',
TENANT_NOT_FOUND: 'Tenant does not exist',
CONFLICT: 'Conflict detected',
INVITATION_SUCCESS: 'Invitation sent successfully',
INVITEDUSER_CONFLICT: (admin)=>`Invited user is already a ${admin} of invited cohort`,
TENANT_EXISTS: 'Tenant already exists',
TENANT_NAME_EXISTS :(name)=>`Tenant with name ${name} already exists. Cannot Update`,
TENANT_CREATE: 'Tenant created successfully',
TENANT_UPDATE: 'Tenant updated successfully',
TENANT_DELETE: 'Tenant deleted successfully',
Expand Down
23 changes: 23 additions & 0 deletions src/invitation/dto/create-invitation.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Expose } from 'class-transformer';
import { IsUUID, IsEmail, IsNotEmpty, isNotEmpty, IsDefined, IsOptional } from 'class-validator';

export class CreateInvitationDto {
@IsUUID()
@IsNotEmpty({message : "tenantId is required"})
@Expose()
tenantId: string;

@IsUUID()
@IsNotEmpty({message : "cohortId is required"})
@Expose()
cohortId: string;

@IsEmail()
@IsNotEmpty()
invitedTo: string;

@IsEmail()
@IsOptional()
invitedBy: string;

}
45 changes: 45 additions & 0 deletions src/invitation/entities/invitation.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
Timestamp,
UpdateDateColumn,
} from "typeorm";

@Entity("Invitations")
export class Invitations {
@PrimaryGeneratedColumn("uuid")
invitationId: string;

@Column()
tenantId: string;

@Column()
cohortId: string;

@Column()
invitedTo: string;

@Column()
invitedBy: string;

@Column({
type: "enum",
enum: ["Pending", "Accepted", "Rejected"],
default: "Pending",
})
invitationStatus: "Pending" | "Accepted" | "Rejected";

@CreateDateColumn({
type: "timestamp with time zone",
default: () => "CURRENT_TIMESTAMP",
})
sentAt: Date;

@UpdateDateColumn({
type: "timestamp with time zone",
default: () => "CURRENT_TIMESTAMP",
})
updatedAt: Date;
}
18 changes: 18 additions & 0 deletions src/invitation/invitation.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InvitationController } from './invitation.controller';

describe('InvitationController', () => {
let controller: InvitationController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [InvitationController],
}).compile();

controller = module.get<InvitationController>(InvitationController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
29 changes: 29 additions & 0 deletions src/invitation/invitation.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Body, Controller, Post, Req, Res, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
import { InvitationService } from './invitation.service';
import { ApiBadRequestResponse, ApiBody, ApiCreatedResponse, ApiForbiddenResponse } from '@nestjs/swagger';
import { CreateInvitationDto } from './dto/create-invitation.dto';
import { Request, Response } from 'express';
import { JwtAuthGuard } from 'src/common/guards/keycloak.guard';

@Controller('invitation')
@UseGuards(JwtAuthGuard)
export class InvitationController {
constructor (
private invitationService: InvitationService,
) {}
@Post("/sendinvite")
@ApiBody({type :CreateInvitationDto})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiCreatedResponse({ description: "Invite Send Successfully" })
@ApiForbiddenResponse({ description: "Forbidden" })
@ApiBadRequestResponse({ description: "Bad request." })

public async sendInvite(
@Req() request: Request,
@Res() response: Response,
@Body() createInvitationDto: CreateInvitationDto,
) {
return await this.invitationService.sendInvite(request, createInvitationDto, response);
}

}
27 changes: 27 additions & 0 deletions src/invitation/invitation.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { InvitationController } from './invitation.controller';
import { InvitationService } from './invitation.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostgresModule } from 'src/adapters/postgres/postgres-module';
import { RolePrivilegeMapping } from 'src/rbac/assign-privilege/entities/assign-privilege.entity';
import { UserRoleMapping } from 'src/rbac/assign-role/entities/assign-role.entity';
import { Role } from 'src/rbac/role/entities/role.entity';
import { UserTenantMapping } from 'src/userTenantMapping/entities/user-tenant-mapping.entity';
import { User } from 'src/user/entities/user-entity'
import { Cohort } from 'src/cohort/entities/cohort.entity';
import { Tenants } from 'src/userTenantMapping/entities/tenant.entity';
import { Invitations } from './entities/invitation.entity';
import { PostgresRoleService } from 'src/adapters/postgres/rbac/role-adapter';
import { PostgresAssignPrivilegeService } from 'src/adapters/postgres/rbac/privilegerole.adapter';
import { CohortMembers } from 'src/cohortMembers/entities/cohort-member.entity';
import { PostgresUserService } from 'src/adapters/postgres/user-adapter';

@Module({
imports: [
TypeOrmModule.forFeature([Invitations,UserRoleMapping,UserTenantMapping,Role,RolePrivilegeMapping,User,Cohort,Tenants,CohortMembers]),
PostgresModule
],
controllers: [InvitationController],
providers: [InvitationService,PostgresRoleService,PostgresAssignPrivilegeService]
})
export class InvitationModule {}
18 changes: 18 additions & 0 deletions src/invitation/invitation.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InvitationService } from './invitation.service';

describe('InvitationService', () => {
let service: InvitationService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [InvitationService],
}).compile();

service = module.get<InvitationService>(InvitationService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
161 changes: 161 additions & 0 deletions src/invitation/invitation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import jwt_decode from "jwt-decode";
import APIResponse from "src/common/responses/response";
import { PostgresAssignPrivilegeService } from 'src/adapters/postgres/rbac/privilegerole.adapter';
import { PostgresRoleService } from 'src/adapters/postgres/rbac/role-adapter';
import { PostgresUserService } from 'src/adapters/postgres/user-adapter';
import { Cohort } from 'src/cohort/entities/cohort.entity';
import { UserRoleMapping } from 'src/rbac/assign-role/entities/assign-role.entity';
import { User } from 'src/user/entities/user-entity';
import { Tenants } from 'src/userTenantMapping/entities/tenant.entity';
import { UserTenantMapping } from 'src/userTenantMapping/entities/user-tenant-mapping.entity';
import { Repository } from 'typeorm';
import { API_RESPONSES } from '@utils/response.messages';
import { Invitations } from './entities/invitation.entity';
import { CohortMembers } from 'src/cohortMembers/entities/cohort-member.entity';
import { APIID } from '@utils/api-id.config';
@Injectable()
export class InvitationService {
constructor(
@InjectRepository(UserTenantMapping)
private UserTenantMappingRepository: Repository<UserTenantMapping>,
@InjectRepository(Invitations)
public invitationsRepository: Repository<Invitations>,
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(CohortMembers)
private cohortMembersRepository: Repository<CohortMembers>,
@InjectRepository(Cohort)
private cohortRepository: Repository<Cohort>,
private readonly userService: PostgresUserService,
private roleService: PostgresRoleService,
private rolePrivilegeService: PostgresAssignPrivilegeService,
) {}
public async sendInvite(request, createInvitationDto, response) {
const apiId =APIID.SEND_INVITATION
try {
const decoded = jwt_decode(request.headers["authorization"]);
createInvitationDto.invitedBy = decoded["email"];

// Check if the tenant-cohort mapping exists
const tenantCohortExist = await this.cohortRepository.findOne({
where: {
tenantId: createInvitationDto.tenantId,
cohortId: createInvitationDto.cohortId,
},
});

if (!tenantCohortExist) {
return APIResponse.error(
response,
apiId,
API_RESPONSES.CONFLICT,
"Tenant and cohort mapping not found",
HttpStatus.CONFLICT
);
}
// check if duplicate request exist with status pending
const checkInvitaionExist = await this.invitationsRepository.findOne({
where: {
tenantId: createInvitationDto.tenantId,
cohortId: createInvitationDto.cohortId,
invitedTo: createInvitationDto.invitedTo,
invitationStatus: "Pending"
},
})
if (checkInvitaionExist) {
return APIResponse.error(
response,
apiId,
API_RESPONSES.CONFLICT,
"Invitation already sent",
HttpStatus.CONFLICT
);
}

// Check if the user exists
const checkUser = await this.usersRepository.findOne({
where: { email: createInvitationDto.invitedTo },
});

if (!checkUser) {
const result = await this.invitationsRepository.save(
createInvitationDto
);
return APIResponse.success(
response,
apiId,
result,
HttpStatus.OK,
API_RESPONSES.INVITATION_SUCCESS
);
}

// Fetch user roles
const userRoles = await this.userService.getUserRoles(checkUser.userId,createInvitationDto.tenantId);

if (!userRoles) {
const result = await this.invitationsRepository.save(
createInvitationDto
);
return APIResponse.success(
response,
apiId,
result,
HttpStatus.OK,
API_RESPONSES.INVITATION_SUCCESS
);
}

// Handle different user roles
if (userRoles.code === "tenant_admin") {
return APIResponse.error(
response,
apiId,
API_RESPONSES.CONFLICT,
API_RESPONSES.INVITEDUSER_CONFLICT('tenant admin'),
HttpStatus.CONFLICT
);
}

if (userRoles.code === "cohort_admin") {
// Check if the user is already mapped to the cohort
const cohortExists = await this.cohortMembersRepository.findOne({
where: {
userId: checkUser.userId,
cohortId: createInvitationDto.cohortId,
},
});

if (cohortExists) {
return APIResponse.error(
response,
apiId,
API_RESPONSES.CONFLICT,
API_RESPONSES.INVITEDUSER_CONFLICT('cohort admin'),
HttpStatus.CONFLICT
);
}
}

// Save invitation if no conflicts
const result = await this.invitationsRepository.save(createInvitationDto);
return APIResponse.success(
response,
apiId,
result,
HttpStatus.OK,
API_RESPONSES.INVITATION_SUCCESS
);
} catch (error) {
return APIResponse.error(
response,
apiId,
API_RESPONSES.INTERNAL_SERVER_ERROR,
error,
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
}

0 comments on commit 5954fe1

Please sign in to comment.