Skip to content

Commit b7e9a44

Browse files
authored
feat(server): support resource group (#1442)
* feat(server): support teamwork * feat(server): adapt to teamwork * feat(server): add `InjectApplication` * fix(server): fix team bug * refactor * fix * feat(server): improve team application * refactor(server): refactor team invite code * refactor
1 parent 9722e9a commit b7e9a44

28 files changed

+1539
-35
lines changed

server/src/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { AuthenticationModule } from './authentication/authentication.module'
2525
import { FunctionTemplateModule } from './function-template/function-template.module'
2626
import { MulterModule } from '@nestjs/platform-express'
2727
import { RecycleBinModule } from './recycle-bin/recycle-bin.module'
28+
import { GroupModule } from './group/group.module'
2829
import { APP_INTERCEPTOR } from '@nestjs/core'
2930
import { AppInterceptor } from './app.interceptor'
3031
import { InterceptorModule } from './interceptor/interceptor.module'
@@ -71,6 +72,7 @@ import { InterceptorModule } from './interceptor/interceptor.module'
7172
FunctionTemplateModule,
7273
MulterModule.register(),
7374
RecycleBinModule,
75+
GroupModule,
7476
InterceptorModule,
7577
],
7678
controllers: [AppController],

server/src/application/application.controller.ts

