feat: Organization services
This commit is contained in:
@@ -11,6 +11,9 @@ import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ResponseInterceptor } from 'common/interceptors/response.interceptor';
|
||||
import { ExceptionsHandler } from '@nestjs/core/exceptions/exceptions-handler';
|
||||
import { HttpExceptionFilter } from 'common/exceptions/exception-filter';
|
||||
import { OrganizationModule } from './organization/organization.module';
|
||||
import { OrganizationMembershipModule } from './organization-membership/organization-membership.module';
|
||||
import { AuthorizationModule } from './authorization/authorization.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -21,6 +24,9 @@ import { HttpExceptionFilter } from 'common/exceptions/exception-filter';
|
||||
AuthModule,
|
||||
RequestContextModule,
|
||||
PrismaModule,
|
||||
OrganizationModule,
|
||||
OrganizationMembershipModule,
|
||||
AuthorizationModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
} from './dto';
|
||||
import { Response } from 'express';
|
||||
import { DataResponse } from 'common/http';
|
||||
import { Public } from './decorators';
|
||||
|
||||
@Controller('auth')
|
||||
@Public()
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ import { RequestContextModule } from 'core/als/request-context.module';
|
||||
AuthService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useFactory: () => AuthGuard,
|
||||
inject: [Reflector],
|
||||
useClass: AuthGuard,
|
||||
},
|
||||
],
|
||||
controllers: [AuthController],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { RequestContextService } from 'core/als/request-context.service';
|
||||
@@ -10,6 +11,7 @@ import { Request } from 'express';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PUBLIC_KEY } from 'common/keys';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
@@ -30,7 +32,9 @@ export class AuthGuard implements CanActivate {
|
||||
if (!token) throw new UnauthorizedException();
|
||||
|
||||
try {
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(token);
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
|
||||
secret: 'demo',
|
||||
});
|
||||
this.requestContext.set('user', payload);
|
||||
|
||||
return true;
|
||||
|
||||
10
src/authorization/authorization.module.ts
Normal file
10
src/authorization/authorization.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthorizationService } from './authorization.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
providers: [AuthorizationService],
|
||||
imports: [PrismaModule],
|
||||
exports: [AuthorizationService],
|
||||
})
|
||||
export class AuthorizationModule {}
|
||||
18
src/authorization/authorization.service.spec.ts
Normal file
18
src/authorization/authorization.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthorizationService } from './authorization.service';
|
||||
|
||||
describe('AuthorizationService', () => {
|
||||
let service: AuthorizationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthorizationService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthorizationService>(AuthorizationService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
71
src/authorization/authorization.service.ts
Normal file
71
src/authorization/authorization.service.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { USER_ORGANIZATION_OPERATIONS } from './operations';
|
||||
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||
|
||||
@Injectable()
|
||||
export class AuthorizationService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// can perform operation
|
||||
async canPerformOperation(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
operation: USER_ORGANIZATION_OPERATIONS,
|
||||
) {
|
||||
switch (operation) {
|
||||
case USER_ORGANIZATION_OPERATIONS.DELETE_ORGANIZATION:
|
||||
return await this.isOwner(userId, orgId);
|
||||
case USER_ORGANIZATION_OPERATIONS.UPDATE_ORGANIZATION:
|
||||
return await this.isOwner(userId, orgId);
|
||||
case USER_ORGANIZATION_OPERATIONS.INVITE_USERS:
|
||||
return await this.canInvite(userId, orgId);
|
||||
}
|
||||
}
|
||||
|
||||
private async isOwner(userId: string, orgId: string) {
|
||||
const isUserPartOfOrganization = await this.isUserPartOfOrganization(
|
||||
userId,
|
||||
orgId,
|
||||
);
|
||||
|
||||
return isUserPartOfOrganization
|
||||
? isUserPartOfOrganization.role === ORG_ROLE.owner
|
||||
: !!isUserPartOfOrganization;
|
||||
}
|
||||
|
||||
private async canInvite(userId: string, orgId: string) {
|
||||
const isUserPartOfOrganization = await this.isUserPartOfOrganization(
|
||||
userId,
|
||||
orgId,
|
||||
);
|
||||
|
||||
return isUserPartOfOrganization
|
||||
? isUserPartOfOrganization.role === ORG_ROLE.admin ||
|
||||
isUserPartOfOrganization.role === ORG_ROLE.owner
|
||||
: !!isUserPartOfOrganization;
|
||||
}
|
||||
|
||||
private async isAdmin(userId: string, orgId: string) {
|
||||
const isUserPartOfOrganization = await this.isUserPartOfOrganization(
|
||||
userId,
|
||||
orgId,
|
||||
);
|
||||
|
||||
return isUserPartOfOrganization
|
||||
? isUserPartOfOrganization.role === ORG_ROLE.admin
|
||||
: !!isUserPartOfOrganization;
|
||||
}
|
||||
|
||||
// HELPER FUNCTION
|
||||
private async isUserPartOfOrganization(userId: string, orgId: string) {
|
||||
return await this.prisma.organizationUserJoinTable.findUnique({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
orgId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
5
src/authorization/operations.ts
Normal file
5
src/authorization/operations.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum USER_ORGANIZATION_OPERATIONS {
|
||||
UPDATE_ORGANIZATION = 'update_organization',
|
||||
DELETE_ORGANIZATION = 'delete_organization',
|
||||
INVITE_USERS = 'invite_users',
|
||||
}
|
||||
1
src/organization-membership/dto/index.ts
Normal file
1
src/organization-membership/dto/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './invite-to-org.dto';
|
||||
16
src/organization-membership/dto/invite-to-org.dto.ts
Normal file
16
src/organization-membership/dto/invite-to-org.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator';
|
||||
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||
|
||||
export class InviteUserToOrganizationRequestDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
invitedUserId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
orgId: string;
|
||||
|
||||
@IsEnum(ORG_ROLE)
|
||||
@IsNotEmpty()
|
||||
role: ORG_ROLE;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OrganizationMembershipController } from './organization-membership.controller';
|
||||
|
||||
describe('OrganizationMembershipController', () => {
|
||||
let controller: OrganizationMembershipController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [OrganizationMembershipController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<OrganizationMembershipController>(OrganizationMembershipController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller('organization-membership')
|
||||
export class OrganizationMembershipController {}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrganizationMembershipController } from './organization-membership.controller';
|
||||
import { OrganizationMembershipService } from './organization-membership.service';
|
||||
import { OrganizationModule } from 'src/organization/organization.module';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { AuthorizationModule } from 'src/authorization/authorization.module';
|
||||
|
||||
@Module({
|
||||
controllers: [OrganizationMembershipController],
|
||||
providers: [OrganizationMembershipService],
|
||||
imports: [OrganizationModule, UserModule, PrismaModule, AuthorizationModule],
|
||||
})
|
||||
export class OrganizationMembershipModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OrganizationMembershipService } from './organization-membership.service';
|
||||
|
||||
describe('OrganizationMembershipService', () => {
|
||||
let service: OrganizationMembershipService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [OrganizationMembershipService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<OrganizationMembershipService>(OrganizationMembershipService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
148
src/organization-membership/organization-membership.service.ts
Normal file
148
src/organization-membership/organization-membership.service.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { OrganizationService } from 'src/organization/organization.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { InviteUserToOrganizationRequestDTO } from './dto';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import {
|
||||
ORGANIZATION_JOIN_REQUEST,
|
||||
ORGANIZATION_JOIN_REQUEST_TYPE,
|
||||
} from 'prisma/generated/prisma/enums';
|
||||
import { AuthorizationService } from 'src/authorization/authorization.service';
|
||||
import { USER_ORGANIZATION_OPERATIONS } from 'src/authorization/operations';
|
||||
import { Prisma } from 'prisma/generated/prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationMembershipService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly orgService: OrganizationService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly authorization: AuthorizationService,
|
||||
) {}
|
||||
async inviteUserToOrg(
|
||||
userId: string,
|
||||
dto: InviteUserToOrganizationRequestDTO,
|
||||
) {
|
||||
const [orgExists, invitedUser] = await Promise.all([
|
||||
this.orgService.findById(dto.orgId),
|
||||
this.userService.getById(dto.invitedUserId),
|
||||
]);
|
||||
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
if (!invitedUser) throw new NotFoundException('User');
|
||||
|
||||
const userAlreadyPart =
|
||||
await this.prisma.organizationUserJoinTable.findUnique({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
orgId: dto.orgId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (userAlreadyPart)
|
||||
throw new BadRequestException('User already part of this organization');
|
||||
|
||||
const canInviteUser = await this.authorization.canPerformOperation(
|
||||
userId,
|
||||
dto.orgId,
|
||||
USER_ORGANIZATION_OPERATIONS.INVITE_USERS,
|
||||
);
|
||||
if (!canInviteUser) throw new ForbiddenException('Insufficient Permission');
|
||||
|
||||
try {
|
||||
const invitation = await this.prisma.organizationJoinRequest.create({
|
||||
data: {
|
||||
...dto,
|
||||
userId: dto.invitedUserId,
|
||||
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
|
||||
},
|
||||
});
|
||||
|
||||
return invitation;
|
||||
} catch (err) {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (err.code === 'P2002')
|
||||
throw new BadRequestException('User invitation already sent.');
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
requestToJoin() {}
|
||||
|
||||
// TODO: reject, rejectReason
|
||||
async acceptInvite(userId: string, orgId: string) {
|
||||
const orgExists = await this.orgService.findById(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
const [userAlreadyPart, isUserInvited] = await Promise.all([
|
||||
this.prisma.organizationUserJoinTable.findUnique({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
orgId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
this.prisma.organizationJoinRequest.findUnique({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
orgId,
|
||||
userId,
|
||||
},
|
||||
status: ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (userAlreadyPart)
|
||||
throw new BadRequestException('User already part of this organization');
|
||||
if (!isUserInvited)
|
||||
throw new BadRequestException(
|
||||
'User has no invitations from this organization',
|
||||
);
|
||||
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
await tx.organizationJoinRequest.update({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
userId,
|
||||
orgId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: ORGANIZATION_JOIN_REQUEST.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
return await tx.organizationUserJoinTable.create({
|
||||
data: {
|
||||
orgId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getUserInvitations(
|
||||
userId: string,
|
||||
requestType: ORGANIZATION_JOIN_REQUEST_TYPE,
|
||||
status: ORGANIZATION_JOIN_REQUEST = ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||
) {
|
||||
return await this.prisma.organizationJoinRequest.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
status: status,
|
||||
requestType: requestType,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
2
src/organization/dtos/index.ts
Normal file
2
src/organization/dtos/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './organization.dto';
|
||||
export * from './organization-response.dto';
|
||||
15
src/organization/dtos/organization-response.dto.ts
Normal file
15
src/organization/dtos/organization-response.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Organization } from 'prisma/generated/prisma/client';
|
||||
|
||||
export class OrganizationDTO {
|
||||
readonly name: string;
|
||||
readonly description: string | null;
|
||||
readonly createdAt: Date;
|
||||
|
||||
constructor(organization: Organization) {
|
||||
this.name = organization.name;
|
||||
this.description = organization.description;
|
||||
this.createdAt = organization.createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateNewOrganizationResponseDTO extends OrganizationDTO {}
|
||||
30
src/organization/dtos/organization.dto.ts
Normal file
30
src/organization/dtos/organization.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { AtLeastOneField } from 'common/validators';
|
||||
|
||||
export class CreateNewOrganizationRequestDTO {
|
||||
@ApiProperty({
|
||||
description: "Organization's name",
|
||||
example: 'Lions',
|
||||
type: 'string',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Short description for organization',
|
||||
example: 'A cool organization active for over 10 years.',
|
||||
type: 'string',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@AtLeastOneField({
|
||||
message: 'Provide at least one field to update',
|
||||
})
|
||||
export class UpdateOrganizationRequestDTO extends PartialType(
|
||||
CreateNewOrganizationRequestDTO,
|
||||
) {}
|
||||
18
src/organization/organization.controller.spec.ts
Normal file
18
src/organization/organization.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OrganizationController } from './organization.controller';
|
||||
|
||||
describe('OrganizationController', () => {
|
||||
let controller: OrganizationController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [OrganizationController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<OrganizationController>(OrganizationController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
94
src/organization/organization.controller.ts
Normal file
94
src/organization/organization.controller.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
CreateNewOrganizationRequestDTO,
|
||||
OrganizationDTO,
|
||||
UpdateOrganizationRequestDTO,
|
||||
} from './dtos';
|
||||
import { OrganizationService } from './organization.service';
|
||||
import { RequestContextService } from 'core/als/request-context.service';
|
||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { DataResponse } from 'common/http';
|
||||
|
||||
@Controller('organization')
|
||||
@ApiBearerAuth('access-token')
|
||||
export class OrganizationController {
|
||||
constructor(
|
||||
private readonly orgService: OrganizationService,
|
||||
private readonly requestContext: RequestContextService,
|
||||
) {}
|
||||
|
||||
@Post('')
|
||||
async createNewOrganization(
|
||||
@Body() body: CreateNewOrganizationRequestDTO,
|
||||
): Promise<DataResponse<OrganizationDTO>> {
|
||||
const user = this.requestContext.user;
|
||||
const newOrg = await this.orgService.createNewOrganization(
|
||||
user.userId,
|
||||
body,
|
||||
);
|
||||
|
||||
return new DataResponse<OrganizationDTO>(
|
||||
new OrganizationDTO(newOrg),
|
||||
'Organization created successfully.',
|
||||
);
|
||||
}
|
||||
|
||||
@Get('')
|
||||
async getOrganizations(): Promise<DataResponse<OrganizationDTO[]>> {
|
||||
const organizations = await this.orgService.getOrganizations();
|
||||
|
||||
return new DataResponse(
|
||||
organizations.map((organization) => new OrganizationDTO(organization)),
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getAnOrganization(
|
||||
@Param('id') id: string,
|
||||
): Promise<DataResponse<OrganizationDTO>> {
|
||||
const organization = await this.orgService.getOrganizationById(id);
|
||||
return new DataResponse(new OrganizationDTO(organization));
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async updateAnOrganization(
|
||||
@Param('id') orgId: string,
|
||||
@Body() body: UpdateOrganizationRequestDTO,
|
||||
): Promise<DataResponse<OrganizationDTO>> {
|
||||
const user = this.requestContext.user;
|
||||
const updatedOrg = await this.orgService.updateAnOrganization(
|
||||
user.userId,
|
||||
orgId,
|
||||
body,
|
||||
);
|
||||
|
||||
return new DataResponse<OrganizationDTO>(
|
||||
new OrganizationDTO(updatedOrg),
|
||||
'Organization updated successfully',
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteAnOrganization(
|
||||
@Param('id') orgId: string,
|
||||
): Promise<DataResponse<OrganizationDTO>> {
|
||||
const user = this.requestContext.user;
|
||||
const deletedOrg = await this.orgService.deleteAnOrganization(
|
||||
user.userId,
|
||||
orgId,
|
||||
);
|
||||
|
||||
return new DataResponse<OrganizationDTO>(
|
||||
new OrganizationDTO(deletedOrg),
|
||||
'Organization deleted successfully',
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/organization/organization.module.ts
Normal file
20
src/organization/organization.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrganizationController } from './organization.controller';
|
||||
import { OrganizationService } from './organization.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { RequestContextModule } from 'core/als/request-context.module';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { AuthorizationModule } from 'src/authorization/authorization.module';
|
||||
|
||||
@Module({
|
||||
controllers: [OrganizationController],
|
||||
providers: [OrganizationService],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
RequestContextModule,
|
||||
UserModule,
|
||||
AuthorizationModule,
|
||||
],
|
||||
exports: [OrganizationService],
|
||||
})
|
||||
export class OrganizationModule {}
|
||||
18
src/organization/organization.service.spec.ts
Normal file
18
src/organization/organization.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { OrganizationService } from './organization.service';
|
||||
|
||||
describe('OrganizationService', () => {
|
||||
let service: OrganizationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [OrganizationService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<OrganizationService>(OrganizationService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
120
src/organization/organization.service.ts
Normal file
120
src/organization/organization.service.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
CreateNewOrganizationRequestDTO,
|
||||
UpdateOrganizationRequestDTO,
|
||||
} from './dtos';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { RequestContextService } from 'core/als/request-context.service';
|
||||
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||
import { AuthorizationService } from 'src/authorization/authorization.service';
|
||||
import { USER_ORGANIZATION_OPERATIONS } from 'src/authorization/operations';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
// private readonly reqContext: RequestContextService,
|
||||
private readonly authorization: AuthorizationService,
|
||||
) {}
|
||||
async createNewOrganization(
|
||||
userId: string,
|
||||
dto: CreateNewOrganizationRequestDTO,
|
||||
) {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
const newOrganization = await tx.organization.create({
|
||||
data: dto,
|
||||
});
|
||||
|
||||
await tx.organizationUserJoinTable.create({
|
||||
data: {
|
||||
orgId: newOrganization.id,
|
||||
userId: userId,
|
||||
role: ORG_ROLE.owner,
|
||||
},
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: Pagination
|
||||
async getOrganizations() {
|
||||
return await this.prisma.organization.findMany();
|
||||
}
|
||||
|
||||
async getOrganizationById(orgId: string) {
|
||||
const orgExists = await this.findById(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
return orgExists;
|
||||
}
|
||||
|
||||
async updateAnOrganization(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
dto: UpdateOrganizationRequestDTO,
|
||||
) {
|
||||
const orgExists = await this.findById(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
const canUserUpdateOrganization =
|
||||
await this.authorization.canPerformOperation(
|
||||
userId,
|
||||
orgId,
|
||||
USER_ORGANIZATION_OPERATIONS.UPDATE_ORGANIZATION,
|
||||
);
|
||||
|
||||
if (!canUserUpdateOrganization)
|
||||
throw new ForbiddenException('Not enough permission');
|
||||
|
||||
return this.prisma.organization.update({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
data: dto,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Either empty or choose someone to be owner
|
||||
async deleteAnOrganization(userId: string, orgId: string) {
|
||||
const orgExists = await this.findById(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
const canUserDeleteOrganization =
|
||||
await this.authorization.canPerformOperation(
|
||||
userId,
|
||||
orgId,
|
||||
USER_ORGANIZATION_OPERATIONS.DELETE_ORGANIZATION,
|
||||
);
|
||||
|
||||
if (!canUserDeleteOrganization)
|
||||
throw new ForbiddenException('Not enough permission');
|
||||
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
const deletedOrganization = await tx.organization.delete({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organizationUserJoinTable.delete({
|
||||
where: {
|
||||
userId_orgId: {
|
||||
userId,
|
||||
orgId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return deletedOrganization;
|
||||
});
|
||||
}
|
||||
|
||||
async findById(orgId: string) {
|
||||
return await this.prisma.organization.findUnique({ where: { id: orgId } });
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return await this.prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateRefreshToken(id: string, refreshToken: string) {
|
||||
return await this.prisma.user.update({
|
||||
where: { id },
|
||||
|
||||
Reference in New Issue
Block a user