|
|
@@ -1,32 +1,42 @@
|
|
|
-import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'
|
|
|
+import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'
|
|
|
import { OrgsRepository } from './orgs.repository'
|
|
|
-import { CreateOrgDto, UpdateOrgDto } from './dto/org.dto'
|
|
|
+import { AddMemberDto, CreateOrgDto, UpdateMemberRoleDto, UpdateOrgDto } from './dto/org.dto'
|
|
|
+
|
|
|
+type OrgRole = 'org_admin' | 'project_manager' | 'viewer'
|
|
|
|
|
|
@Injectable()
|
|
|
export class OrgsService {
|
|
|
constructor(private readonly repo: OrgsRepository) {}
|
|
|
|
|
|
- create(dto: CreateOrgDto) {
|
|
|
- return this.repo.create(dto)
|
|
|
+ async create(dto: CreateOrgDto, creatorUserId: string) {
|
|
|
+ const org = await this.repo.create(dto)
|
|
|
+ await this.repo.addMember(org!.id, creatorUserId, 'org_admin')
|
|
|
+ return org
|
|
|
}
|
|
|
|
|
|
- findAll() {
|
|
|
- return this.repo.findAll()
|
|
|
+ async findAll(userId: string) {
|
|
|
+ const orgIds = await this.getUserOrgIds(userId)
|
|
|
+ if (orgIds.length === 0) return []
|
|
|
+ const all = await this.repo.findAll()
|
|
|
+ return all.filter((o) => orgIds.includes(o.id))
|
|
|
}
|
|
|
|
|
|
- async findOne(id: string) {
|
|
|
+ async findOne(id: string, userId: string) {
|
|
|
+ await this.ensureUserInOrg(userId, id)
|
|
|
const org = await this.repo.findById(id)
|
|
|
if (!org) throw new NotFoundException(`Organization ${id} not found`)
|
|
|
return org
|
|
|
}
|
|
|
|
|
|
- async update(id: string, dto: UpdateOrgDto) {
|
|
|
+ async update(id: string, dto: UpdateOrgDto, userId: string) {
|
|
|
+ await this.ensureRoleInOrg(userId, id, ['org_admin'])
|
|
|
const org = await this.repo.findById(id)
|
|
|
if (!org) throw new NotFoundException(`Organization ${id} not found`)
|
|
|
return this.repo.update(id, dto)
|
|
|
}
|
|
|
|
|
|
- async remove(id: string) {
|
|
|
+ async remove(id: string, userId: string) {
|
|
|
+ await this.ensureRoleInOrg(userId, id, ['org_admin'])
|
|
|
const org = await this.repo.findById(id)
|
|
|
if (!org) throw new NotFoundException(`Organization ${id} not found`)
|
|
|
await this.repo.remove(id)
|
|
|
@@ -41,8 +51,60 @@ export class OrgsService {
|
|
|
return membership
|
|
|
}
|
|
|
|
|
|
+ async ensureRoleInOrg(userId: string, orgId: string, allowedRoles: OrgRole[]) {
|
|
|
+ const membership = await this.ensureUserInOrg(userId, orgId)
|
|
|
+ const role = membership.role as OrgRole
|
|
|
+ if (!allowedRoles.includes(role)) {
|
|
|
+ throw new ForbiddenException(`Role ${role} is not allowed`) }
|
|
|
+ return membership
|
|
|
+ }
|
|
|
+
|
|
|
async getUserOrgIds(userId: string): Promise<string[]> {
|
|
|
const memberships = await this.repo.getUserMemberships(userId)
|
|
|
return memberships.map((m) => m.orgId)
|
|
|
}
|
|
|
+
|
|
|
+ async listMembers(orgId: string, userId: string) {
|
|
|
+ await this.ensureUserInOrg(userId, orgId)
|
|
|
+ const members = await this.repo.getOrgMembers(orgId)
|
|
|
+ return members
|
|
|
+ }
|
|
|
+
|
|
|
+ async addMember(orgId: string, actorUserId: string, dto: AddMemberDto) {
|
|
|
+ await this.ensureRoleInOrg(actorUserId, orgId, ['org_admin'])
|
|
|
+
|
|
|
+ const user = await this.repo.findUserByEmail(dto.email)
|
|
|
+ if (!user) {
|
|
|
+ throw new BadRequestException(`User ${dto.email} not found`) }
|
|
|
+
|
|
|
+ const exists = await this.repo.isUserMemberOfOrg(user.id, orgId)
|
|
|
+ if (exists) {
|
|
|
+ throw new BadRequestException('User is already a member of this org') }
|
|
|
+
|
|
|
+ return this.repo.addMember(orgId, user.id, dto.role)
|
|
|
+ }
|
|
|
+
|
|
|
+ async updateMemberRole(orgId: string, actorUserId: string, memberUserId: string, dto: UpdateMemberRoleDto) {
|
|
|
+ await this.ensureRoleInOrg(actorUserId, orgId, ['org_admin'])
|
|
|
+ const exists = await this.repo.isUserMemberOfOrg(memberUserId, orgId)
|
|
|
+ if (!exists) {
|
|
|
+ throw new NotFoundException('Member not found in organization') }
|
|
|
+
|
|
|
+ return this.repo.updateMemberRole(orgId, memberUserId, dto.role)
|
|
|
+ }
|
|
|
+
|
|
|
+ async removeMember(orgId: string, actorUserId: string, memberUserId: string) {
|
|
|
+ await this.ensureRoleInOrg(actorUserId, orgId, ['org_admin'])
|
|
|
+
|
|
|
+ const actorMembership = await this.ensureUserInOrg(actorUserId, orgId)
|
|
|
+ if (actorUserId === memberUserId && actorMembership.role === 'org_admin') {
|
|
|
+ const members = await this.repo.getOrgMembers(orgId)
|
|
|
+ const adminCount = members.filter((m) => m.role === 'org_admin').length
|
|
|
+ if (adminCount <= 1) {
|
|
|
+ throw new BadRequestException('Cannot remove the last org_admin from organization') }
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.repo.removeMember(orgId, memberUserId)
|
|
|
+ return { success: true }
|
|
|
+ }
|
|
|
}
|