+37-18
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@ import {
55
Patch,
66
Param,
77
UseGuards,
8-
Req,
98
Logger,
109
Post,
1110
Delete,
11+
ForbiddenException,
1212
} from '@nestjs/common'
1313
import {
1414
ApiBearerAuth,
1515
ApiOperation,
1616
ApiResponse,
1717
ApiTags,
1818
} from '@nestjs/swagger'
19-
import { IRequest } from '../utils/interface'
2019
import { JwtAuthGuard } from '../authentication/jwt.auth.guard'
2120
import {
2221
ApiResponseArray,
@@ -49,6 +48,11 @@ import { ResourceService } from 'src/billing/resource.service'
4948
import { RuntimeDomainService } from 'src/gateway/runtime-domain.service'
5049
import { BindCustomDomainDto } from 'src/website/dto/update-website.dto'
5150
import { RuntimeDomain } from 'src/gateway/entities/runtime-domain'
51+
import { GroupRole, getRoleLevel } from 'src/group/entities/group-member'
52+
import { GroupRoles } from 'src/group/group-role.decorator'
53+
import { InjectApplication, InjectGroup, InjectUser } from 'src/utils/decorator'
54+
import { User } from 'src/user/entities/user'
55+
import { GroupWithRole } from 'src/group/entities/group'
5256

5357
@ApiTags('Application')
5458
@Controller('applications')
@@ -73,14 +77,12 @@ export class ApplicationController {
7377
@ApiOperation({ summary: 'Create application' })
7478
@ApiResponseObject(ApplicationWithRelations)
7579
@Post()
76-
async create(@Req() req: IRequest, @Body() dto: CreateApplicationDto) {
80+
async create(@Body() dto: CreateApplicationDto, @InjectUser() user: User) {
7781
const error = dto.autoscaling.validate()
7882
if (error) {
7983
return ResponseUtil.error(error)
8084
}
8185

82-
const user = req.user
83-
8486
// check regionId exists
8587
const region = await this.region.findOneDesensitized(
8688
new ObjectId(dto.regionId),
@@ -141,8 +143,7 @@ export class ApplicationController {
141143
@Get()
142144
@ApiOperation({ summary: 'Get user application list' })
143145
@ApiResponseArray(ApplicationWithRelations)
144-
async findAll(@Req() req: IRequest) {
145-
const user = req.user
146+
async findAll(@InjectUser() user: User) {
146147
const data = await this.application.findAllByUser(user._id)
147148
return ResponseUtil.ok(data)
148149
}
@@ -207,6 +208,7 @@ export class ApplicationController {
207208
*/
208209
@ApiOperation({ summary: 'Update application name' })
209210
@ApiResponseObject(Application)
211+
@GroupRoles(GroupRole.Admin)
210212
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
211213
@Patch(':appid/name')
212214
async updateName(
@@ -227,13 +229,16 @@ export class ApplicationController {
227229
async updateState(
228230
@Param('appid') appid: string,
229231
@Body() dto: UpdateApplicationStateDto,
230-
@Req() req: IRequest,
232+
@InjectApplication() app: Application,
233+
@InjectGroup() group: GroupWithRole,
231234
) {
232-
const app = req.application
233-
const user = req.user
235+
if (dto.state === ApplicationState.Deleted) {
236+
throw new ForbiddenException('cannot update state to deleted')
237+
}
238+
const userid = app.createdBy
234239

235240
// check account balance
236-
const account = await this.account.findOne(user._id)
241+
const account = await this.account.findOne(userid)
237242
const balance = account?.balance || 0
238243
if (balance < 0) {
239244
return ResponseUtil.error(`account balance is not enough`)
@@ -272,6 +277,15 @@ export class ApplicationController {
272277
)
273278
}
274279

280+
if (
281+
[ApplicationState.Stopped, ApplicationState.Running].includes(
282+
dto.state,
283+
) &&
284+
getRoleLevel(group.role) < getRoleLevel(GroupRole.Admin)
285+
) {
286+
return ResponseUtil.error('no permission')
287+
}
288+
275289
const doc = await this.application.updateState(appid, dto.state)
276290
return ResponseUtil.ok(doc)
277291
}
@@ -281,20 +295,20 @@ export class ApplicationController {
281295
*/
282296
@ApiOperation({ summary: 'Update application bundle' })
283297
@ApiResponseObject(ApplicationBundle)
298+
@GroupRoles(GroupRole.Admin)
284299
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
285300
@Patch(':appid/bundle')
286301
async updateBundle(
287302
@Param('appid') appid: string,
288303
@Body() dto: UpdateApplicationBundleDto,
289-
@Req() req: IRequest,
304+
@InjectApplication() app: ApplicationWithRelations,
290305
) {
291306
const error = dto.autoscaling.validate()
292307
if (error) {
293308
return ResponseUtil.error(error)
294309
}
295310

296-
const app = await this.application.findOne(appid)
297-
const user = req.user
311+
const userid = app.createdBy
298312
const regionId = app.regionId
299313

300314
// check if trial tier
@@ -304,7 +318,7 @@ export class ApplicationController {
304318
})
305319
if (isTrialTier) {
306320
const bundle = await this.resource.findTrialBundle(regionId)
307-
const trials = await this.application.findTrialApplications(user._id)
321+
const trials = await this.application.findTrialApplications(userid)
308322
const limitOfFreeTier = bundle?.limitCountOfFreeTierPerUser || 0
309323
if (trials.length >= (limitOfFreeTier || 0)) {
310324
return ResponseUtil.error(
@@ -334,6 +348,7 @@ export class ApplicationController {
334348
*/
335349
@ApiResponseObject(RuntimeDomain)
336350
@ApiOperation({ summary: 'Bind custom domain to application' })
351+
@GroupRoles(GroupRole.Admin)
337352
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
338353
@Patch(':appid/domain')
339354
async bindDomain(
@@ -368,6 +383,7 @@ export class ApplicationController {
368383
*/
369384
@ApiResponse({ type: ResponseUtil<boolean> })
370385
@ApiOperation({ summary: 'Check if domain is resolved' })
386+
@GroupRoles(GroupRole.Admin)
371387
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
372388
@Post(':appid/domain/resolved')
373389
async checkResolved(
@@ -387,6 +403,7 @@ export class ApplicationController {
387403
*/
388404
@ApiResponseObject(RuntimeDomain)
389405
@ApiOperation({ summary: 'Remove custom domain of application' })
406+
@GroupRoles(GroupRole.Admin)
390407
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
391408
@Delete(':appid/domain')
392409
async remove(@Param('appid') appid: string) {
@@ -408,11 +425,13 @@ export class ApplicationController {
408425
*/
409426
@ApiOperation({ summary: 'Delete an application' })
410427
@ApiResponseObject(Application)
428+
@GroupRoles(GroupRole.Owner)
411429
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
412430
@Delete(':appid')
413-
async delete(@Param('appid') appid: string, @Req() req: IRequest) {
414-
const app = req.application
415-
431+
async delete(
432+
@Param('appid') appid: string,
433+
@InjectApplication() app: ApplicationWithRelations,
434+
) {
416435
// check: only stopped application can be deleted
417436
if (
418437
app.state !== ApplicationState.Stopped &&

server/src/application/application.service.ts

+29-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ import {
2121
ApplicationBundle,
2222
ApplicationBundleResource,
2323
} from './entities/application-bundle'
24+
import { GroupService } from 'src/group/group.service'
25+
import { GroupMember } from 'src/group/entities/group-member'
2426

2527
@Injectable()
2628
export class ApplicationService {
2729
private readonly logger = new Logger(ApplicationService.name)
2830

31+
constructor(private readonly groupService: GroupService) {}
32+
2933
/**
3034
* Create application
3135
* - create configuration
@@ -85,7 +89,7 @@ export class ApplicationService {
8589
state: dto.state || ApplicationState.Running,
8690
phase: ApplicationPhase.Creating,
8791
tags: [],
88-
createdBy: new ObjectId(userid),
92+
createdBy: userid,
8993
lockedAt: TASK_LOCK_INIT_TIME,
9094
regionId: new ObjectId(dto.regionId),
9195
runtimeId: new ObjectId(dto.runtimeId),
@@ -96,6 +100,7 @@ export class ApplicationService {
96100
{ session },
97101
)
98102

103+
await this.groupService.create(appid, userid, appid)
99104
// commit transaction
100105
await session.commitTransaction()
101106
} catch (error) {
@@ -110,11 +115,30 @@ export class ApplicationService {
110115
const db = SystemDatabase.db
111116

112117
const doc = await db
113-
.collection('Application')
114-
.aggregate<ApplicationWithRelations>()
118+
.collection<GroupMember>('GroupMember')
119+
.aggregate()
120+
.match({
121+
uid: userid,
122+
})
123+
.lookup({
124+
from: 'GroupApplication',
125+
localField: 'groupId',
126+
foreignField: 'groupId',
127+
as: 'applications',
128+
})
129+
.unwind('$applications')
130+
.project({
131+
_id: 0,
132+
appid: '$applications.appid',
133+
})
134+
.toArray()
135+
136+
const res = db
137+
.collection<Application>('Application')
138+
.aggregate()
115139
.match({
116-
createdBy: new ObjectId(userid),
117140
phase: { $ne: ApplicationPhase.Deleted },
141+
appid: { $in: doc.map((v) => v.appid) },
118142
})
119143
.lookup({
120144
from: 'ApplicationBundle',
@@ -136,7 +160,7 @@ export class ApplicationService {
136160
})
137161
.toArray()
138162

139-
return doc
163+
return res
140164
}
141165

142166
async findOne(appid: string) {

server/src/authentication/application.auth.guard.ts

+45-3
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,70 @@ import {
77
import { ApplicationService } from '../application/application.service'
88
import { IRequest } from '../utils/interface'
99
import { User } from 'src/user/entities/user'
10+
import { GroupService } from 'src/group/group.service'
11+
import { getRoleLevel } from 'src/group/entities/group-member'
12+
import { Reflector } from '@nestjs/core'
1013

1114
@Injectable()
1215
export class ApplicationAuthGuard implements CanActivate {
1316
logger = new Logger(ApplicationAuthGuard.name)
14-
constructor(private appService: ApplicationService) {}
17+
constructor(
18+
private readonly appService: ApplicationService,
19+
private readonly groupService: GroupService,
20+
private readonly reflector: Reflector,
21+
) {}
1522
async canActivate(context: ExecutionContext) {
1623
const request = context.switchToHttp().getRequest() as IRequest
1724
const appid = request.params.appid
1825
const user = request.user as User
1926

27+
// check appid
2028
const app = await this.appService.findOne(appid)
2129
if (!app) {
2230
return false
2331
}
2432

25-
const author_id = app.createdBy?.toString()
26-
if (author_id !== user._id.toString()) {
33+
const ok = await this.checkGroupAuth(appid, user, context)
34+
if (!ok) {
2735
return false
2836
}
2937

3038
// inject app to request
3139
request.application = app
40+
41+
return true
42+
}
43+
44+
async checkGroupAuth(appid: string, user: User, context: ExecutionContext) {
45+
const request = context.switchToHttp().getRequest() as IRequest
46+
47+
// check group
48+
const groups = await this.groupService.findGroupsByAppidAndUid(
49+
appid,
50+
user._id,
51+
)
52+
if (groups.length === 0) {
53+
return false
54+
}
55+
56+
groups.sort((a, b) => getRoleLevel(b.role) - getRoleLevel(a.role))
57+
const group = groups[0]
58+
59+
// check group role
60+
const roles = this.reflector.get<string[]>(
61+
'group-roles',
62+
context.getHandler(),
63+
)
64+
if (roles?.length > 0) {
65+
const roleLevels = roles.map(getRoleLevel).sort((a, b) => a - b)
66+
67+
if (roleLevels[0] > getRoleLevel(group.role)) {
68+
return false
69+
}
70+
}
71+
72+
request.group = group
73+
3274
return true
3375
}
3476
}

0 commit comments

Comments
 (0)