@@ -22,6 +22,7 @@ import { Knex } from 'knex';
2222import { difference , keyBy , map } from 'lodash' ;
2323import { InjectModel } from 'nest-knexjs' ;
2424import { ClsService } from 'nestjs-cls' ;
25+ import { ThresholdConfig , IThresholdConfig } from '../../configs/threshold.config' ;
2526import { CustomHttpException } from '../../custom.exception' ;
2627import { InjectDbProvider } from '../../db-provider/db.provider' ;
2728import { IDbProvider } from '../../db-provider/db.provider.interface' ;
@@ -43,7 +44,8 @@ export class CollaboratorService {
4344 private readonly cls : ClsService < IClsStore > ,
4445 private readonly eventEmitterService : EventEmitterService ,
4546 @InjectModel ( 'CUSTOM_KNEX' ) private readonly knex : Knex ,
46- @InjectDbProvider ( ) private readonly dbProvider : IDbProvider
47+ @InjectDbProvider ( ) private readonly dbProvider : IDbProvider ,
48+ @ThresholdConfig ( ) private readonly thresholdConfig : IThresholdConfig
4749 ) { }
4850
4951 async createSpaceCollaborator ( {
@@ -82,6 +84,20 @@ export class CollaboratorService {
8284 }
8385 ) ;
8486 }
87+ if ( role === Role . Owner ) {
88+ const userIds = collaborators
89+ . filter ( ( c ) => c . principalType === PrincipalType . User )
90+ . map ( ( c ) => c . principalId ) ;
91+ if ( userIds . length > 0 ) {
92+ const countMap = await this . countUserOwnedSpaces ( userIds ) ;
93+ for ( const uid of userIds ) {
94+ await this . validateOwnedSpaceLimit (
95+ countMap . get ( uid ) ?? 0 ,
96+ uid !== currentUserId ? uid : undefined
97+ ) ;
98+ }
99+ }
100+ }
85101 // if has exist base collaborator, then delete it
86102 const bases = await this . prismaService . txClient ( ) . base . findMany ( {
87103 where : {
@@ -548,6 +564,61 @@ export class CollaboratorService {
548564 return collaborators . length === 1 && collaborators [ 0 ] . principal_id === userId ;
549565 }
550566
567+ async countUserOwnedSpaces ( userId : string ) : Promise < number > ;
568+ async countUserOwnedSpaces ( userIds : string [ ] ) : Promise < Map < string , number > > ;
569+ async countUserOwnedSpaces (
570+ userIdOrIds : string | string [ ]
571+ ) : Promise < number | Map < string , number > > {
572+ const isSingle = typeof userIdOrIds === 'string' ;
573+ const userIds = isSingle ? [ userIdOrIds ] : userIdOrIds ;
574+ if ( userIds . length === 0 ) return isSingle ? 0 : new Map ( ) ;
575+ const builder = this . knex ( 'collaborator' )
576+ . join ( 'space' , 'collaborator.resource_id' , 'space.id' )
577+ . whereIn ( 'collaborator.principal_id' , userIds )
578+ . where ( 'collaborator.principal_type' , PrincipalType . User )
579+ . where ( 'collaborator.resource_type' , CollaboratorType . Space )
580+ . where ( 'collaborator.role_name' , Role . Owner )
581+ . whereNull ( 'space.deleted_time' )
582+ . groupBy ( 'collaborator.principal_id' )
583+ . select ( 'collaborator.principal_id as user_id' )
584+ . count ( '* as count' ) ;
585+ const result = await this . prismaService
586+ . txClient ( )
587+ . $queryRawUnsafe < { user_id : string ; count : number } [ ] > ( builder . toQuery ( ) ) ;
588+ if ( isSingle ) {
589+ return Number ( result [ 0 ] ?. count ?? 0 ) ;
590+ }
591+ const countMap = new Map < string , number > ( ) ;
592+ for ( const row of result ) {
593+ countMap . set ( row . user_id , Number ( row . count ) ) ;
594+ }
595+ return countMap ;
596+ }
597+
598+ async validateOwnedSpaceLimit ( currentCount : number , userId ?: string ) : Promise < void > {
599+ const maxCount = this . thresholdConfig . maxOwnedSpaceCount ;
600+ if ( maxCount <= 0 || currentCount < maxCount ) return ;
601+
602+ const userName = userId
603+ ? await this . prismaService . user
604+ . findUnique ( { where : { id : userId } , select : { name : true , email : true } } )
605+ . then ( ( user ) => ( user ? `${ user . name } (${ user . email } )` : undefined ) )
606+ : undefined ;
607+
608+ throw new CustomHttpException (
609+ `Owned space limit exceeded, max: ${ maxCount } ${ userName ? `, user: ${ userName } ` : '' } ` ,
610+ HttpErrorCode . VALIDATION_ERROR ,
611+ {
612+ localization : {
613+ i18nKey : userId
614+ ? 'httpErrors.space.ownedSpaceLimitExceededOther'
615+ : 'httpErrors.space.ownedSpaceLimitExceeded' ,
616+ context : userId ? { max : maxCount , name : userName } : { max : maxCount } ,
617+ } ,
618+ }
619+ ) ;
620+ }
621+
551622 async deleteCollaborator ( {
552623 resourceId,
553624 resourceType,
@@ -659,6 +730,19 @@ export class CollaboratorService {
659730 ) ;
660731 }
661732
733+ if (
734+ role === Role . Owner &&
735+ resourceType === CollaboratorType . Space &&
736+ targetColl . roleName !== Role . Owner &&
737+ principalType === PrincipalType . User
738+ ) {
739+ const count = await this . countUserOwnedSpaces ( principalId ) ;
740+ await this . validateOwnedSpaceLimit (
741+ count ,
742+ principalId !== currentUserId ? principalId : undefined
743+ ) ;
744+ }
745+
662746 const res = await this . prismaService . txClient ( ) . collaborator . updateMany ( {
663747 where : {
664748 resourceId : resourceId ,
0 commit comments