feat: Organization operations like invite and accept

This commit is contained in:
sauravdhakal12
2026-02-22 17:27:37 +05:45
parent afed1731d2
commit 90b0192cd2
22 changed files with 1604 additions and 73 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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() {}
}

View File

@@ -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 {}

View File

@@ -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,
},
},
},
});
}
}

View File

@@ -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;

View File

@@ -29,7 +29,7 @@ export class PrismaService
super({
adapter: adapter,
log: ['info', 'error', 'warn'],
log: ['info', 'warn'],
});
}
async onModuleInit() {

View File

@@ -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 },