mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 06:12:38 +10:00
Refactoring
* Refactor workspace membership system * Create setup endpoint * Use Passport.js * Several updates and fixes
This commit is contained in:
@ -11,49 +11,49 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@mantine/core": "^7.4.2",
|
"@mantine/core": "^7.6.2",
|
||||||
"@mantine/form": "^7.4.2",
|
"@mantine/form": "^7.6.2",
|
||||||
"@mantine/hooks": "^7.4.2",
|
"@mantine/hooks": "^7.6.2",
|
||||||
"@mantine/modals": "^7.4.2",
|
"@mantine/modals": "^7.6.2",
|
||||||
"@mantine/notifications": "^7.4.2",
|
"@mantine/notifications": "^7.6.2",
|
||||||
"@mantine/spotlight": "^7.4.2",
|
"@mantine/spotlight": "^7.6.2",
|
||||||
"@tabler/icons-react": "^2.46.0",
|
"@tabler/icons-react": "^3.1.0",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.28.4",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.8",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.5.0",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"jotai": "^2.6.2",
|
"jotai": "^2.7.1",
|
||||||
"jotai-optics": "^0.3.1",
|
"jotai-optics": "^0.3.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-arborist": "^3.4.0",
|
"react-arborist": "^3.4.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.21.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.5",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/eslint-plugin-query": "^5.17.20",
|
"@tanstack/eslint-plugin-query": "^5.27.7",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "20.11.5",
|
"@types/node": "20.11.28",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
"@typescript-eslint/parser": "^6.19.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"optics-ts": "^2.4.1",
|
"optics-ts": "^2.4.1",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.35",
|
||||||
"postcss-preset-mantine": "^1.12.3",
|
"postcss-preset-mantine": "^1.13.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.2",
|
||||||
"vite": "^5.0.12"
|
"vite": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,67 +26,70 @@
|
|||||||
"migration:show": "pnpm run typeorm migration:show"
|
"migration:show": "pnpm run typeorm migration:show"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.456.0",
|
"@aws-sdk/client-s3": "^3.535.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.456.0",
|
"@aws-sdk/s3-request-presigner": "^3.535.0",
|
||||||
"@casl/ability": "^6.7.0",
|
"@casl/ability": "^6.7.0",
|
||||||
"@fastify/multipart": "^8.1.0",
|
"@fastify/multipart": "^8.1.0",
|
||||||
"@fastify/static": "^6.12.0",
|
"@fastify/static": "^7.0.1",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.3",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.2.0",
|
||||||
"@nestjs/core": "^10.3.0",
|
"@nestjs/core": "^10.3.3",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/mapped-types": "^2.0.4",
|
"@nestjs/mapped-types": "^2.0.5",
|
||||||
"@nestjs/platform-fastify": "^10.3.0",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-socket.io": "^10.3.0",
|
"@nestjs/platform-fastify": "^10.3.3",
|
||||||
"@nestjs/serve-static": "^4.0.0",
|
"@nestjs/platform-socket.io": "^10.3.3",
|
||||||
"@nestjs/typeorm": "^10.0.1",
|
"@nestjs/serve-static": "^4.0.1",
|
||||||
"@nestjs/websockets": "^10.3.0",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
|
"@nestjs/websockets": "^10.3.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.1",
|
||||||
"fastify": "^4.25.2",
|
"fastify": "^4.26.2",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.2.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pg-tsquery": "^8.4.1",
|
"pg-tsquery": "^8.4.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.2.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sanitize-filename-ts": "^1.0.2",
|
"sanitize-filename-ts": "^1.0.2",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"socket.io": "^4.7.2",
|
"socket.io": "^4.7.5",
|
||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.20",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.2.1",
|
"@nestjs/cli": "^10.3.2",
|
||||||
"@nestjs/schematics": "^10.0.3",
|
"@nestjs/schematics": "^10.1.1",
|
||||||
"@nestjs/testing": "^10.3.0",
|
"@nestjs/testing": "^10.3.3",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/bytes": "^3.1.4",
|
"@types/bytes": "^3.1.4",
|
||||||
"@types/debounce": "^1.2.4",
|
"@types/debounce": "^1.2.4",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/jest": "^29.5.10",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.11.28",
|
||||||
"@types/supertest": "^2.0.16",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
"eslint": "^8.54.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.2.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.4",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.2",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { join } from 'path';
|
|||||||
CollaborationModule,
|
CollaborationModule,
|
||||||
WsModule,
|
WsModule,
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: join(__dirname, '..', '..', 'client/dist'),
|
rootPath: join(__dirname, '..', '..', '..', 'client/dist'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@ -12,18 +12,18 @@ import {
|
|||||||
import { AttachmentService } from './attachment.service';
|
import { AttachmentService } from './attachment.service';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { AttachmentInterceptor } from './attachment.interceptor';
|
import { AttachmentInterceptor } from './attachment.interceptor';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import * as bytes from 'bytes';
|
import * as bytes from 'bytes';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@Controller('attachments')
|
@Controller('attachments')
|
||||||
export class AttachmentController {
|
export class AttachmentController {
|
||||||
constructor(private readonly attachmentService: AttachmentService) {}
|
constructor(private readonly attachmentService: AttachmentService) {}
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@Post('upload/avatar')
|
@Post('upload/avatar')
|
||||||
@UseInterceptors(AttachmentInterceptor)
|
@UseInterceptors(AttachmentInterceptor)
|
||||||
@ -50,7 +50,7 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@Post('upload/workspace-logo')
|
@Post('upload/workspace-logo')
|
||||||
@UseInterceptors(AttachmentInterceptor)
|
@UseInterceptors(AttachmentInterceptor)
|
||||||
@ -58,7 +58,7 @@ export class AttachmentController {
|
|||||||
@Req() req: FastifyRequest,
|
@Req() req: FastifyRequest,
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const maxFileSize = bytes('5MB');
|
const maxFileSize = bytes('5MB');
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@Post('upload/file')
|
@Post('upload/file')
|
||||||
@UseInterceptors(AttachmentInterceptor)
|
@UseInterceptors(AttachmentInterceptor)
|
||||||
@ -87,7 +87,7 @@ export class AttachmentController {
|
|||||||
@Req() req: FastifyRequest,
|
@Req() req: FastifyRequest,
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const maxFileSize = bytes('20MB');
|
const maxFileSize = bytes('20MB');
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AttachmentService } from './attachment.service';
|
import { AttachmentService } from './attachment.service';
|
||||||
import { AttachmentController } from './attachment.controller';
|
import { AttachmentController } from './attachment.controller';
|
||||||
import { StorageModule } from '../storage/storage.module';
|
import { StorageModule } from '../../integrations/storage/storage.module';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Attachment } from './entities/attachment.entity';
|
import { Attachment } from './entities/attachment.entity';
|
||||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
import { AttachmentRepository } from './repositories/attachment.repository';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
|
|
||||||
@ -13,7 +12,6 @@ import { WorkspaceModule } from '../workspace/workspace.module';
|
|||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Attachment]),
|
TypeOrmModule.forFeature([Attachment]),
|
||||||
StorageModule,
|
StorageModule,
|
||||||
AuthModule,
|
|
||||||
UserModule,
|
UserModule,
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { StorageService } from '../storage/storage.service';
|
import { StorageService } from '../../integrations/storage/storage.service';
|
||||||
import { MultipartFile } from '@fastify/multipart';
|
import { MultipartFile } from '@fastify/multipart';
|
||||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
import { AttachmentRepository } from './repositories/attachment.repository';
|
||||||
import { Attachment } from './entities/attachment.entity';
|
import { Attachment } from './entities/attachment.entity';
|
||||||
|
|||||||
@ -1,27 +1,47 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
NotFoundException,
|
||||||
Post,
|
Post,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { CreateUserDto } from '../user/dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
|
import { SetupGuard } from './guards/setup.guard';
|
||||||
|
import { EnvironmentService } from '../../environment/environment.service';
|
||||||
|
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private authService: AuthService) {}
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(@Body() loginInput: LoginDto) {
|
async login(@Req() req, @Body() loginInput: LoginDto) {
|
||||||
return await this.authService.login(loginInput);
|
return this.authService.login(loginInput, req.raw.workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('register')
|
@Post('register')
|
||||||
async register(@Body() createUserDto: CreateUserDto) {
|
async register(@Req() req, @Body() createUserDto: CreateUserDto) {
|
||||||
return await this.authService.register(createUserDto);
|
return this.authService.register(createUserDto, req.raw.workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(SetupGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('setup')
|
||||||
|
async setupWorkspace(
|
||||||
|
@Req() req,
|
||||||
|
@Body() createAdminUserDto: CreateAdminUserDto,
|
||||||
|
) {
|
||||||
|
if (this.environmentService.isCloud()) throw new NotFoundException();
|
||||||
|
return this.authService.setup(createAdminUserDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,17 @@ import { AuthService } from './services/auth.service';
|
|||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { EnvironmentService } from '../../environment/environment.service';
|
import { EnvironmentService } from '../../environment/environment.service';
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
|
import { SignupService } from './services/signup.service';
|
||||||
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { SpaceModule } from '../space/space.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
useFactory: async (environmentService: EnvironmentService) => {
|
useFactory: async (environmentService: EnvironmentService) => {
|
||||||
return {
|
return {
|
||||||
global: true,
|
|
||||||
secret: environmentService.getJwtSecret(),
|
secret: environmentService.getJwtSecret(),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
expiresIn: environmentService.getJwtTokenExpiresIn(),
|
expiresIn: environmentService.getJwtTokenExpiresIn(),
|
||||||
@ -18,10 +22,13 @@ import { TokenService } from './services/token.service';
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: [EnvironmentService],
|
inject: [EnvironmentService],
|
||||||
}),
|
} as any),
|
||||||
|
UserModule,
|
||||||
|
WorkspaceModule,
|
||||||
|
SpaceModule,
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, TokenService],
|
providers: [AuthService, SignupService, TokenService, JwtStrategy],
|
||||||
exports: [TokenService],
|
exports: [TokenService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
8
apps/server/src/core/auth/auth.utils.ts
Normal file
8
apps/server/src/core/auth/auth.utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
export async function comparePasswordHash(
|
||||||
|
plainPassword: string,
|
||||||
|
passwordHash: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return bcrypt.compare(plainPassword, passwordHash);
|
||||||
|
}
|
||||||
15
apps/server/src/core/auth/dto/create-admin-user.dto.ts
Normal file
15
apps/server/src/core/auth/dto/create-admin-user.dto.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
import { CreateUserDto } from './create-user.dto';
|
||||||
|
|
||||||
|
export class CreateAdminUserDto extends CreateUserDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(3)
|
||||||
|
@MaxLength(35)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(4)
|
||||||
|
@MaxLength(35)
|
||||||
|
@IsString()
|
||||||
|
workspaceName: string;
|
||||||
|
}
|
||||||
@ -2,13 +2,14 @@ import {
|
|||||||
IsEmail,
|
IsEmail,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString, MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateUserDto {
|
export class CreateUserDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@MinLength(3)
|
@MinLength(3)
|
||||||
|
@MaxLength(35)
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
16
apps/server/src/core/auth/dto/jwt-payload.ts
Normal file
16
apps/server/src/core/auth/dto/jwt-payload.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export enum JwtType {
|
||||||
|
ACCESS = 'access',
|
||||||
|
REFRESH = 'refresh',
|
||||||
|
}
|
||||||
|
export type JwtPayload = {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
workspaceId: string;
|
||||||
|
type: 'access';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JwtRefreshPayload = {
|
||||||
|
sub: string;
|
||||||
|
workspaceId: string;
|
||||||
|
type: 'refresh';
|
||||||
|
};
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import {
|
|
||||||
CanActivate,
|
|
||||||
ExecutionContext,
|
|
||||||
Injectable,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { TokenService } from '../services/token.service';
|
|
||||||
import { UserService } from '../../user/user.service';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { IS_PUBLIC_KEY } from '../../../decorators/public.decorator';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JwtGuard implements CanActivate {
|
|
||||||
constructor(
|
|
||||||
private tokenService: TokenService,
|
|
||||||
private userService: UserService,
|
|
||||||
private reflector: Reflector,
|
|
||||||
) {}
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
||||||
context.getHandler(),
|
|
||||||
context.getClass(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isPublic) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
const token: string = await this.tokenService.extractTokenFromHeader(
|
|
||||||
request,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
throw new UnauthorizedException('Invalid jwt token');
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload;
|
|
||||||
|
|
||||||
try {
|
|
||||||
payload = await this.tokenService.verifyJwt(token);
|
|
||||||
} catch (error) {
|
|
||||||
throw new UnauthorizedException('Could not verify jwt token');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
//fetch user and current workspace data from db
|
|
||||||
request['user'] = await this.userService.getUserInstance(payload.sub);
|
|
||||||
} catch (error) {
|
|
||||||
throw new UnauthorizedException('Failed to fetch auth user');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
apps/server/src/core/auth/guards/setup.guard.ts
Normal file
14
apps/server/src/core/auth/guards/setup.guard.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
|
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SetupGuard implements CanActivate {
|
||||||
|
constructor(private workspaceRepository: WorkspaceRepository) {}
|
||||||
|
async canActivate(): Promise<boolean> {
|
||||||
|
const workspaceCount = await this.workspaceRepository.count();
|
||||||
|
if (workspaceCount > 0) {
|
||||||
|
throw new ForbiddenException('Workspace setup already completed.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +1,57 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { LoginDto } from '../dto/login.dto';
|
import { LoginDto } from '../dto/login.dto';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { CreateUserDto } from '../../user/dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import { UserService } from '../../user/user.service';
|
import { UserService } from '../../user/user.service';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
import { TokensDto } from '../dto/tokens.dto';
|
import { TokensDto } from '../dto/tokens.dto';
|
||||||
|
import { UserRepository } from '../../user/repositories/user.repository';
|
||||||
|
import { comparePasswordHash } from '../auth.utils';
|
||||||
|
import { SignupService } from './signup.service';
|
||||||
|
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
private signupService: SignupService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
|
private userRepository: UserRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async login(loginDto: LoginDto) {
|
async login(loginDto: LoginDto, workspaceId: string) {
|
||||||
const user: User = await this.userService.findByEmail(loginDto.email);
|
const user = await this.userRepository.findOneByEmail(
|
||||||
const invalidCredentialsMessage = 'email or password does not match';
|
loginDto.email,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!user ||
|
!user ||
|
||||||
!(await this.userService.compareHash(loginDto.password, user.password))
|
!(await comparePasswordHash(loginDto.password, user.password))
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedException(invalidCredentialsMessage);
|
throw new UnauthorizedException('email or password does not match');
|
||||||
}
|
}
|
||||||
|
|
||||||
user.lastLoginAt = new Date();
|
user.lastLoginAt = new Date();
|
||||||
|
await this.userRepository.save(user);
|
||||||
|
|
||||||
|
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||||
|
return { tokens };
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(createUserDto: CreateUserDto, workspaceId: string) {
|
||||||
|
const user: User = await this.signupService.signup(
|
||||||
|
createUserDto,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||||
|
|
||||||
return { tokens };
|
return { tokens };
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(createUserDto: CreateUserDto) {
|
async setup(createAdminUserDto: CreateAdminUserDto) {
|
||||||
const user: User = await this.userService.create(createUserDto);
|
const user: User = await this.signupService.firstSetup(createAdminUserDto);
|
||||||
|
|
||||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||||
|
|
||||||
|
|||||||
121
apps/server/src/core/auth/services/signup.service.ts
Normal file
121
apps/server/src/core/auth/services/signup.service.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { transactionWrapper } from '../../../helpers/db.helper';
|
||||||
|
import { UserRepository } from '../../user/repositories/user.repository';
|
||||||
|
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
||||||
|
import { WorkspaceService } from '../../workspace/services/workspace.service';
|
||||||
|
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
|
||||||
|
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||||
|
import { SpaceService } from '../../space/space.service';
|
||||||
|
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SignupService {
|
||||||
|
constructor(
|
||||||
|
private userRepository: UserRepository,
|
||||||
|
private workspaceRepository: WorkspaceRepository,
|
||||||
|
private workspaceService: WorkspaceService,
|
||||||
|
private spaceService: SpaceService,
|
||||||
|
private dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
prepareUser(createUserDto: CreateUserDto): User {
|
||||||
|
const user = new User();
|
||||||
|
user.name = createUserDto.name || createUserDto.email.split('@')[0];
|
||||||
|
user.email = createUserDto.email.toLowerCase();
|
||||||
|
user.password = createUserDto.password;
|
||||||
|
user.locale = 'en';
|
||||||
|
user.lastLoginAt = new Date();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(
|
||||||
|
createUserDto: CreateUserDto,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<User> {
|
||||||
|
return await transactionWrapper(
|
||||||
|
async (transactionManager: EntityManager) => {
|
||||||
|
let user = this.prepareUser(createUserDto);
|
||||||
|
user = await transactionManager.save(user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signup(
|
||||||
|
createUserDto: CreateUserDto,
|
||||||
|
workspaceId: string,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<User> {
|
||||||
|
const userCheck = await this.userRepository.findOneByEmail(
|
||||||
|
createUserDto.email,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
if (userCheck) {
|
||||||
|
throw new BadRequestException('You have an account on this workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await transactionWrapper(
|
||||||
|
async (manager: EntityManager) => {
|
||||||
|
// create user
|
||||||
|
const user = await this.createUser(createUserDto, manager);
|
||||||
|
|
||||||
|
// add user to workspace
|
||||||
|
await this.workspaceService.addUserToWorkspace(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
undefined,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWorkspace(
|
||||||
|
user: User,
|
||||||
|
workspaceName,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<Workspace> {
|
||||||
|
return await transactionWrapper(
|
||||||
|
async (manager: EntityManager) => {
|
||||||
|
// for cloud
|
||||||
|
const workspaceData: CreateWorkspaceDto = {
|
||||||
|
name: workspaceName,
|
||||||
|
// hostname: '', // generate
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.workspaceService.create(user, workspaceData, manager);
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async firstSetup(
|
||||||
|
createAdminUserDto: CreateAdminUserDto,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<User> {
|
||||||
|
return await transactionWrapper(
|
||||||
|
async (manager: EntityManager) => {
|
||||||
|
// create user
|
||||||
|
const user = await this.createUser(createAdminUserDto, manager);
|
||||||
|
await this.createWorkspace(
|
||||||
|
user,
|
||||||
|
createAdminUserDto.workspaceName,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { EnvironmentService } from '../../../environment/environment.service';
|
import { EnvironmentService } from '../../../environment/environment.service';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { FastifyRequest } from 'fastify';
|
|
||||||
import { TokensDto } from '../dto/tokens.dto';
|
import { TokensDto } from '../dto/tokens.dto';
|
||||||
|
import { JwtPayload, JwtRefreshPayload, JwtType } from '../dto/jwt-payload';
|
||||||
export type JwtPayload = { sub: string; email: string };
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
@ -13,31 +11,37 @@ export class TokenService {
|
|||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
async generateJwt(user: User): Promise<string> {
|
|
||||||
|
async generateAccessToken(user: User): Promise<string> {
|
||||||
const payload: JwtPayload = {
|
const payload: JwtPayload = {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
workspaceId: user.workspaceId,
|
||||||
|
type: JwtType.ACCESS,
|
||||||
};
|
};
|
||||||
return await this.jwtService.signAsync(payload);
|
return this.jwtService.sign(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateRefreshToken(userId: string, workspaceId): Promise<string> {
|
||||||
|
const payload: JwtRefreshPayload = {
|
||||||
|
sub: userId,
|
||||||
|
workspaceId,
|
||||||
|
type: JwtType.REFRESH,
|
||||||
|
};
|
||||||
|
const expiresIn = '30d'; // todo: fix
|
||||||
|
return this.jwtService.sign(payload, { expiresIn });
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateTokens(user: User): Promise<TokensDto> {
|
async generateTokens(user: User): Promise<TokensDto> {
|
||||||
return {
|
return {
|
||||||
accessToken: await this.generateJwt(user),
|
accessToken: await this.generateAccessToken(user),
|
||||||
refreshToken: null,
|
refreshToken: await this.generateRefreshToken(user.id, user.workspaceId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyJwt(token: string) {
|
async verifyJwt(token: string) {
|
||||||
return await this.jwtService.verifyAsync(token, {
|
return this.jwtService.verifyAsync(token, {
|
||||||
secret: this.environmentService.getJwtSecret(),
|
secret: this.environmentService.getJwtSecret(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractTokenFromHeader(
|
|
||||||
request: FastifyRequest,
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
|
||||||
return type === 'Bearer' ? token : undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
64
apps/server/src/core/auth/strategies/jwt.strategy.ts
Normal file
64
apps/server/src/core/auth/strategies/jwt.strategy.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { EnvironmentService } from '../../../environment/environment.service';
|
||||||
|
import { JwtPayload, JwtType } from '../dto/jwt-payload';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { UserRepository } from '../../user/repositories/user.repository';
|
||||||
|
import { UserService } from '../../user/user.service';
|
||||||
|
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private userService: UserService,
|
||||||
|
private userRepository: UserRepository,
|
||||||
|
private workspaceRepository: WorkspaceRepository,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: environmentService.getJwtSecret(),
|
||||||
|
passReqToCallback: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(req, payload: JwtPayload) {
|
||||||
|
// CLOUD ENV
|
||||||
|
if (this.environmentService.isCloud()) {
|
||||||
|
if (req.raw.workspaceId && req.raw.workspaceId !== payload.workspaceId) {
|
||||||
|
throw new BadRequestException('Workspace does not match');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.workspaceId || payload.type !== JwtType.ACCESS) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await this.workspaceRepository.findById(
|
||||||
|
payload.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: payload.sub,
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, workspace };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,9 +9,8 @@ import {
|
|||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { Action } from '../ability.action';
|
import { Action } from '../ability.action';
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||||
import { WorkspaceUser } from '../../workspace/entities/workspace-user.entity';
|
|
||||||
import { WorkspaceInvitation } from '../../workspace/entities/workspace-invitation.entity';
|
import { WorkspaceInvitation } from '../../workspace/entities/workspace-invitation.entity';
|
||||||
import { Role } from '../../../helpers/types/permission';
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
import { Group } from '../../group/entities/group.entity';
|
import { Group } from '../../group/entities/group.entity';
|
||||||
import { GroupUser } from '../../group/entities/group-user.entity';
|
import { GroupUser } from '../../group/entities/group-user.entity';
|
||||||
import { Attachment } from '../../attachment/entities/attachment.entity';
|
import { Attachment } from '../../attachment/entities/attachment.entity';
|
||||||
@ -23,7 +22,6 @@ import { Comment } from '../../comment/entities/comment.entity';
|
|||||||
export type Subjects =
|
export type Subjects =
|
||||||
| InferSubjects<
|
| InferSubjects<
|
||||||
| typeof Workspace
|
| typeof Workspace
|
||||||
| typeof WorkspaceUser
|
|
||||||
| typeof WorkspaceInvitation
|
| typeof WorkspaceInvitation
|
||||||
| typeof Space
|
| typeof Space
|
||||||
| typeof SpaceUser
|
| typeof SpaceUser
|
||||||
@ -34,6 +32,7 @@ export type Subjects =
|
|||||||
| typeof Page
|
| typeof Page
|
||||||
| typeof User
|
| typeof User
|
||||||
>
|
>
|
||||||
|
| 'workspaceUser'
|
||||||
| 'all';
|
| 'all';
|
||||||
export type AppAbility = MongoAbility<[Action, Subjects]>;
|
export type AppAbility = MongoAbility<[Action, Subjects]>;
|
||||||
|
|
||||||
@ -42,13 +41,13 @@ export default class CaslAbilityFactory {
|
|||||||
createForWorkspace(user: User, workspace: Workspace) {
|
createForWorkspace(user: User, workspace: Workspace) {
|
||||||
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
||||||
|
|
||||||
const userRole = workspace?.workspaceUser.role;
|
const userRole = user.role;
|
||||||
console.log(userRole);
|
|
||||||
|
|
||||||
if (userRole === Role.OWNER) {
|
if (userRole === UserRole.OWNER || userRole === UserRole.ADMIN) {
|
||||||
// Workspace Users
|
// Workspace Users
|
||||||
can<any>([Action.Manage], Workspace);
|
can<any>([Action.Manage], Workspace);
|
||||||
can<any>([Action.Manage], WorkspaceUser);
|
can<any>([Action.Manage], 'workspaceUser');
|
||||||
|
|
||||||
can<any>([Action.Manage], WorkspaceInvitation);
|
can<any>([Action.Manage], WorkspaceInvitation);
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
@ -59,8 +58,8 @@ export default class CaslAbilityFactory {
|
|||||||
can<any>([Action.Manage], Attachment);
|
can<any>([Action.Manage], Attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userRole === Role.MEMBER) {
|
if (userRole === UserRole.MEMBER) {
|
||||||
can<any>([Action.Read], WorkspaceUser);
|
// can<any>([Action.Read], WorkspaceUser);
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
can<any>([Action.Read], Group);
|
can<any>([Action.Read], Group);
|
||||||
|
|||||||
@ -21,8 +21,8 @@ export class PoliciesGuard implements CanActivate {
|
|||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const user = request['user'].user;
|
const user = request.user.user;
|
||||||
const workspace = request['user'].workspace;
|
const workspace = request.user.workspace;
|
||||||
|
|
||||||
const ability = this.caslAbilityFactory.createForWorkspace(user, workspace);
|
const ability = this.caslAbilityFactory.createForWorkspace(user, workspace);
|
||||||
|
|
||||||
|
|||||||
@ -9,16 +9,15 @@ import {
|
|||||||
import { CommentService } from './comment.service';
|
import { CommentService } from './comment.service';
|
||||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||||
import { UpdateCommentDto } from './dto/update-comment.dto';
|
import { UpdateCommentDto } from './dto/update-comment.dto';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import { CommentsInput, SingleCommentInput } from './dto/comments.input';
|
import { CommentsInput, SingleCommentInput } from './dto/comments.input';
|
||||||
import { ResolveCommentDto } from './dto/resolve-comment.dto';
|
import { ResolveCommentDto } from './dto/resolve-comment.dto';
|
||||||
import { WorkspaceService } from '../workspace/services/workspace.service';
|
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('comments')
|
@Controller('comments')
|
||||||
export class CommentController {
|
export class CommentController {
|
||||||
constructor(private readonly commentService: CommentService) {}
|
constructor(private readonly commentService: CommentService) {}
|
||||||
@ -28,7 +27,7 @@ export class CommentController {
|
|||||||
async create(
|
async create(
|
||||||
@Body() createCommentDto: CreateCommentDto,
|
@Body() createCommentDto: CreateCommentDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.commentService.create(user.id, workspace.id, createCommentDto);
|
return this.commentService.create(user.id, workspace.id, createCommentDto);
|
||||||
}
|
}
|
||||||
@ -40,7 +39,7 @@ export class CommentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('view')
|
@Post('info')
|
||||||
findOne(@Body() input: SingleCommentInput) {
|
findOne(@Body() input: SingleCommentInput) {
|
||||||
return this.commentService.findWithCreator(input.id);
|
return this.commentService.findWithCreator(input.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,11 @@ import { CommentService } from './comment.service';
|
|||||||
import { CommentController } from './comment.controller';
|
import { CommentController } from './comment.controller';
|
||||||
import { CommentRepository } from './repositories/comment.repository';
|
import { CommentRepository } from './repositories/comment.repository';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { Comment } from './entities/comment.entity';
|
import { Comment } from './entities/comment.entity';
|
||||||
import { PageModule } from '../page/page.module';
|
import { PageModule } from '../page/page.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Comment]), AuthModule, PageModule],
|
imports: [TypeOrmModule.forFeature([Comment]), PageModule],
|
||||||
controllers: [CommentController],
|
controllers: [CommentController],
|
||||||
providers: [CommentService, CommentRepository],
|
providers: [CommentService, CommentRepository],
|
||||||
exports: [CommentService, CommentRepository],
|
exports: [CommentService, CommentRepository],
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import {
|
||||||
|
MiddlewareConsumer,
|
||||||
|
Module,
|
||||||
|
NestModule,
|
||||||
|
RequestMethod,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { WorkspaceModule } from './workspace/workspace.module';
|
import { WorkspaceModule } from './workspace/workspace.module';
|
||||||
import { PageModule } from './page/page.module';
|
import { PageModule } from './page/page.module';
|
||||||
import { StorageModule } from './storage/storage.module';
|
import { StorageModule } from '../integrations/storage/storage.module';
|
||||||
import { AttachmentModule } from './attachment/attachment.module';
|
import { AttachmentModule } from './attachment/attachment.module';
|
||||||
import { EnvironmentModule } from '../environment/environment.module';
|
import { EnvironmentModule } from '../environment/environment.module';
|
||||||
import { CommentModule } from './comment/comment.module';
|
import { CommentModule } from './comment/comment.module';
|
||||||
@ -11,6 +16,7 @@ import { SearchModule } from './search/search.module';
|
|||||||
import { SpaceModule } from './space/space.module';
|
import { SpaceModule } from './space/space.module';
|
||||||
import { GroupModule } from './group/group.module';
|
import { GroupModule } from './group/group.module';
|
||||||
import { CaslModule } from './casl/casl.module';
|
import { CaslModule } from './casl/casl.module';
|
||||||
|
import { DomainMiddleware } from '../middlewares/domain.middleware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -29,4 +35,11 @@ import { CaslModule } from './casl/casl.module';
|
|||||||
CaslModule,
|
CaslModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {}
|
export class CoreModule implements NestModule {
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer
|
||||||
|
.apply(DomainMiddleware)
|
||||||
|
.exclude({ path: 'auth/setup', method: RequestMethod.POST })
|
||||||
|
.forRoutes('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export class GroupUser {
|
|||||||
@Column()
|
@Column()
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.workspaceUsers, {
|
@ManyToOne(() => User, (user) => user.groups, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'userId' })
|
@JoinColumn({ name: 'userId' })
|
||||||
|
|||||||
@ -8,9 +8,8 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { GroupService } from './services/group.service';
|
import { GroupService } from './services/group.service';
|
||||||
import { CreateGroupDto } from './dto/create-group.dto';
|
import { CreateGroupDto } from './dto/create-group.dto';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
import { GroupUserService } from './services/group-user.service';
|
import { GroupUserService } from './services/group-user.service';
|
||||||
@ -25,8 +24,9 @@ import { GroupUser } from './entities/group-user.entity';
|
|||||||
import { PoliciesGuard } from '../casl/guards/policies.guard';
|
import { PoliciesGuard } from '../casl/guards/policies.guard';
|
||||||
import { CheckPolicies } from '../casl/decorators/policies.decorator';
|
import { CheckPolicies } from '../casl/decorators/policies.decorator';
|
||||||
import { AppAbility } from '../casl/abilities/casl-ability.factory';
|
import { AppAbility } from '../casl/abilities/casl-ability.factory';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('groups')
|
@Controller('groups')
|
||||||
export class GroupController {
|
export class GroupController {
|
||||||
constructor(
|
constructor(
|
||||||
@ -39,21 +39,21 @@ export class GroupController {
|
|||||||
getWorkspaceGroups(
|
getWorkspaceGroups(
|
||||||
@Body() pagination: PaginationOptions,
|
@Body() pagination: PaginationOptions,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupService.getGroupsInWorkspace(workspace.id, pagination);
|
return this.groupService.getWorkspaceGroups(workspace.id, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Group))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Group))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/details')
|
@Post('/info')
|
||||||
getGroup(
|
getGroup(
|
||||||
@Body() groupIdDto: GroupIdDto,
|
@Body() groupIdDto: GroupIdDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupService.getGroup(groupIdDto.groupId, workspace.id);
|
return this.groupService.getGroupInfo(groupIdDto.groupId, workspace.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@ -63,7 +63,7 @@ export class GroupController {
|
|||||||
createGroup(
|
createGroup(
|
||||||
@Body() createGroupDto: CreateGroupDto,
|
@Body() createGroupDto: CreateGroupDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupService.createGroup(user, workspace.id, createGroupDto);
|
return this.groupService.createGroup(user, workspace.id, createGroupDto);
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ export class GroupController {
|
|||||||
updateGroup(
|
updateGroup(
|
||||||
@Body() updateGroupDto: UpdateGroupDto,
|
@Body() updateGroupDto: UpdateGroupDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupService.updateGroup(workspace.id, updateGroupDto);
|
return this.groupService.updateGroup(workspace.id, updateGroupDto);
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ export class GroupController {
|
|||||||
getGroupMembers(
|
getGroupMembers(
|
||||||
@Body() groupIdDto: GroupIdDto,
|
@Body() groupIdDto: GroupIdDto,
|
||||||
@Body() pagination: PaginationOptions,
|
@Body() pagination: PaginationOptions,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupUserService.getGroupUsers(
|
return this.groupUserService.getGroupUsers(
|
||||||
groupIdDto.groupId,
|
groupIdDto.groupId,
|
||||||
@ -103,7 +103,7 @@ export class GroupController {
|
|||||||
addGroupMember(
|
addGroupMember(
|
||||||
@Body() addGroupUserDto: AddGroupUserDto,
|
@Body() addGroupUserDto: AddGroupUserDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupUserService.addUserToGroup(
|
return this.groupUserService.addUserToGroup(
|
||||||
addGroupUserDto.userId,
|
addGroupUserDto.userId,
|
||||||
@ -134,7 +134,7 @@ export class GroupController {
|
|||||||
deleteGroup(
|
deleteGroup(
|
||||||
@Body() groupIdDto: GroupIdDto,
|
@Body() groupIdDto: GroupIdDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.groupService.deleteGroup(groupIdDto.groupId, workspace.id);
|
return this.groupService.deleteGroup(groupIdDto.groupId, workspace.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
|||||||
import { GroupService } from './services/group.service';
|
import { GroupService } from './services/group.service';
|
||||||
import { GroupController } from './group.controller';
|
import { GroupController } from './group.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { Group } from './entities/group.entity';
|
import { Group } from './entities/group.entity';
|
||||||
import { GroupUser } from './entities/group-user.entity';
|
import { GroupUser } from './entities/group-user.entity';
|
||||||
import { GroupRepository } from './respositories/group.repository';
|
import { GroupRepository } from './respositories/group.repository';
|
||||||
@ -10,7 +9,7 @@ import { GroupUserRepository } from './respositories/group-user.repository';
|
|||||||
import { GroupUserService } from './services/group-user.service';
|
import { GroupUserService } from './services/group-user.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Group, GroupUser]), AuthModule],
|
imports: [TypeOrmModule.forFeature([Group, GroupUser])],
|
||||||
controllers: [GroupController],
|
controllers: [GroupController],
|
||||||
providers: [
|
providers: [
|
||||||
GroupService,
|
GroupService,
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
import { GroupUserRepository } from '../respositories/group-user.repository';
|
import { GroupUserRepository } from '../respositories/group-user.repository';
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { WorkspaceUser } from '../../workspace/entities/workspace-user.entity';
|
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
import { transactionWrapper } from '../../../helpers/db.helper';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { GroupUser } from '../entities/group-user.entity';
|
import { GroupUser } from '../entities/group-user.entity';
|
||||||
@ -55,9 +54,10 @@ export class GroupUserService {
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
manager?: EntityManager,
|
manager?: EntityManager,
|
||||||
): Promise<WorkspaceUser> {
|
): Promise<any> {
|
||||||
let addedUser;
|
let addedUser;
|
||||||
|
|
||||||
|
/*
|
||||||
await transactionWrapper(
|
await transactionWrapper(
|
||||||
async (manager) => {
|
async (manager) => {
|
||||||
const group = await manager.findOneBy(Group, {
|
const group = await manager.findOneBy(Group, {
|
||||||
@ -106,7 +106,7 @@ export class GroupUserService {
|
|||||||
this.dataSource,
|
this.dataSource,
|
||||||
manager,
|
manager,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
return addedUser;
|
return addedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class GroupService {
|
|||||||
return await this.groupRepository.save(group);
|
return await this.groupRepository.save(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroup(groupId: string, workspaceId: string): Promise<Group> {
|
async getGroupInfo(groupId: string, workspaceId: string): Promise<Group> {
|
||||||
const group = await this.groupRepository
|
const group = await this.groupRepository
|
||||||
.createQueryBuilder('group')
|
.createQueryBuilder('group')
|
||||||
.where('group.id = :groupId', { groupId })
|
.where('group.id = :groupId', { groupId })
|
||||||
@ -68,11 +68,11 @@ export class GroupService {
|
|||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupsInWorkspace(
|
async getWorkspaceGroups(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
paginationOptions: PaginationOptions,
|
||||||
): Promise<PaginatedResult<Group>> {
|
): Promise<PaginatedResult<Group>> {
|
||||||
const [groupsInWorkspace, count] = await this.groupRepository
|
const [groups, count] = await this.groupRepository
|
||||||
.createQueryBuilder('group')
|
.createQueryBuilder('group')
|
||||||
.where('group.workspaceId = :workspaceId', { workspaceId })
|
.where('group.workspaceId = :workspaceId', { workspaceId })
|
||||||
.loadRelationCountAndMap(
|
.loadRelationCountAndMap(
|
||||||
@ -86,7 +86,7 @@ export class GroupService {
|
|||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
return new PaginatedResult(groupsInWorkspace, paginationMeta);
|
return new PaginatedResult(groups, paginationMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteGroup(groupId: string, workspaceId: string): Promise<void> {
|
async deleteGroup(groupId: string, workspaceId: string): Promise<void> {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
import { PageService } from './services/page.service';
|
import { PageService } from './services/page.service';
|
||||||
import { CreatePageDto } from './dto/create-page.dto';
|
import { CreatePageDto } from './dto/create-page.dto';
|
||||||
import { UpdatePageDto } from './dto/update-page.dto';
|
import { UpdatePageDto } from './dto/update-page.dto';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import { MovePageDto } from './dto/move-page.dto';
|
import { MovePageDto } from './dto/move-page.dto';
|
||||||
import { PageDetailsDto } from './dto/page-details.dto';
|
import { PageDetailsDto } from './dto/page-details.dto';
|
||||||
import { DeletePageDto } from './dto/delete-page.dto';
|
import { DeletePageDto } from './dto/delete-page.dto';
|
||||||
@ -19,10 +18,11 @@ import { HistoryDetailsDto } from './dto/history-details.dto';
|
|||||||
import { PageHistoryDto } from './dto/page-history.dto';
|
import { PageHistoryDto } from './dto/page-history.dto';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('pages')
|
@Controller('pages')
|
||||||
export class PageController {
|
export class PageController {
|
||||||
constructor(
|
constructor(
|
||||||
@ -32,7 +32,7 @@ export class PageController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/details')
|
@Post('/info')
|
||||||
async getPage(@Body() input: PageDetailsDto) {
|
async getPage(@Body() input: PageDetailsDto) {
|
||||||
return this.pageService.findOne(input.id);
|
return this.pageService.findOne(input.id);
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ export class PageController {
|
|||||||
async create(
|
async create(
|
||||||
@Body() createPageDto: CreatePageDto,
|
@Body() createPageDto: CreatePageDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.pageService.create(user.id, workspace.id, createPageDto);
|
return this.pageService.create(user.id, workspace.id, createPageDto);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { PageController } from './page.controller';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Page } from './entities/page.entity';
|
import { Page } from './entities/page.entity';
|
||||||
import { PageRepository } from './repositories/page.repository';
|
import { PageRepository } from './repositories/page.repository';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
import { PageOrderingService } from './services/page-ordering.service';
|
import { PageOrderingService } from './services/page-ordering.service';
|
||||||
import { PageOrdering } from './entities/page-ordering.entity';
|
import { PageOrdering } from './entities/page-ordering.entity';
|
||||||
@ -15,7 +14,6 @@ import { PageHistoryRepository } from './repositories/page-history.repository';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Page, PageOrdering, PageHistory]),
|
TypeOrmModule.forFeature([Page, PageOrdering, PageHistory]),
|
||||||
AuthModule,
|
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
],
|
],
|
||||||
controllers: [PageController],
|
controllers: [PageController],
|
||||||
|
|||||||
@ -7,13 +7,13 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { SearchDTO } from './dto/search.dto';
|
import { SearchDTO } from './dto/search.dto';
|
||||||
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('search')
|
@Controller('search')
|
||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private readonly searchService: SearchService) {}
|
constructor(private readonly searchService: SearchService) {}
|
||||||
@ -23,7 +23,7 @@ export class SearchController {
|
|||||||
async pageSearch(
|
async pageSearch(
|
||||||
@Query('type') type: string,
|
@Query('type') type: string,
|
||||||
@Body() searchDto: SearchDTO,
|
@Body() searchDto: SearchDTO,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
if (!type || type === 'page') {
|
if (!type || type === 'page') {
|
||||||
return this.searchService.searchPage(
|
return this.searchService.searchPage(
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SearchController } from './search.controller';
|
import { SearchController } from './search.controller';
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { PageModule } from '../page/page.module';
|
import { PageModule } from '../page/page.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, PageModule],
|
imports: [PageModule],
|
||||||
controllers: [SearchController],
|
controllers: [SearchController],
|
||||||
providers: [SearchService],
|
providers: [SearchService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,4 +9,8 @@ export class CreateSpaceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
8
apps/server/src/core/space/dto/space-id.dto.ts
Normal file
8
apps/server/src/core/space/dto/space-id.dto.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class SpaceIdDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
spaceId: string;
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ export class SpaceUser {
|
|||||||
@Column()
|
@Column()
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.spaceUsers, {
|
@ManyToOne(() => User, (user) => user.spaces, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'userId' })
|
@JoinColumn({ name: 'userId' })
|
||||||
@ -32,7 +32,6 @@ export class SpaceUser {
|
|||||||
@ManyToOne(() => Space, (space) => space.spaceUsers, {
|
@ManyToOne(() => Space, (space) => space.spaceUsers, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'spaceId' })
|
|
||||||
space: Space;
|
space: Space;
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
@Column({ length: 100, nullable: true })
|
||||||
|
|||||||
@ -6,14 +6,17 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||||
import { SpaceUser } from './space-user.entity';
|
import { SpaceUser } from './space-user.entity';
|
||||||
import { Page } from '../../page/entities/page.entity';
|
import { Page } from '../../page/entities/page.entity';
|
||||||
|
import { SpacePrivacy, SpaceRole } from '../../../helpers/types/permission';
|
||||||
|
|
||||||
@Entity('spaces')
|
@Entity('spaces')
|
||||||
|
@Unique(['slug', 'workspaceId'])
|
||||||
export class Space {
|
export class Space {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
@ -24,11 +27,17 @@ export class Space {
|
|||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
@Column({ length: 255, nullable: true })
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true, unique: true })
|
@Column({ length: 100, default: SpacePrivacy.OPEN })
|
||||||
hostname: string;
|
privacy: string;
|
||||||
|
|
||||||
|
@Column({ length: 100, default: SpaceRole.WRITER })
|
||||||
|
defaultRole: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
@ -46,7 +55,7 @@ export class Space {
|
|||||||
@JoinColumn({ name: 'workspaceId' })
|
@JoinColumn({ name: 'workspaceId' })
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
|
|
||||||
@OneToMany(() => SpaceUser, (workspaceUser) => workspaceUser.space)
|
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space)
|
||||||
spaceUsers: SpaceUser[];
|
spaceUsers: SpaceUser[];
|
||||||
|
|
||||||
@OneToMany(() => Page, (page) => page.space)
|
@OneToMany(() => Page, (page) => page.space)
|
||||||
|
|||||||
@ -8,7 +8,11 @@ export class SpaceRepository extends Repository<Space> {
|
|||||||
super(Space, dataSource.createEntityManager());
|
super(Space, dataSource.createEntityManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(spaceId: string) {
|
async findById(spaceId: string, workspaceId: string): Promise<Space> {
|
||||||
return this.findOneBy({ id: spaceId });
|
const queryBuilder = this.dataSource.createQueryBuilder(Space, 'space');
|
||||||
|
return await queryBuilder
|
||||||
|
.where('space.id = :id', { id: spaceId })
|
||||||
|
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
|
||||||
|
.getOne();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,72 @@
|
|||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import { SpaceService } from './space.service';
|
import { SpaceService } from './space.service';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { CurrentWorkspace } from '../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
import { SpaceIdDto } from './dto/space-id.dto';
|
||||||
|
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('spaces')
|
@Controller('spaces')
|
||||||
export class SpaceController {
|
export class SpaceController {
|
||||||
constructor(private readonly spaceService: SpaceService) {}
|
constructor(private readonly spaceService: SpaceService) {}
|
||||||
|
|
||||||
// get all spaces user is a member of
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async getUserSpaces(
|
async getWorkspaceSpaces(
|
||||||
|
@Body()
|
||||||
|
pagination: PaginationOptions,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.spaceService.getUserSpacesInWorkspace(user.id, workspace.id);
|
// TODO: only show spaces user can see. e.g open and private with user being a member
|
||||||
|
return this.spaceService.getWorkspaceSpaces(workspace.id, pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all spaces user is a member of
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('user')
|
||||||
|
async getUserSpaces(
|
||||||
|
@Body()
|
||||||
|
pagination: PaginationOptions,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.spaceService.getUserSpaces(user.id, workspace.id, pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('info')
|
||||||
|
async getSpaceInfo(
|
||||||
|
@Body() spaceIdDto: SpaceIdDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.spaceService.getSpaceInfo(spaceIdDto.spaceId, workspace.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('members')
|
||||||
|
async getSpaceMembers(
|
||||||
|
@Body() spaceIdDto: SpaceIdDto,
|
||||||
|
@Body()
|
||||||
|
pagination: PaginationOptions,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.spaceService.getSpaceUsers(
|
||||||
|
spaceIdDto.spaceId,
|
||||||
|
workspace.id,
|
||||||
|
pagination,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,12 @@ import { SpaceService } from './space.service';
|
|||||||
import { SpaceController } from './space.controller';
|
import { SpaceController } from './space.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Space } from './entities/space.entity';
|
import { Space } from './entities/space.entity';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { SpaceUser } from './entities/space-user.entity';
|
import { SpaceUser } from './entities/space-user.entity';
|
||||||
import { SpaceRepository } from './repositories/space.repository';
|
import { SpaceRepository } from './repositories/space.repository';
|
||||||
import { SpaceUserRepository } from './repositories/space-user.repository';
|
import { SpaceUserRepository } from './repositories/space-user.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Space, SpaceUser]), AuthModule],
|
imports: [TypeOrmModule.forFeature([Space, SpaceUser])],
|
||||||
controllers: [SpaceController],
|
controllers: [SpaceController],
|
||||||
providers: [SpaceService, SpaceRepository, SpaceUserRepository],
|
providers: [SpaceService, SpaceRepository, SpaceUserRepository],
|
||||||
exports: [SpaceService, SpaceRepository, SpaceUserRepository],
|
exports: [SpaceService, SpaceRepository, SpaceUserRepository],
|
||||||
|
|||||||
@ -5,14 +5,15 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreateSpaceDto } from './dto/create-space.dto';
|
import { CreateSpaceDto } from './dto/create-space.dto';
|
||||||
import { Space } from './entities/space.entity';
|
import { Space } from './entities/space.entity';
|
||||||
import { plainToInstance } from 'class-transformer';
|
|
||||||
import { SpaceRepository } from './repositories/space.repository';
|
import { SpaceRepository } from './repositories/space.repository';
|
||||||
import { SpaceUserRepository } from './repositories/space-user.repository';
|
import { SpaceUserRepository } from './repositories/space-user.repository';
|
||||||
import { SpaceUser } from './entities/space-user.entity';
|
import { SpaceUser } from './entities/space-user.entity';
|
||||||
import { transactionWrapper } from '../../helpers/db.helper';
|
import { transactionWrapper } from '../../helpers/db.helper';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
import { WorkspaceUser } from '../workspace/entities/workspace-user.entity';
|
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
|
import { PaginationOptions } from '../../helpers/pagination/pagination-options';
|
||||||
|
import { PaginationMetaDto } from '../../helpers/pagination/pagination-meta-dto';
|
||||||
|
import { PaginatedResult } from '../../helpers/pagination/paginated-result';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
@ -24,33 +25,26 @@ export class SpaceService {
|
|||||||
|
|
||||||
async create(
|
async create(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId,
|
workspaceId: string,
|
||||||
createSpaceDto?: CreateSpaceDto,
|
createSpaceDto?: CreateSpaceDto,
|
||||||
manager?: EntityManager,
|
manager?: EntityManager,
|
||||||
) {
|
): Promise<Space> {
|
||||||
let space: Space;
|
return await transactionWrapper(
|
||||||
|
|
||||||
await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
async (manager: EntityManager) => {
|
||||||
if (createSpaceDto) {
|
const space = new Space();
|
||||||
space = plainToInstance(Space, createSpaceDto);
|
space.name = createSpaceDto.name ?? 'untitled space ';
|
||||||
} else {
|
space.description = createSpaceDto.description ?? '';
|
||||||
space = new Space();
|
|
||||||
}
|
|
||||||
|
|
||||||
space.creatorId = userId;
|
space.creatorId = userId;
|
||||||
space.workspaceId = workspaceId;
|
space.workspaceId = workspaceId;
|
||||||
|
|
||||||
space.name = createSpaceDto?.name ?? 'untitled space';
|
space.slug = space.name.toLowerCase(); // TODO: fix
|
||||||
space.description = createSpaceDto?.description ?? null;
|
|
||||||
|
|
||||||
space = await manager.save(space);
|
await manager.save(space);
|
||||||
|
return space;
|
||||||
},
|
},
|
||||||
this.dataSource,
|
this.dataSource,
|
||||||
manager,
|
manager,
|
||||||
);
|
);
|
||||||
|
|
||||||
return space;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUserToSpace(
|
async addUserToSpace(
|
||||||
@ -60,27 +54,15 @@ export class SpaceService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
manager?: EntityManager,
|
manager?: EntityManager,
|
||||||
): Promise<SpaceUser> {
|
): Promise<SpaceUser> {
|
||||||
let addedUser: SpaceUser;
|
return await transactionWrapper(
|
||||||
|
|
||||||
await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
async (manager: EntityManager) => {
|
||||||
const userExists = await manager.exists(User, {
|
const userExists = await manager.exists(User, {
|
||||||
where: { id: userId },
|
where: { id: userId, workspaceId },
|
||||||
});
|
});
|
||||||
if (!userExists) {
|
if (!userExists) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// only workspace users can be added to workspace spaces
|
|
||||||
const workspaceUser = await manager.findOneBy(WorkspaceUser, {
|
|
||||||
userId: userId,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!workspaceUser) {
|
|
||||||
throw new NotFoundException('User is not a member of this workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingSpaceUser = await manager.findOneBy(SpaceUser, {
|
const existingSpaceUser = await manager.findOneBy(SpaceUser, {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
spaceId: spaceId,
|
spaceId: spaceId,
|
||||||
@ -94,27 +76,106 @@ export class SpaceService {
|
|||||||
spaceUser.userId = userId;
|
spaceUser.userId = userId;
|
||||||
spaceUser.spaceId = spaceId;
|
spaceUser.spaceId = spaceId;
|
||||||
spaceUser.role = role;
|
spaceUser.role = role;
|
||||||
|
await manager.save(spaceUser);
|
||||||
|
|
||||||
addedUser = await manager.save(spaceUser);
|
return spaceUser;
|
||||||
},
|
},
|
||||||
this.dataSource,
|
this.dataSource,
|
||||||
manager,
|
manager,
|
||||||
);
|
);
|
||||||
|
|
||||||
return addedUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSpacesInWorkspace(userId: string, workspaceId: string) {
|
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
|
||||||
const spaces = await this.spaceUserRepository.find({
|
const space = await this.spaceRepository
|
||||||
relations: ['space'],
|
.createQueryBuilder('space')
|
||||||
|
.where('space.id = :spaceId', { spaceId })
|
||||||
|
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
|
||||||
|
.loadRelationCountAndMap(
|
||||||
|
'space.userCount',
|
||||||
|
'space.spaceUsers',
|
||||||
|
'spaceUsers',
|
||||||
|
)
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
throw new NotFoundException('Space not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return space;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkspaceSpaces(
|
||||||
|
workspaceId: string,
|
||||||
|
paginationOptions: PaginationOptions,
|
||||||
|
): Promise<PaginatedResult<Space>> {
|
||||||
|
const [spaces, count] = await this.spaceRepository
|
||||||
|
.createQueryBuilder('space')
|
||||||
|
.where('space.workspaceId = :workspaceId', { workspaceId })
|
||||||
|
.loadRelationCountAndMap(
|
||||||
|
'space.userCount',
|
||||||
|
'space.spaceUsers',
|
||||||
|
'spaceUsers',
|
||||||
|
)
|
||||||
|
.take(paginationOptions.limit)
|
||||||
|
.skip(paginationOptions.skip)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
|
||||||
|
return new PaginatedResult(spaces, paginationMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserSpaces(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
paginationOptions: PaginationOptions,
|
||||||
|
) {
|
||||||
|
const [userSpaces, count] = await this.spaceUserRepository
|
||||||
|
.createQueryBuilder('spaceUser')
|
||||||
|
.leftJoinAndSelect('spaceUser.space', 'space')
|
||||||
|
.where('spaceUser.userId = :userId', { userId })
|
||||||
|
.andWhere('space.workspaceId = :workspaceId', { workspaceId })
|
||||||
|
.loadRelationCountAndMap(
|
||||||
|
'space.userCount',
|
||||||
|
'space.spaceUsers',
|
||||||
|
'spaceUsers',
|
||||||
|
)
|
||||||
|
.take(paginationOptions.limit)
|
||||||
|
.skip(paginationOptions.skip)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
const spaces = userSpaces.map((userSpace) => userSpace.space);
|
||||||
|
|
||||||
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
return new PaginatedResult(spaces, paginationMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpaceUsers(
|
||||||
|
spaceId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
paginationOptions: PaginationOptions,
|
||||||
|
) {
|
||||||
|
const [spaceUsers, count] = await this.spaceUserRepository.findAndCount({
|
||||||
|
relations: ['user'],
|
||||||
where: {
|
where: {
|
||||||
userId: userId,
|
|
||||||
space: {
|
space: {
|
||||||
workspaceId: workspaceId,
|
id: spaceId,
|
||||||
|
workspaceId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
take: paginationOptions.limit,
|
||||||
|
skip: paginationOptions.skip,
|
||||||
});
|
});
|
||||||
|
|
||||||
return spaces.map((userSpace: SpaceUser) => userSpace.space);
|
const users = spaceUsers.map((spaceUser) => {
|
||||||
|
delete spaceUser.user.password;
|
||||||
|
return {
|
||||||
|
...spaceUser.user,
|
||||||
|
spaceRole: spaceUser.role,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
return new PaginatedResult(users, paginationMeta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreateUserDto } from './create-user.dto';
|
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserDto extends PartialType(CreateUserDto) {
|
export class UpdateUserDto extends PartialType(CreateUserDto) {
|
||||||
|
|||||||
@ -3,19 +3,22 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||||
import { WorkspaceUser } from '../../workspace/entities/workspace-user.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
import { Page } from '../../page/entities/page.entity';
|
||||||
import { Comment } from '../../comment/entities/comment.entity';
|
import { Comment } from '../../comment/entities/comment.entity';
|
||||||
import { Space } from '../../space/entities/space.entity';
|
import { Space } from '../../space/entities/space.entity';
|
||||||
import { SpaceUser } from '../../space/entities/space-user.entity';
|
import { SpaceUser } from '../../space/entities/space-user.entity';
|
||||||
|
import { Group } from '../../group/entities/group.entity';
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
|
@Unique(['email', 'workspaceId'])
|
||||||
export class User {
|
export class User {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
@ -23,7 +26,7 @@ export class User {
|
|||||||
@Column({ length: 255, nullable: true })
|
@Column({ length: 255, nullable: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ length: 255, unique: true })
|
@Column({ length: 255 })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@ -35,6 +38,15 @@ export class User {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, length: 100 })
|
||||||
|
role: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
workspaceId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Workspace, (workspace) => workspace.users)
|
||||||
|
workspace: Workspace;
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
@Column({ length: 100, nullable: true })
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|
||||||
@ -56,11 +68,8 @@ export class User {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@OneToMany(() => Workspace, (workspace) => workspace.creator)
|
@OneToMany(() => Group, (group) => group.creator)
|
||||||
workspaces: Workspace[];
|
groups: Group[];
|
||||||
|
|
||||||
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.user)
|
|
||||||
workspaceUsers: WorkspaceUser[];
|
|
||||||
|
|
||||||
@OneToMany(() => Page, (page) => page.creator)
|
@OneToMany(() => Page, (page) => page.creator)
|
||||||
createdPages: Page[];
|
createdPages: Page[];
|
||||||
@ -69,10 +78,10 @@ export class User {
|
|||||||
comments: Comment[];
|
comments: Comment[];
|
||||||
|
|
||||||
@OneToMany(() => Space, (space) => space.creator)
|
@OneToMany(() => Space, (space) => space.creator)
|
||||||
spaces: Space[];
|
createdSpaces: Space[];
|
||||||
|
|
||||||
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.user)
|
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.user)
|
||||||
spaceUsers: SpaceUser[];
|
spaces: SpaceUser[];
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
delete this.password;
|
delete this.password;
|
||||||
@ -85,8 +94,3 @@ export class User {
|
|||||||
this.password = await bcrypt.hash(this.password, saltRounds);
|
this.password = await bcrypt.hash(this.password, saltRounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserRole = {
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
export type UserWithRole = User & UserRole;
|
|
||||||
|
|||||||
@ -7,11 +7,29 @@ export class UserRepository extends Repository<User> {
|
|||||||
constructor(private dataSource: DataSource) {
|
constructor(private dataSource: DataSource) {
|
||||||
super(User, dataSource.createEntityManager());
|
super(User, dataSource.createEntityManager());
|
||||||
}
|
}
|
||||||
async findByEmail(email: string) {
|
async findByEmail(email: string): Promise<User> {
|
||||||
return this.findOneBy({ email: email });
|
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
||||||
|
return await queryBuilder.where('user.email = :email', { email }).getOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(userId: string) {
|
async findById(userId: string): Promise<User> {
|
||||||
return this.findOneBy({ id: userId });
|
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
||||||
|
return await queryBuilder.where('user.id = :id', { id: userId }).getOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByEmail(email: string, workspaceId: string): Promise<User> {
|
||||||
|
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
||||||
|
return await queryBuilder
|
||||||
|
.where('user.email = :email', { email })
|
||||||
|
.andWhere('user.workspaceId = :workspaceId', { workspaceId })
|
||||||
|
.getOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdx(userId: string, workspaceId: string): Promise<User> {
|
||||||
|
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
|
||||||
|
return await queryBuilder
|
||||||
|
.where('user.id = :id', { id: userId })
|
||||||
|
.andWhere('user.workspaceId = :workspaceId', { workspaceId })
|
||||||
|
.getOne();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
UseGuards,
|
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
UnauthorizedException,
|
|
||||||
Post,
|
Post,
|
||||||
Body,
|
UnauthorizedException,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(private readonly userService: UserService) {}
|
||||||
@ -28,16 +27,13 @@ export class UserController {
|
|||||||
throw new UnauthorizedException('Invalid user');
|
throw new UnauthorizedException('Invalid user');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user };
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('info')
|
@Post('info')
|
||||||
async getUserInfo(@AuthUser() user: User) {
|
async getUserInfo(@AuthUser() user: User) {
|
||||||
const data: { workspace: Workspace; user: User } =
|
return await this.userService.getUserInstance(user.id);
|
||||||
await this.userService.getUserInstance(user.id);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { UserController } from './user.controller';
|
import { UserController } from './user.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User]), AuthModule, WorkspaceModule],
|
imports: [TypeOrmModule.forFeature([User])],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService, UserRepository],
|
providers: [UserService, UserRepository],
|
||||||
exports: [UserService, UserRepository],
|
exports: [UserService, UserRepository],
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { UserService } from './user.service';
|
|||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from '../auth/dto/create-user.dto';
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
let userService: UserService;
|
let userService: UserService;
|
||||||
@ -63,7 +63,7 @@ describe('UserService', () => {
|
|||||||
lastLoginIp: null,
|
lastLoginIp: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
userRepository.findByEmail.mockResolvedValue(undefined);
|
//userRepository.findByEmail.mockResolvedValue(undefined);
|
||||||
userRepository.save.mockResolvedValue(savedUser);
|
userRepository.save.mockResolvedValue(savedUser);
|
||||||
|
|
||||||
const result = await userService.create(createUserDto);
|
const result = await userService.create(createUserDto);
|
||||||
|
|||||||
@ -3,92 +3,31 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
import { plainToInstance } from 'class-transformer';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { WorkspaceService } from '../workspace/services/workspace.service';
|
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
|
||||||
import { transactionWrapper } from '../../helpers/db.helper';
|
|
||||||
import { CreateWorkspaceDto } from '../workspace/dto/create-workspace.dto';
|
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
|
||||||
|
|
||||||
export type UserWithWorkspace = {
|
|
||||||
user: User;
|
|
||||||
workspace: Workspace;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(private userRepository: UserRepository) {}
|
||||||
private userRepository: UserRepository,
|
|
||||||
private workspaceService: WorkspaceService,
|
|
||||||
private dataSource: DataSource,
|
|
||||||
) {}
|
|
||||||
async create(
|
|
||||||
createUserDto: CreateUserDto,
|
|
||||||
manager?: EntityManager,
|
|
||||||
): Promise<User> {
|
|
||||||
let user: User;
|
|
||||||
|
|
||||||
const existingUser: User = await this.findByEmail(createUserDto.email);
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
throw new BadRequestException('A user with this email already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
|
||||||
user = plainToInstance(User, createUserDto);
|
|
||||||
user.locale = 'en';
|
|
||||||
user.lastLoginAt = new Date();
|
|
||||||
user.name = createUserDto.email.split('@')[0];
|
|
||||||
|
|
||||||
user = await manager.save(User, user);
|
|
||||||
|
|
||||||
const createWorkspaceDto: CreateWorkspaceDto = {
|
|
||||||
name: 'My Workspace',
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.workspaceService.createOrJoinWorkspace(
|
|
||||||
user.id,
|
|
||||||
createWorkspaceDto,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
this.dataSource,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserInstance(userId: string): Promise<UserWithWorkspace> {
|
|
||||||
const user: User = await this.findById(userId);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
let workspace;
|
|
||||||
|
|
||||||
try {
|
|
||||||
workspace = await this.workspaceService.getUserCurrentWorkspace(userId);
|
|
||||||
} catch (error) {
|
|
||||||
//console.log(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, workspace };
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(userId: string) {
|
async findById(userId: string) {
|
||||||
return this.userRepository.findById(userId);
|
return this.userRepository.findById(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByEmail(email: string) {
|
async getUserInstance(userId: string): Promise<any> {
|
||||||
return this.userRepository.findByEmail(email);
|
const user: User = await this.userRepository.findOne({
|
||||||
|
relations: ['workspace'],
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(userId: string, updateUserDto: UpdateUserDto) {
|
async update(userId: string, updateUserDto: UpdateUserDto) {
|
||||||
@ -101,6 +40,7 @@ export class UserService {
|
|||||||
user.name = updateUserDto.name;
|
user.name = updateUserDto.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo need workspace scoping
|
||||||
if (updateUserDto.email && user.email != updateUserDto.email) {
|
if (updateUserDto.email && user.email != updateUserDto.email) {
|
||||||
if (await this.userRepository.findByEmail(updateUserDto.email)) {
|
if (await this.userRepository.findByEmail(updateUserDto.email)) {
|
||||||
throw new BadRequestException('A user with this email already exists');
|
throw new BadRequestException('A user with this email already exists');
|
||||||
@ -114,11 +54,4 @@ export class UserService {
|
|||||||
|
|
||||||
return this.userRepository.save(user);
|
return this.userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async compareHash(
|
|
||||||
plainPassword: string,
|
|
||||||
passwordHash: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return await bcrypt.compare(plainPassword, passwordHash);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,18 +7,14 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { WorkspaceService } from '../services/workspace.service';
|
import { WorkspaceService } from '../services/workspace.service';
|
||||||
import { JwtGuard } from '../../auth/guards/jwt.guard';
|
|
||||||
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||||
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
||||||
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
||||||
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
|
|
||||||
import { AddWorkspaceUserDto } from '../dto/add-workspace-user.dto';
|
|
||||||
import { AuthUser } from '../../../decorators/auth-user.decorator';
|
import { AuthUser } from '../../../decorators/auth-user.decorator';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { CurrentWorkspace } from '../../../decorators/current-workspace.decorator';
|
import { AuthWorkspace } from '../../../decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from '../entities/workspace.entity';
|
import { Workspace } from '../entities/workspace.entity';
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { WorkspaceUserService } from '../services/workspace-user.service';
|
|
||||||
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
|
||||||
import { Public } from '../../../decorators/public.decorator';
|
import { Public } from '../../../decorators/public.decorator';
|
||||||
import {
|
import {
|
||||||
@ -27,14 +23,15 @@ import {
|
|||||||
RevokeInviteDto,
|
RevokeInviteDto,
|
||||||
} from '../dto/invitation.dto';
|
} from '../dto/invitation.dto';
|
||||||
import { Action } from '../../casl/ability.action';
|
import { Action } from '../../casl/ability.action';
|
||||||
import { WorkspaceUser } from '../entities/workspace-user.entity';
|
|
||||||
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
||||||
import { CheckPolicies } from '../../casl/decorators/policies.decorator';
|
import { CheckPolicies } from '../../casl/decorators/policies.decorator';
|
||||||
import { AppAbility } from '../../casl/abilities/casl-ability.factory';
|
import { AppAbility } from '../../casl/abilities/casl-ability.factory';
|
||||||
import { PoliciesGuard } from '../../casl/guards/policies.guard';
|
import { PoliciesGuard } from '../../casl/guards/policies.guard';
|
||||||
|
import { WorkspaceUserService } from '../services/workspace-user.service';
|
||||||
|
import { JwtAuthGuard } from '../../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('workspaces')
|
@Controller('workspace')
|
||||||
export class WorkspaceController {
|
export class WorkspaceController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
@ -43,33 +40,21 @@ export class WorkspaceController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/')
|
@Post('/info')
|
||||||
async getUserWorkspaces(
|
async getWorkspace(
|
||||||
@Body()
|
|
||||||
pagination: PaginationOptions,
|
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceService.getUserWorkspaces(user.id, pagination);
|
return this.workspaceService.getWorkspaceInfo(workspace.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('create')
|
|
||||||
async createWorkspace(
|
|
||||||
@Body() createWorkspaceDto: CreateWorkspaceDto,
|
|
||||||
@AuthUser() user: User,
|
|
||||||
) {
|
|
||||||
return this.workspaceService.create(user.id, createWorkspaceDto);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Workspace))
|
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Workspace))
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('update')
|
@Post('update')
|
||||||
async updateWorkspace(
|
async updateWorkspace(
|
||||||
@Body() updateWorkspaceDto: UpdateWorkspaceDto,
|
@Body() updateWorkspaceDto: UpdateWorkspaceDto,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceService.update(workspace.id, updateWorkspaceDto);
|
return this.workspaceService.update(workspace.id, updateWorkspaceDto);
|
||||||
}
|
}
|
||||||
@ -79,19 +64,19 @@ export class WorkspaceController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
|
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
|
||||||
return this.workspaceService.delete(deleteWorkspaceDto);
|
// return this.workspaceService.delete(deleteWorkspaceDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
ability.can(Action.Read, WorkspaceUser),
|
ability.can(Action.Read, 'workspaceUser'),
|
||||||
)
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members')
|
@Post('members')
|
||||||
async getWorkspaceMembers(
|
async getWorkspaceMembers(
|
||||||
@Body()
|
@Body()
|
||||||
pagination: PaginationOptions,
|
pagination: PaginationOptions,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceUserService.getWorkspaceUsers(
|
return this.workspaceUserService.getWorkspaceUsers(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
@ -100,48 +85,25 @@ export class WorkspaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
// @CheckPolicies((ability: AppAbility) =>
|
||||||
ability.can(Action.Manage, WorkspaceUser),
|
// ability.can(Action.Manage, 'WorkspaceUser'),
|
||||||
)
|
// )
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members/add')
|
@Post('members/deactivate')
|
||||||
async addWorkspaceMember(
|
async deactivateWorkspaceMember() {
|
||||||
@Body() addWorkspaceUserDto: AddWorkspaceUserDto,
|
return this.workspaceUserService.deactivateUser();
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
|
||||||
) {
|
|
||||||
return this.workspaceUserService.addUserToWorkspace(
|
|
||||||
addWorkspaceUserDto.userId,
|
|
||||||
workspace.id,
|
|
||||||
addWorkspaceUserDto.role,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
@UseGuards(PoliciesGuard)
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
@CheckPolicies((ability: AppAbility) =>
|
||||||
ability.can(Action.Manage, WorkspaceUser),
|
ability.can(Action.Manage, 'workspaceUser'),
|
||||||
)
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('members/remove')
|
|
||||||
async removeWorkspaceMember(
|
|
||||||
@Body() removeWorkspaceUserDto: RemoveWorkspaceUserDto,
|
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
|
||||||
) {
|
|
||||||
return this.workspaceUserService.removeUserFromWorkspace(
|
|
||||||
removeWorkspaceUserDto.userId,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(PoliciesGuard)
|
|
||||||
@CheckPolicies((ability: AppAbility) =>
|
|
||||||
ability.can(Action.Manage, WorkspaceUser),
|
|
||||||
)
|
)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('members/role')
|
@Post('members/role')
|
||||||
async updateWorkspaceMemberRole(
|
async updateWorkspaceMemberRole(
|
||||||
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
||||||
@AuthUser() authUser: User,
|
@AuthUser() authUser: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceUserService.updateWorkspaceUserRole(
|
return this.workspaceUserService.updateWorkspaceUserRole(
|
||||||
authUser,
|
authUser,
|
||||||
@ -159,7 +121,7 @@ export class WorkspaceController {
|
|||||||
async inviteUser(
|
async inviteUser(
|
||||||
@Body() inviteUserDto: InviteUserDto,
|
@Body() inviteUserDto: InviteUserDto,
|
||||||
@AuthUser() authUser: User,
|
@AuthUser() authUser: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceInvitationService.createInvitation(
|
return this.workspaceInvitationService.createInvitation(
|
||||||
authUser,
|
authUser,
|
||||||
@ -172,9 +134,9 @@ export class WorkspaceController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('invite/accept')
|
@Post('invite/accept')
|
||||||
async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
|
async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
|
||||||
return this.workspaceInvitationService.acceptInvitation(
|
// return this.workspaceInvitationService.acceptInvitation(
|
||||||
acceptInviteDto.invitationId,
|
// acceptInviteDto.invitationId,
|
||||||
);
|
//);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: authorize permission with guards
|
// TODO: authorize permission with guards
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
|
import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
import { WorkspaceUserRole } from '../entities/workspace-user.entity';
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
|
|
||||||
export class InviteUserDto {
|
export class InviteUserDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@ -9,7 +9,7 @@ export class InviteUserDto {
|
|||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@IsEnum(WorkspaceUserRole)
|
@IsEnum(UserRole)
|
||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
Unique,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { Workspace } from './workspace.entity';
|
|
||||||
import { User } from '../../user/entities/user.entity';
|
|
||||||
|
|
||||||
@Entity('workspace_users')
|
|
||||||
@Unique(['workspaceId', 'userId'])
|
|
||||||
export class WorkspaceUser {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
userId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.workspaceUsers, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'userId' })
|
|
||||||
user: User;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
workspaceId: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Column({ length: 100, nullable: true })
|
|
||||||
role: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WorkspaceUserRole {
|
|
||||||
OWNER = 'owner',
|
|
||||||
ADMIN = 'admin',
|
|
||||||
MEMBER = 'member',
|
|
||||||
}
|
|
||||||
@ -4,18 +4,18 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
OneToMany,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
|
DeleteDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { WorkspaceUser } from './workspace-user.entity';
|
|
||||||
import { Page } from '../../page/entities/page.entity';
|
import { Page } from '../../page/entities/page.entity';
|
||||||
import { WorkspaceInvitation } from './workspace-invitation.entity';
|
import { WorkspaceInvitation } from './workspace-invitation.entity';
|
||||||
import { Comment } from '../../comment/entities/comment.entity';
|
import { Comment } from '../../comment/entities/comment.entity';
|
||||||
import { Space } from '../../space/entities/space.entity';
|
import { Space } from '../../space/entities/space.entity';
|
||||||
import { Group } from '../../group/entities/group.entity';
|
import { Group } from '../../group/entities/group.entity';
|
||||||
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
|
|
||||||
@Entity('workspaces')
|
@Entity('workspaces')
|
||||||
export class Workspace {
|
export class Workspace {
|
||||||
@ -46,12 +46,15 @@ export class Workspace {
|
|||||||
@Column({ type: 'jsonb', nullable: true })
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
settings: any;
|
settings: any;
|
||||||
|
|
||||||
@Column()
|
@Column({ default: UserRole.MEMBER })
|
||||||
|
defaultRole: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.workspaces)
|
//@ManyToOne(() => User, (user) => user.workspaces)
|
||||||
@JoinColumn({ name: 'creatorId' })
|
// @JoinColumn({ name: 'creatorId' })
|
||||||
creator: User;
|
// creator: User;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
defaultSpaceId: string;
|
defaultSpaceId: string;
|
||||||
@ -66,8 +69,11 @@ export class Workspace {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.workspace)
|
@DeleteDateColumn()
|
||||||
workspaceUsers: WorkspaceUser[];
|
deletedAt: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => User, (user) => user.workspace)
|
||||||
|
users: [];
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => WorkspaceInvitation,
|
() => WorkspaceInvitation,
|
||||||
@ -87,5 +93,5 @@ export class Workspace {
|
|||||||
@OneToMany(() => Group, (group) => group.workspace)
|
@OneToMany(() => Group, (group) => group.workspace)
|
||||||
groups: [];
|
groups: [];
|
||||||
|
|
||||||
workspaceUser?: WorkspaceUser;
|
// workspaceUser?: WorkspaceUser;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
|
||||||
import { WorkspaceUser } from '../entities/workspace-user.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WorkspaceUserRepository extends Repository<WorkspaceUser> {
|
|
||||||
constructor(private dataSource: DataSource) {
|
|
||||||
super(WorkspaceUser, dataSource.createEntityManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,7 +8,24 @@ export class WorkspaceRepository extends Repository<Workspace> {
|
|||||||
super(Workspace, dataSource.createEntityManager());
|
super(Workspace, dataSource.createEntityManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(workspaceId: string) {
|
async findById(workspaceId: string): Promise<Workspace> {
|
||||||
return this.findOneBy({ id: workspaceId });
|
// see: https://github.com/typeorm/typeorm/issues/9316
|
||||||
|
const queryBuilder = this.dataSource.createQueryBuilder(
|
||||||
|
Workspace,
|
||||||
|
'workspace',
|
||||||
|
);
|
||||||
|
return await queryBuilder
|
||||||
|
.where('workspace.id = :id', { id: workspaceId })
|
||||||
|
.getOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findFirst(): Promise<Workspace> {
|
||||||
|
const createdWorkspace = await this.find({
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
return createdWorkspace[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { WorkspaceInvitationRepository } from '../repositories/workspace-invitat
|
|||||||
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { WorkspaceService } from './workspace.service';
|
import { WorkspaceService } from './workspace.service';
|
||||||
import { WorkspaceUserService } from './workspace-user.service';
|
|
||||||
import { WorkspaceUserRole } from '../entities/workspace-user.entity';
|
|
||||||
import { UserService } from '../../user/user.service';
|
import { UserService } from '../../user/user.service';
|
||||||
import { InviteUserDto } from '../dto/invitation.dto';
|
import { InviteUserDto } from '../dto/invitation.dto';
|
||||||
|
import { WorkspaceUserService } from './workspace-user.service';
|
||||||
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
|
import { UserRepository } from '../../user/repositories/user.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceInvitationService {
|
export class WorkspaceInvitationService {
|
||||||
@ -15,6 +16,7 @@ export class WorkspaceInvitationService {
|
|||||||
private workspaceService: WorkspaceService,
|
private workspaceService: WorkspaceService,
|
||||||
private workspaceUserService: WorkspaceUserService,
|
private workspaceUserService: WorkspaceUserService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
private userRepository: UserRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findInvitedUserByEmail(
|
async findInvitedUserByEmail(
|
||||||
@ -32,38 +34,18 @@ export class WorkspaceInvitationService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
inviteUserDto: InviteUserDto,
|
inviteUserDto: InviteUserDto,
|
||||||
): Promise<WorkspaceInvitation> {
|
): Promise<WorkspaceInvitation> {
|
||||||
const authUserMembership =
|
|
||||||
await this.workspaceUserService.findWorkspaceUser(
|
|
||||||
authUser.id,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!authUserMembership) {
|
|
||||||
throw new BadRequestException('Inviting user must be a workspace member');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authUserMembership.role != WorkspaceUserRole.OWNER) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'Only workspace owners can invite new members',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const invitedUser = await this.userService.findByEmail(inviteUserDto.email);
|
|
||||||
|
|
||||||
// check if invited user is already a workspace member
|
// check if invited user is already a workspace member
|
||||||
if (invitedUser) {
|
const invitedUser =
|
||||||
const invitedUserMembership =
|
await this.workspaceUserService.findWorkspaceUserByEmail(
|
||||||
await this.workspaceUserService.findWorkspaceUser(
|
inviteUserDto.email,
|
||||||
invitedUser.id,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (invitedUserMembership) {
|
if (invitedUser) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'This user already a member of this workspace',
|
'User is already a member of this workspace',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// check if user was already invited
|
// check if user was already invited
|
||||||
const existingInvitation = await this.findInvitedUserByEmail(
|
const existingInvitation = await this.findInvitedUserByEmail(
|
||||||
@ -72,7 +54,7 @@ export class WorkspaceInvitationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existingInvitation) {
|
if (existingInvitation) {
|
||||||
throw new BadRequestException('This user has already been invited');
|
throw new BadRequestException('User has already been invited');
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitation = new WorkspaceInvitation();
|
const invitation = new WorkspaceInvitation();
|
||||||
@ -97,14 +79,22 @@ export class WorkspaceInvitationService {
|
|||||||
|
|
||||||
// TODO: to be completed
|
// TODO: to be completed
|
||||||
|
|
||||||
// check if user is in the system already
|
// check if user is already a member
|
||||||
const invitedUser = await this.userService.findByEmail(invitation.email);
|
const invitedUser =
|
||||||
|
await this.workspaceUserService.findWorkspaceUserByEmail(
|
||||||
|
invitation.email,
|
||||||
|
invitation.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
if (invitedUser) {
|
if (invitedUser) {
|
||||||
// fetch the workspace
|
throw new BadRequestException(
|
||||||
// add the user to the workspace
|
'User is already a member of this workspace',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return invitation;
|
// add create account for user
|
||||||
|
// add the user to the workspace
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeInvitation(invitationId: string): Promise<void> {
|
async revokeInvitation(invitationId: string): Promise<void> {
|
||||||
|
|||||||
@ -1,69 +1,34 @@
|
|||||||
import {
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
|
|
||||||
import {
|
|
||||||
WorkspaceUser,
|
|
||||||
WorkspaceUserRole,
|
|
||||||
} from '../entities/workspace-user.entity';
|
|
||||||
import { Workspace } from '../entities/workspace.entity';
|
|
||||||
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { WorkspaceRepository } from '../repositories/workspace.repository';
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
import { UserRepository } from '../../user/repositories/user.repository';
|
||||||
|
import { UserRole } from '../../../helpers/types/permission';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceUserService {
|
export class WorkspaceUserService {
|
||||||
constructor(
|
constructor(
|
||||||
private workspaceUserRepository: WorkspaceUserRepository,
|
private workspaceRepository: WorkspaceRepository,
|
||||||
private dataSource: DataSource,
|
private userRepository: UserRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async addUserToWorkspace(
|
async getWorkspaceUsers(
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
role: string,
|
paginationOptions: PaginationOptions,
|
||||||
manager?: EntityManager,
|
): Promise<PaginatedResult<User>> {
|
||||||
): Promise<WorkspaceUser> {
|
const [workspaceUsers, count] = await this.userRepository.findAndCount({
|
||||||
let addedUser;
|
where: {
|
||||||
|
workspaceId,
|
||||||
await transactionWrapper(
|
|
||||||
async (manager) => {
|
|
||||||
const userExists = await manager.exists(User, {
|
|
||||||
where: { id: userId },
|
|
||||||
});
|
|
||||||
if (!userExists) {
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingWorkspaceUser = await manager.findOneBy(WorkspaceUser, {
|
|
||||||
userId: userId,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingWorkspaceUser) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'User is already a member of this workspace',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceUser = new WorkspaceUser();
|
|
||||||
workspaceUser.userId = userId;
|
|
||||||
workspaceUser.workspaceId = workspaceId;
|
|
||||||
workspaceUser.role = role;
|
|
||||||
|
|
||||||
addedUser = await manager.save(workspaceUser);
|
|
||||||
},
|
},
|
||||||
this.dataSource,
|
take: paginationOptions.limit,
|
||||||
manager,
|
skip: paginationOptions.skip,
|
||||||
);
|
});
|
||||||
|
|
||||||
return addedUser;
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
return new PaginatedResult(workspaceUsers, paginationMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceUserRole(
|
async updateWorkspaceUserRole(
|
||||||
@ -80,16 +45,14 @@ export class WorkspaceUserService {
|
|||||||
return workspaceUser;
|
return workspaceUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceOwnerCount = await this.workspaceUserRepository.count({
|
const workspaceOwnerCount = await this.userRepository.count({
|
||||||
where: {
|
where: {
|
||||||
role: WorkspaceUserRole.OWNER,
|
role: UserRole.OWNER,
|
||||||
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (workspaceUser.role === UserRole.OWNER && workspaceOwnerCount === 1) {
|
||||||
workspaceUser.role === WorkspaceUserRole.OWNER &&
|
|
||||||
workspaceOwnerCount === 1
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'There must be at least one workspace owner',
|
'There must be at least one workspace owner',
|
||||||
);
|
);
|
||||||
@ -97,105 +60,26 @@ export class WorkspaceUserService {
|
|||||||
|
|
||||||
workspaceUser.role = workspaceUserRoleDto.role;
|
workspaceUser.role = workspaceUserRoleDto.role;
|
||||||
|
|
||||||
return this.workspaceUserRepository.save(workspaceUser);
|
return this.userRepository.save(workspaceUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeUserFromWorkspace(
|
async deactivateUser(): Promise<any> {
|
||||||
userId: string,
|
return 'todo';
|
||||||
workspaceId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const workspaceUser = await this.findAndValidateWorkspaceUser(
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceOwnerCount = await this.workspaceUserRepository.count({
|
|
||||||
where: {
|
|
||||||
role: WorkspaceUserRole.OWNER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
workspaceUser.role === WorkspaceUserRole.OWNER &&
|
|
||||||
workspaceOwnerCount === 1
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'There must be at least one workspace owner',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceUserRepository.delete({
|
async findWorkspaceUser(userId: string, workspaceId: string): Promise<User> {
|
||||||
userId,
|
return await this.userRepository.findOneBy({
|
||||||
|
id: userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserWorkspaces(
|
async findWorkspaceUserByEmail(
|
||||||
userId: string,
|
email: string,
|
||||||
paginationOptions: PaginationOptions,
|
|
||||||
): Promise<PaginatedResult<Workspace>> {
|
|
||||||
const [workspaces, count] = await this.workspaceUserRepository.findAndCount(
|
|
||||||
{
|
|
||||||
where: { userId: userId },
|
|
||||||
relations: ['workspace'],
|
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const userWorkspaces = workspaces.map(
|
|
||||||
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
|
||||||
return new PaginatedResult(userWorkspaces, paginationMeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWorkspaceUsers(
|
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
paginationOptions: PaginationOptions,
|
): Promise<User> {
|
||||||
): Promise<PaginatedResult<any>> {
|
return await this.userRepository.findOneBy({
|
||||||
const [workspaceUsers, count] =
|
email: email,
|
||||||
await this.workspaceUserRepository.findAndCount({
|
|
||||||
relations: ['user'],
|
|
||||||
where: {
|
|
||||||
workspace: {
|
|
||||||
id: workspaceId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
});
|
|
||||||
|
|
||||||
const users = workspaceUsers.map((workspaceUser) => {
|
|
||||||
workspaceUser.user.password = '';
|
|
||||||
return {
|
|
||||||
...workspaceUser.user,
|
|
||||||
role: workspaceUser.role,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
|
||||||
return new PaginatedResult(users, paginationMeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserRoleInWorkspace(
|
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const workspaceUser = await this.findAndValidateWorkspaceUser(
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
return workspaceUser.role ? workspaceUser.role : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findWorkspaceUser(
|
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<WorkspaceUser> {
|
|
||||||
return await this.workspaceUserRepository.findOneBy({
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -203,13 +87,13 @@ export class WorkspaceUserService {
|
|||||||
async findAndValidateWorkspaceUser(
|
async findAndValidateWorkspaceUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<WorkspaceUser> {
|
): Promise<User> {
|
||||||
const workspaceUser = await this.findWorkspaceUser(userId, workspaceId);
|
const user = await this.findWorkspaceUser(userId, workspaceId);
|
||||||
|
|
||||||
if (!workspaceUser) {
|
if (!user) {
|
||||||
throw new BadRequestException('Workspace member not found');
|
throw new BadRequestException('Workspace member not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceUser;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,31 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
|
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
|
||||||
import { WorkspaceRepository } from '../repositories/workspace.repository';
|
import { WorkspaceRepository } from '../repositories/workspace.repository';
|
||||||
import { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
|
|
||||||
import {
|
|
||||||
WorkspaceUser,
|
|
||||||
WorkspaceUserRole,
|
|
||||||
} from '../entities/workspace-user.entity';
|
|
||||||
import { Workspace } from '../entities/workspace.entity';
|
import { Workspace } from '../entities/workspace.entity';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||||
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
||||||
import { SpaceService } from '../../space/space.service';
|
import { SpaceService } from '../../space/space.service';
|
||||||
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
|
|
||||||
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
|
|
||||||
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
|
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
import { transactionWrapper } from '../../../helpers/db.helper';
|
||||||
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
|
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
|
||||||
import { WorkspaceUserService } from './workspace-user.service';
|
import { UserRepository } from '../../user/repositories/user.repository';
|
||||||
|
import { SpaceRole, UserRole } from '../../../helpers/types/permission';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { EnvironmentService } from '../../../environment/environment.service';
|
||||||
|
import { Space } from '../../space/entities/space.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceService {
|
export class WorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private workspaceRepository: WorkspaceRepository,
|
private workspaceRepository: WorkspaceRepository,
|
||||||
private workspaceUserRepository: WorkspaceUserRepository,
|
private userRepository: UserRepository,
|
||||||
private spaceService: SpaceService,
|
private spaceService: SpaceService,
|
||||||
private workspaceUserService: WorkspaceUserService,
|
private environmentService: EnvironmentService,
|
||||||
|
|
||||||
private dataSource: DataSource,
|
private dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
@ -35,115 +34,118 @@ export class WorkspaceService {
|
|||||||
return this.workspaceRepository.findById(workspaceId);
|
return this.workspaceRepository.findById(workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(workspace: Workspace) {
|
async getWorkspaceInfo(workspaceId: string): Promise<Workspace> {
|
||||||
return this.workspaceRepository.save(workspace);
|
const space = await this.workspaceRepository
|
||||||
|
.createQueryBuilder('workspace')
|
||||||
|
.where('workspace.id = :workspaceId', { workspaceId })
|
||||||
|
.loadRelationCountAndMap(
|
||||||
|
'workspace.userCount',
|
||||||
|
'workspace.users',
|
||||||
|
'workspaceUsers',
|
||||||
|
)
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
throw new NotFoundException('Workspace not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrJoinWorkspace(
|
return space;
|
||||||
userId,
|
|
||||||
createWorkspaceDto?: CreateWorkspaceDto,
|
|
||||||
manager?: EntityManager,
|
|
||||||
) {
|
|
||||||
await transactionWrapper(
|
|
||||||
async (manager: EntityManager) => {
|
|
||||||
const workspaceCount = await manager
|
|
||||||
.createQueryBuilder(Workspace, 'workspace')
|
|
||||||
.getCount();
|
|
||||||
|
|
||||||
if (workspaceCount === 0) {
|
|
||||||
// create first workspace and add user to workspace as owner
|
|
||||||
const createdWorkspace = await this.create(
|
|
||||||
userId,
|
|
||||||
createWorkspaceDto ?? null,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
await this.workspaceUserService.addUserToWorkspace(
|
|
||||||
userId,
|
|
||||||
createdWorkspace.id,
|
|
||||||
WorkspaceUserRole.OWNER,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
// create default space and add user to it too.
|
|
||||||
const createdSpace = await this.spaceService.create(
|
|
||||||
userId,
|
|
||||||
createdWorkspace.id,
|
|
||||||
{ name: 'General' } as CreateSpaceDto,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.spaceService.addUserToSpace(
|
|
||||||
userId,
|
|
||||||
createdSpace.id,
|
|
||||||
WorkspaceUserRole.OWNER,
|
|
||||||
createdWorkspace.id,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
createdWorkspace.defaultSpaceId = createdSpace.id;
|
|
||||||
await manager.save(createdWorkspace);
|
|
||||||
} else {
|
|
||||||
// limited to single workspace
|
|
||||||
// fetch the oldest workspace and add user to it
|
|
||||||
const firstWorkspace = await manager.find(Workspace, {
|
|
||||||
order: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
take: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// add user to workspace and default space
|
|
||||||
|
|
||||||
await this.workspaceUserService.addUserToWorkspace(
|
|
||||||
userId,
|
|
||||||
firstWorkspace[0].id,
|
|
||||||
WorkspaceUserRole.MEMBER,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.spaceService.addUserToSpace(
|
|
||||||
userId,
|
|
||||||
firstWorkspace[0].defaultSpaceId,
|
|
||||||
WorkspaceUserRole.MEMBER,
|
|
||||||
firstWorkspace[0].id,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
this.dataSource,
|
|
||||||
manager,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
userId: string,
|
user: User,
|
||||||
createWorkspaceDto?: CreateWorkspaceDto,
|
createWorkspaceDto: CreateWorkspaceDto,
|
||||||
manager?: EntityManager,
|
manager?: EntityManager,
|
||||||
): Promise<Workspace> {
|
): Promise<Workspace> {
|
||||||
let workspace: Workspace;
|
return await transactionWrapper(
|
||||||
|
|
||||||
await transactionWrapper(
|
|
||||||
async (manager) => {
|
async (manager) => {
|
||||||
if (createWorkspaceDto) {
|
let workspace = new Workspace();
|
||||||
workspace = plainToInstance(Workspace, createWorkspaceDto);
|
|
||||||
} else {
|
|
||||||
workspace = new Workspace();
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.inviteCode = uuid();
|
|
||||||
workspace.creatorId = userId;
|
|
||||||
|
|
||||||
//if (workspace.name && !workspace.hostname?.trim()) {
|
|
||||||
// workspace.hostname = generateHostname(createWorkspaceDto.name);
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
workspace.name = createWorkspaceDto.name;
|
||||||
|
workspace.hostname = createWorkspaceDto?.hostname;
|
||||||
|
workspace.description = createWorkspaceDto.description;
|
||||||
|
workspace.inviteCode = uuidv4();
|
||||||
|
workspace.creatorId = user.id;
|
||||||
workspace = await manager.save(workspace);
|
workspace = await manager.save(workspace);
|
||||||
|
|
||||||
|
user.workspaceId = workspace.id;
|
||||||
|
user.role = UserRole.OWNER;
|
||||||
|
await manager.save(user);
|
||||||
|
|
||||||
|
// create default space
|
||||||
|
const spaceData: CreateSpaceDto = {
|
||||||
|
name: 'General',
|
||||||
|
};
|
||||||
|
|
||||||
|
// create default space
|
||||||
|
const createdSpace = await this.spaceService.create(
|
||||||
|
user.id,
|
||||||
|
workspace.id,
|
||||||
|
spaceData,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
// and add user to it too.
|
||||||
|
await this.spaceService.addUserToSpace(
|
||||||
|
user.id,
|
||||||
|
createdSpace.id,
|
||||||
|
SpaceRole.OWNER,
|
||||||
|
workspace.id,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace.defaultSpaceId = createdSpace.id;
|
||||||
|
await manager.save(workspace);
|
||||||
|
return workspace;
|
||||||
},
|
},
|
||||||
this.dataSource,
|
this.dataSource,
|
||||||
manager,
|
manager,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addUserToWorkspace(
|
||||||
|
user: User,
|
||||||
|
workspaceId,
|
||||||
|
assignedRole?: UserRole,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<Workspace> {
|
||||||
|
return await transactionWrapper(
|
||||||
|
async (manager: EntityManager) => {
|
||||||
|
const workspace = await manager.findOneBy(Workspace, {
|
||||||
|
id: workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new BadRequestException('Workspace does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
user.role = assignedRole ?? workspace.defaultRole;
|
||||||
|
user.workspaceId = workspace.id;
|
||||||
|
await manager.save(user);
|
||||||
|
|
||||||
|
const space = await manager.findOneBy(Space, {
|
||||||
|
id: workspace.defaultSpaceId,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
throw new NotFoundException('Space not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// add user to default space
|
||||||
|
await this.spaceService.addUserToSpace(
|
||||||
|
user.id,
|
||||||
|
space.id,
|
||||||
|
space.defaultRole,
|
||||||
|
workspace.id,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
@ -178,42 +180,4 @@ export class WorkspaceService {
|
|||||||
// remove all existing users from workspace
|
// remove all existing users from workspace
|
||||||
// delete workspace
|
// delete workspace
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserCurrentWorkspace(userId: string): Promise<Workspace> {
|
|
||||||
const userWorkspace = await this.workspaceUserRepository.findOne({
|
|
||||||
relations: ['workspace'],
|
|
||||||
where: { userId: userId },
|
|
||||||
order: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userWorkspace) {
|
|
||||||
throw new NotFoundException('No workspace found for this user');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { workspace, ...workspaceUser } = userWorkspace;
|
|
||||||
return { ...workspace, workspaceUser } as Workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserWorkspaces(
|
|
||||||
userId: string,
|
|
||||||
paginationOptions: PaginationOptions,
|
|
||||||
): Promise<PaginatedResult<Workspace>> {
|
|
||||||
const [workspaces, count] = await this.workspaceUserRepository.findAndCount(
|
|
||||||
{
|
|
||||||
where: { userId: userId },
|
|
||||||
relations: ['workspace'],
|
|
||||||
take: paginationOptions.limit,
|
|
||||||
skip: paginationOptions.skip,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const userWorkspaces = workspaces.map(
|
|
||||||
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
|
||||||
return new PaginatedResult(userWorkspaces, paginationMeta);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,20 +4,17 @@ import { WorkspaceController } from './controllers/workspace.controller';
|
|||||||
import { WorkspaceRepository } from './repositories/workspace.repository';
|
import { WorkspaceRepository } from './repositories/workspace.repository';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Workspace } from './entities/workspace.entity';
|
import { Workspace } from './entities/workspace.entity';
|
||||||
import { WorkspaceUser } from './entities/workspace-user.entity';
|
|
||||||
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
|
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
|
||||||
import { WorkspaceUserRepository } from './repositories/workspace-user.repository';
|
|
||||||
import { AuthModule } from '../auth/auth.module';
|
|
||||||
import { SpaceModule } from '../space/space.module';
|
import { SpaceModule } from '../space/space.module';
|
||||||
import { WorkspaceUserService } from './services/workspace-user.service';
|
|
||||||
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
|
||||||
import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository';
|
import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository';
|
||||||
|
import { WorkspaceUserService } from './services/workspace-user.service';
|
||||||
|
import { UserModule } from '../user/user.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Workspace, WorkspaceUser, WorkspaceInvitation]),
|
TypeOrmModule.forFeature([Workspace, WorkspaceInvitation]),
|
||||||
AuthModule,
|
SpaceModule, UserModule
|
||||||
SpaceModule,
|
|
||||||
],
|
],
|
||||||
controllers: [WorkspaceController],
|
controllers: [WorkspaceController],
|
||||||
providers: [
|
providers: [
|
||||||
@ -25,9 +22,8 @@ import { WorkspaceInvitationRepository } from './repositories/workspace-invitati
|
|||||||
WorkspaceUserService,
|
WorkspaceUserService,
|
||||||
WorkspaceInvitationService,
|
WorkspaceInvitationService,
|
||||||
WorkspaceRepository,
|
WorkspaceRepository,
|
||||||
WorkspaceUserRepository,
|
|
||||||
WorkspaceInvitationRepository,
|
WorkspaceInvitationRepository,
|
||||||
],
|
],
|
||||||
exports: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
|
exports: [WorkspaceService, WorkspaceRepository],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class RefactorWorkspaceUser1710168946001 implements MigrationInterface {
|
||||||
|
name = 'RefactorWorkspaceUser1710168946001'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" DROP CONSTRAINT "FK_2aab2dd12dc65eb183d99b953e0"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "role" character varying(100)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "workspaceId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ALTER COLUMN "creatorId" DROP NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "FK_949fea12b7977a8b2f483bf802a" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_949fea12b7977a8b2f483bf802a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ALTER COLUMN "creatorId" SET NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "workspaceId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "role"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ADD CONSTRAINT "FK_2aab2dd12dc65eb183d99b953e0" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddDeletedAtToWorkspace1710371336371 implements MigrationInterface {
|
||||||
|
name = 'AddDeletedAtToWorkspace1710371336371'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ADD "deletedAt" TIMESTAMP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" DROP COLUMN "deletedAt"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class RemoveUniqueFromUserEmail1710377632898 implements MigrationInterface {
|
||||||
|
name = 'RemoveUniqueFromUserEmail1710377632898'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddDefaultRoles1710517114813 implements MigrationInterface {
|
||||||
|
name = 'AddDefaultRoles1710517114813'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" DROP CONSTRAINT "UQ_4f0a029f6eefd773fde2143b261"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" DROP COLUMN "hostname"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" ADD "privacy" character varying(100) NOT NULL DEFAULT 'open'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" ADD "defaultRole" character varying(100) NOT NULL DEFAULT 'writer'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ADD "defaultRole" character varying NOT NULL DEFAULT 'member'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" DROP COLUMN "defaultRole"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" DROP COLUMN "defaultRole"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" DROP COLUMN "privacy"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" ADD "hostname" character varying(255)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" ADD CONSTRAINT "UQ_4f0a029f6eefd773fde2143b261" UNIQUE ("hostname")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class UniqueUser1710530661379 implements MigrationInterface {
|
||||||
|
name = 'UniqueUser1710530661379'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "UQ_b368db80982a952e3071e008a2c" UNIQUE ("email", "workspaceId")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_b368db80982a952e3071e008a2c"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class SpaceSlug1710615517137 implements MigrationInterface {
|
||||||
|
name = 'SpaceSlug1710615517137'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" ADD "slug" character varying`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" ADD CONSTRAINT "UQ_c58549749e7a141746940d01f39" UNIQUE ("slug", "workspaceId")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" DROP CONSTRAINT "UQ_c58549749e7a141746940d01f39"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "spaces" DROP COLUMN "slug"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,17 +1,8 @@
|
|||||||
import {
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
createParamDecorator,
|
|
||||||
ExecutionContext,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
export const AuthUser = createParamDecorator(
|
export const AuthUser = createParamDecorator(
|
||||||
(data: unknown, ctx: ExecutionContext) => {
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
const request = ctx.switchToHttp().getRequest();
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user.user;
|
||||||
if (!request['user'] || !request['user'].user) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return request['user'] ? request['user'].user : undefined;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
8
apps/server/src/decorators/auth-workspace.decorator.ts
Normal file
8
apps/server/src/decorators/auth-workspace.decorator.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const AuthWorkspace = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user.workspace;
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import {
|
|
||||||
createParamDecorator,
|
|
||||||
ExecutionContext,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
export const CurrentWorkspace = createParamDecorator(
|
|
||||||
(data: unknown, ctx: ExecutionContext) => {
|
|
||||||
const request = ctx.switchToHttp().getRequest();
|
|
||||||
|
|
||||||
if (!request['user'] || !request['user'].workspace) {
|
|
||||||
throw new UnauthorizedException('Workspace not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return request['user'] ? request['user'].workspace : undefined;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -59,4 +59,15 @@ export class EnvironmentService {
|
|||||||
getAwsS3UsePathStyleEndpoint(): boolean {
|
getAwsS3UsePathStyleEndpoint(): boolean {
|
||||||
return this.configService.get<boolean>('AWS_S3_USE_PATH_STYLE_ENDPOINT');
|
return this.configService.get<boolean>('AWS_S3_USE_PATH_STYLE_ENDPOINT');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isCloud(): boolean {
|
||||||
|
const cloudConfig = this.configService
|
||||||
|
.get<string>('CLOUD', 'false')
|
||||||
|
.toLowerCase();
|
||||||
|
return cloudConfig === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelfHosted(): boolean {
|
||||||
|
return !this.isCloud();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
apps/server/src/guards/jwt-auth.guard.ts
Normal file
35
apps/server/src/guards/jwt-auth.guard.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest(err: any, user: any, info: any) {
|
||||||
|
if (err || !user) {
|
||||||
|
throw err || new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ export async function transactionWrapper(
|
|||||||
operation: (...args) => any,
|
operation: (...args) => any,
|
||||||
datasource: DataSource,
|
datasource: DataSource,
|
||||||
entityManager: EntityManager,
|
entityManager: EntityManager,
|
||||||
) {
|
): Promise<any> {
|
||||||
if (entityManager) {
|
if (entityManager) {
|
||||||
return await operation(entityManager);
|
return await operation(entityManager);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
16
apps/server/src/helpers/types/permission.ts
Normal file
16
apps/server/src/helpers/types/permission.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export enum UserRole {
|
||||||
|
OWNER = 'owner',
|
||||||
|
ADMIN = 'admin', // can have owner permissions but cannot delete workspace
|
||||||
|
MEMBER = 'member',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpaceRole {
|
||||||
|
OWNER = 'owner', // can add members, remove, and delete space
|
||||||
|
WRITER = 'writer', // can read and write pages in space
|
||||||
|
READER = 'reader', // can only read pages in space
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpacePrivacy {
|
||||||
|
OPEN = 'open', // any workspace member can see and join.
|
||||||
|
PRIVATE = 'private', // only added space users can see
|
||||||
|
}
|
||||||
41
apps/server/src/middlewares/domain.middleware.ts
Normal file
41
apps/server/src/middlewares/domain.middleware.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable, NestMiddleware, NotFoundException } from '@nestjs/common';
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { WorkspaceRepository } from '../core/workspace/repositories/workspace.repository';
|
||||||
|
import { EnvironmentService } from '../environment/environment.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DomainMiddleware implements NestMiddleware {
|
||||||
|
constructor(
|
||||||
|
private workspaceRepository: WorkspaceRepository,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
) {}
|
||||||
|
async use(
|
||||||
|
req: FastifyRequest['raw'],
|
||||||
|
res: FastifyReply['raw'],
|
||||||
|
next: () => void,
|
||||||
|
) {
|
||||||
|
if (this.environmentService.isSelfHosted()) {
|
||||||
|
const workspace = await this.workspaceRepository.findFirst();
|
||||||
|
if (!workspace) {
|
||||||
|
throw new NotFoundException('Workspace not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
(req as any).workspaceId = workspace.id;
|
||||||
|
} else if (this.environmentService.isCloud()) {
|
||||||
|
const header = req.headers.host;
|
||||||
|
const subdomain = header.split('.')[0];
|
||||||
|
|
||||||
|
const workspace = await this.workspaceRepository.findOneBy({
|
||||||
|
hostname: subdomain,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new NotFoundException('Workspace not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
(req as any).workspaceId = workspace.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,6 @@
|
|||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@hocuspocus/provider": "^2.9.0",
|
"@hocuspocus/provider": "^2.11.3",
|
||||||
"@hocuspocus/server": "^2.9.0",
|
"@hocuspocus/server": "^2.11.3",
|
||||||
"@hocuspocus/transformer": "^2.9.0",
|
"@hocuspocus/transformer": "^2.11.3",
|
||||||
"@tiptap/extension-code-block": "^2.1.12",
|
"@tiptap/extension-code-block": "^2.1.12",
|
||||||
"@tiptap/extension-collaboration": "^2.1.12",
|
"@tiptap/extension-collaboration": "^2.1.12",
|
||||||
"@tiptap/extension-collaboration-cursor": "^2.1.12",
|
"@tiptap/extension-collaboration-cursor": "^2.1.12",
|
||||||
@ -37,7 +37,7 @@
|
|||||||
"@tiptap/starter-kit": "^2.1.12",
|
"@tiptap/starter-kit": "^2.1.12",
|
||||||
"@tiptap/suggestion": "^2.1.12",
|
"@tiptap/suggestion": "^2.1.12",
|
||||||
"y-indexeddb": "^9.0.12",
|
"y-indexeddb": "^9.0.12",
|
||||||
"yjs": "^13.6.10"
|
"yjs": "^13.6.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nx/js": "17.2.8",
|
"@nx/js": "17.2.8",
|
||||||
|
|||||||
2920
pnpm-lock.yaml
generated
2920
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user