diff --git a/src/app.module.ts b/src/app.module.ts index d2fc43025..c908d0f9f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,7 @@ import { AccessTokensModule } from './modules/access-tokens/access-tokens.module import { AuthModule } from './modules/auth/auth.module'; import { EmailModule } from './modules/email/email.module'; import { EmailsModule } from './modules/emails/emails.module'; +import { GroupsModule } from './modules/groups/groups.module'; import { PrismaModule } from './modules/prisma/prisma.module'; import { SessionsModule } from './modules/sessions/sessions.module'; import { UsersModule } from './modules/user/user.module'; @@ -29,6 +30,7 @@ import { UsersModule } from './modules/user/user.module'; SessionsModule, AccessTokensModule, EmailsModule, + GroupsModule, ], controllers: [AppController], providers: [ diff --git a/src/modules/groups/groups.controller.ts b/src/modules/groups/groups.controller.ts new file mode 100644 index 000000000..2f9955f7e --- /dev/null +++ b/src/modules/groups/groups.controller.ts @@ -0,0 +1,95 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + Put, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { groups } from '@prisma/client'; +import { Expose } from 'src/modules/prisma/prisma.interface'; +import { CursorPipe } from 'src/pipes/cursor.pipe'; +import { OptionalIntPipe } from 'src/pipes/optional-int.pipe'; +import { OrderByPipe } from 'src/pipes/order-by.pipe'; +import { WherePipe } from 'src/pipes/where.pipe'; +import { UserRequest } from '../auth/auth.interface'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { Scopes } from '../auth/scope.decorator'; +import { ScopesGuard } from '../auth/scope.guard'; +import { CreateGroupDto, ReplaceGroupDto, UpdateGroupDto } from './groups.dto'; +import { GroupsService } from './groups.service'; + +@Controller('groups') +@UseGuards(JwtAuthGuard) +export class GroupController { + constructor(private groupsService: GroupsService) {} + + @Post() + @UseGuards(ScopesGuard) + @Scopes('user:write', 'group:write') + async create( + @Req() req: UserRequest, + @Body() data: CreateGroupDto, + ): Promise> { + return this.groupsService.createGroup(req.user.id, data); + } + + @Get() + @UseGuards(ScopesGuard) + @Scopes('group:read') + async getAll( + @Query('skip', OptionalIntPipe) skip?: number, + @Query('take', OptionalIntPipe) take?: number, + @Query('cursor', CursorPipe) cursor?: Record, + @Query('where', WherePipe) where?: Record, + @Query('orderBy', OrderByPipe) orderBy?: Record, + ): Promise[]> { + return this.groupsService.getGroups({ + skip, + take, + orderBy, + cursor, + where, + }); + } + + @Get(':id') + @UseGuards(ScopesGuard) + @Scopes('group{id}:read') + async get(@Param('id', ParseIntPipe) id: number): Promise> { + return this.groupsService.getGroup(Number(id)); + } + + @Patch(':id') + @UseGuards(ScopesGuard) + @Scopes('group{id}:write') + async update( + @Body() data: UpdateGroupDto, + @Param('id', ParseIntPipe) id: number, + ): Promise> { + return this.groupsService.updateGroup(Number(id), data); + } + + @Put(':id') + @UseGuards(ScopesGuard) + @Scopes('group{id}:write') + async replace( + @Body() data: ReplaceGroupDto, + @Param('id', ParseIntPipe) id: number, + ): Promise> { + return this.groupsService.updateGroup(Number(id), data); + } + + @Delete(':id') + @UseGuards(ScopesGuard) + @Scopes('group{id}:delete') + async remove(@Param('id', ParseIntPipe) id: number): Promise> { + return this.groupsService.deleteGroup(Number(id)); + } +} diff --git a/src/modules/groups/groups.dto.ts b/src/modules/groups/groups.dto.ts new file mode 100644 index 000000000..3d8d58563 --- /dev/null +++ b/src/modules/groups/groups.dto.ts @@ -0,0 +1,98 @@ +import { + IsArray, + IsBoolean, + IsNotEmpty, + IsObject, + IsOptional, + IsString, +} from 'class-validator'; + +export class CreateGroupDto { + @IsBoolean() + @IsOptional() + autoJoinDomain?: boolean; + + @IsBoolean() + @IsOptional() + forceTwoFactor?: boolean; + + @IsArray() + @IsOptional() + ipRestrictions?: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsBoolean() + @IsOptional() + onlyAllowDomain?: boolean; + + @IsString() + @IsOptional() + profilePictureUrl?: string; + + @IsObject() + @IsOptional() + attributes: Record; +} + +export class UpdateGroupDto { + @IsBoolean() + @IsOptional() + autoJoinDomain?: boolean; + + @IsBoolean() + @IsOptional() + forceTwoFactor?: boolean; + + @IsArray() + @IsOptional() + ipRestrictions?: string; + + @IsString() + @IsOptional() + name?: string; + + @IsBoolean() + @IsOptional() + onlyAllowDomain?: boolean; + + @IsString() + @IsOptional() + profilePictureUrl?: string; + + @IsObject() + @IsOptional() + attributes: Record; +} + +export class ReplaceGroupDto { + @IsBoolean() + @IsNotEmpty() + autoJoinDomain: boolean; + + @IsBoolean() + @IsNotEmpty() + forceTwoFactor: boolean; + + @IsArray() + @IsNotEmpty() + ipRestrictions: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsBoolean() + @IsNotEmpty() + onlyAllowDomain: boolean; + + @IsString() + @IsNotEmpty() + profilePictureUrl: string; + + @IsObject() + @IsNotEmpty() + attributes: Record; +} diff --git a/src/modules/groups/groups.module.ts b/src/modules/groups/groups.module.ts new file mode 100644 index 000000000..8479c8625 --- /dev/null +++ b/src/modules/groups/groups.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../prisma/prisma.module'; +import { GroupController } from './groups.controller'; +import { GroupsService } from './groups.service'; + +@Module({ + imports: [PrismaModule], + controllers: [GroupController], + providers: [GroupsService], +}) +export class GroupsModule {} diff --git a/src/modules/groups/groups.service.ts b/src/modules/groups/groups.service.ts new file mode 100644 index 000000000..207d3d017 --- /dev/null +++ b/src/modules/groups/groups.service.ts @@ -0,0 +1,101 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + groups, + groupsCreateInput, + groupsOrderByInput, + groupsUpdateInput, + groupsWhereInput, + groupsWhereUniqueInput, +} from '@prisma/client'; +import { Expose } from 'src/modules/prisma/prisma.interface'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class GroupsService { + constructor(private prisma: PrismaService) {} + + async createGroup( + userId: number, + data: Omit, 'user'>, + ): Promise { + return this.prisma.groups.create({ + data: { + ...data, + memberships: { + create: { role: 'OWNER', user: { connect: { id: userId } } }, + }, + }, + }); + } + + async getGroups(params: { + skip?: number; + take?: number; + cursor?: groupsWhereUniqueInput; + where?: groupsWhereInput; + orderBy?: groupsOrderByInput; + }): Promise[]> { + const { skip, take, cursor, where, orderBy } = params; + const groups = await this.prisma.groups.findMany({ + skip, + take, + cursor, + where, + orderBy, + }); + return groups.map(user => this.prisma.expose(user)); + } + + async getGroup(id: number): Promise | null> { + const group = await this.prisma.groups.findOne({ + where: { id }, + }); + if (!group) + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + return this.prisma.expose(group); + } + + async updateGroup( + id: number, + data: groupsUpdateInput, + ): Promise> { + const testGroup = await this.prisma.groups.findOne({ + where: { id }, + }); + if (!testGroup) + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + const group = await this.prisma.groups.update({ + where: { id }, + data, + }); + return this.prisma.expose(group); + } + + async replaceGroup( + id: number, + data: groupsCreateInput, + ): Promise> { + const testGroup = await this.prisma.groups.findOne({ + where: { id }, + }); + if (!testGroup) + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + const group = await this.prisma.groups.update({ + where: { id }, + data, + }); + return this.prisma.expose(group); + } + + async deleteGroup(id: number): Promise> { + const testGroup = await this.prisma.groups.findOne({ + where: { id }, + }); + if (!testGroup) + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + const group = await this.prisma.groups.delete({ + where: { id }, + }); + return this.prisma.expose(group); + } +}