feat: Organization operations like invite and accept
This commit is contained in:
@@ -10,6 +10,7 @@ import { JwtPayload } from '../types';
|
||||
import { Request } from 'express';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PUBLIC_KEY } from 'common/keys';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
@@ -17,6 +18,7 @@ export class AuthGuard implements CanActivate {
|
||||
private readonly reflector: Reflector,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly requestContext: RequestContextService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
@@ -35,6 +37,10 @@ export class AuthGuard implements CanActivate {
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
|
||||
secret: 'demo',
|
||||
});
|
||||
|
||||
// TODO: Redis + Org too, blacklist token
|
||||
const userExists = await this.userService.findById(payload.userId);
|
||||
if (!userExists) throw new UnauthorizedException();
|
||||
this.requestContext.set('user', payload);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsEnum, IsNotEmpty, IsUUID } from 'class-validator';
|
||||
import { ORG_ROLE } from 'prisma/generated/prisma/enums';
|
||||
|
||||
export class InviteUserToOrganizationRequestDTO {
|
||||
@IsUUID()
|
||||
@ApiProperty({
|
||||
description: 'Who to invite',
|
||||
example: 'user1@example.com',
|
||||
type: 'string',
|
||||
})
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
invitedUserId: string;
|
||||
invitedUserEmail: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Organization id',
|
||||
example: 'eeec2c79-766a-4174-8004-2e57642095fe',
|
||||
type: 'string',
|
||||
})
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
orgId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Role to assign',
|
||||
example: ORG_ROLE.member,
|
||||
type: 'string',
|
||||
})
|
||||
@IsEnum(ORG_ROLE)
|
||||
@IsNotEmpty()
|
||||
role: ORG_ROLE;
|
||||
|
||||
@@ -1,4 +1,62 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { OrganizationMembershipService } from './organization-membership.service';
|
||||
import { RequestContextService } from 'core/als/request-context.service';
|
||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { InviteUserToOrganizationRequestDTO } from './dto';
|
||||
|
||||
@Controller('organization-membership')
|
||||
export class OrganizationMembershipController {}
|
||||
@ApiBearerAuth('access-token')
|
||||
export class OrganizationMembershipController {
|
||||
constructor(
|
||||
private readonly orgMemService: OrganizationMembershipService,
|
||||
private readonly requestContext: RequestContextService,
|
||||
) {}
|
||||
|
||||
@Post('/invite')
|
||||
async inviteUserToOrg(@Body() body: InviteUserToOrganizationRequestDTO) {
|
||||
const user = this.requestContext.user;
|
||||
return await this.orgMemService.inviteUserToOrg(user.userId, body);
|
||||
}
|
||||
|
||||
@Post('/request')
|
||||
async requestToJoinOrg() {}
|
||||
|
||||
// TODO: Move invite to org. section and join to user. Also option to cancel invitation and join req.
|
||||
|
||||
/* *
|
||||
* USER OPERATIONS
|
||||
* */
|
||||
@Patch('user/accept-invitation')
|
||||
acceptInvitation() {}
|
||||
|
||||
@Patch('user/reject-invitation')
|
||||
rejectInvitation() {}
|
||||
|
||||
@Get('user/invitations')
|
||||
async getUserInvitations() {
|
||||
const user = this.requestContext.user;
|
||||
return await this.orgMemService.getUserInvitations(user.userId);
|
||||
}
|
||||
|
||||
@Get('user/organizations')
|
||||
async getUserOrganizations() {}
|
||||
|
||||
/* *
|
||||
* ORGANIZATION OPERATIONS
|
||||
* */
|
||||
@Get('organization/:id/members')
|
||||
async getOrganizationMemebers(@Param('id') orgId: string) {
|
||||
return await this.orgMemService.getMemebersOfOrganization(orgId);
|
||||
}
|
||||
|
||||
@Get('organization/:id/invitations')
|
||||
async getOrganizationInvitations(@Param('id') orgId: string) {
|
||||
return await this.orgMemService.getMemebersOfOrganization(orgId);
|
||||
}
|
||||
|
||||
@Patch('organization/:id/accept-request')
|
||||
acceptJoinRequest() {}
|
||||
|
||||
@Patch('organization/:id/reject-request')
|
||||
rejectJoinRequest() {}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,17 @@ 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';
|
||||
import { RequestContextModule } from 'core/als/request-context.module';
|
||||
|
||||
@Module({
|
||||
controllers: [OrganizationMembershipController],
|
||||
providers: [OrganizationMembershipService],
|
||||
imports: [OrganizationModule, UserModule, PrismaModule, AuthorizationModule],
|
||||
imports: [
|
||||
OrganizationModule,
|
||||
UserModule,
|
||||
PrismaModule,
|
||||
AuthorizationModule,
|
||||
RequestContextModule,
|
||||
],
|
||||
})
|
||||
export class OrganizationMembershipModule {}
|
||||
|
||||
@@ -28,9 +28,10 @@ export class OrganizationMembershipService {
|
||||
userId: string,
|
||||
dto: InviteUserToOrganizationRequestDTO,
|
||||
) {
|
||||
const { invitedUserEmail, ...rest } = dto;
|
||||
const [orgExists, invitedUser] = await Promise.all([
|
||||
this.orgService.findById(dto.orgId),
|
||||
this.userService.getById(dto.invitedUserId),
|
||||
this.userService.findByEmail(invitedUserEmail),
|
||||
]);
|
||||
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
@@ -41,10 +42,11 @@ export class OrganizationMembershipService {
|
||||
where: {
|
||||
userId_orgId: {
|
||||
orgId: dto.orgId,
|
||||
userId,
|
||||
userId: invitedUser.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (userAlreadyPart)
|
||||
throw new BadRequestException('User already part of this organization');
|
||||
|
||||
@@ -58,8 +60,8 @@ export class OrganizationMembershipService {
|
||||
try {
|
||||
const invitation = await this.prisma.organizationJoinRequest.create({
|
||||
data: {
|
||||
...dto,
|
||||
userId: dto.invitedUserId,
|
||||
...rest,
|
||||
userId: invitedUser.id,
|
||||
requestType: ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
|
||||
},
|
||||
});
|
||||
@@ -69,16 +71,16 @@ export class OrganizationMembershipService {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (err.code === 'P2002')
|
||||
throw new BadRequestException('User invitation already sent.');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
requestToJoin() {}
|
||||
|
||||
// TODO: reject, rejectReason
|
||||
async acceptInvite(userId: string, orgId: string) {
|
||||
async acceptInvitation(userId: string, orgId: string) {
|
||||
const orgExists = await this.orgService.findById(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
@@ -134,7 +136,7 @@ export class OrganizationMembershipService {
|
||||
|
||||
async getUserInvitations(
|
||||
userId: string,
|
||||
requestType: ORGANIZATION_JOIN_REQUEST_TYPE,
|
||||
requestType: ORGANIZATION_JOIN_REQUEST_TYPE = ORGANIZATION_JOIN_REQUEST_TYPE.INVITED,
|
||||
status: ORGANIZATION_JOIN_REQUEST = ORGANIZATION_JOIN_REQUEST.PENDING,
|
||||
) {
|
||||
return await this.prisma.organizationJoinRequest.findMany({
|
||||
@@ -143,6 +145,29 @@ export class OrganizationMembershipService {
|
||||
status: status,
|
||||
requestType: requestType,
|
||||
},
|
||||
include: {
|
||||
organization: {
|
||||
select: { name: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMemebersOfOrganization(orgId: string) {
|
||||
const orgExists = await this.orgService.findById(orgId);
|
||||
if (!orgExists) throw new NotFoundException('Organization');
|
||||
|
||||
return await this.prisma.organizationUserJoinTable.findMany({
|
||||
where: {
|
||||
orgId: orgId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Organization } from 'prisma/generated/prisma/client';
|
||||
|
||||
export class OrganizationDTO {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string | null;
|
||||
readonly createdAt: Date;
|
||||
|
||||
constructor(organization: Organization) {
|
||||
this.id = organization.id;
|
||||
this.name = organization.name;
|
||||
this.description = organization.description;
|
||||
this.createdAt = organization.createdAt;
|
||||
|
||||
@@ -29,7 +29,7 @@ export class PrismaService
|
||||
|
||||
super({
|
||||
adapter: adapter,
|
||||
log: ['info', 'error', 'warn'],
|
||||
log: ['info', 'warn'],
|
||||
});
|
||||
}
|
||||
async onModuleInit() {
|
||||
|
||||
@@ -28,7 +28,7 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
async findById(id: string) {
|
||||
return await this.prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
@@ -36,6 +36,14 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmail(email: string) {
|
||||
return await this.prisma.user.findUnique({
|
||||
where: {
|
||||
email: email,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateRefreshToken(id: string, refreshToken: string) {
|
||||
return await this.prisma.user.update({
|
||||
where: { id },
|
||||
|
||||
Reference in New Issue
Block a user