mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 21:12:56 +10:00
* fixes and cleanups
* db transactions * add default space to workspace
This commit is contained in:
@ -35,13 +35,19 @@ export class JwtGuard implements CanActivate {
|
|||||||
throw new UnauthorizedException('Invalid jwt token');
|
throw new UnauthorizedException('Invalid jwt token');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let payload;
|
||||||
const payload = await this.tokenService.verifyJwt(token);
|
|
||||||
|
|
||||||
|
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
|
//fetch user and current workspace data from db
|
||||||
request['user'] = await this.userService.getUserInstance(payload.sub);
|
request['user'] = await this.userService.getUserInstance(payload.sub);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new UnauthorizedException('Could not verify jwt token');
|
throw new UnauthorizedException(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
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';
|
||||||
|
|
||||||
@Entity('spaces')
|
@Entity('spaces')
|
||||||
export class Space {
|
export class Space {
|
||||||
@ -39,13 +40,18 @@ export class Space {
|
|||||||
@Column()
|
@Column()
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, { onDelete: 'CASCADE' })
|
@ManyToOne(() => Workspace, (workspace) => workspace.spaces, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
@JoinColumn({ name: 'workspaceId' })
|
@JoinColumn({ name: 'workspaceId' })
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
|
|
||||||
@OneToMany(() => SpaceUser, (workspaceUser) => workspaceUser.space)
|
@OneToMany(() => SpaceUser, (workspaceUser) => workspaceUser.space)
|
||||||
spaceUsers: SpaceUser[];
|
spaceUsers: SpaceUser[];
|
||||||
|
|
||||||
|
@OneToMany(() => Page, (page) => page.space)
|
||||||
|
pages: Page[];
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@ -5,30 +5,45 @@ 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 { DataSource, EntityManager } from 'typeorm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
constructor(
|
constructor(
|
||||||
private spaceRepository: SpaceRepository,
|
private spaceRepository: SpaceRepository,
|
||||||
private spaceUserRepository: SpaceUserRepository,
|
private spaceUserRepository: SpaceUserRepository,
|
||||||
|
private dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(userId: string, workspaceId, createSpaceDto?: CreateSpaceDto) {
|
async create(
|
||||||
|
userId: string,
|
||||||
|
workspaceId,
|
||||||
|
createSpaceDto?: CreateSpaceDto,
|
||||||
|
manager?: EntityManager,
|
||||||
|
) {
|
||||||
let space: Space;
|
let space: Space;
|
||||||
|
|
||||||
if (createSpaceDto) {
|
await transactionWrapper(
|
||||||
space = plainToInstance(Space, createSpaceDto);
|
async (manager: EntityManager) => {
|
||||||
} else {
|
if (createSpaceDto) {
|
||||||
space = new Space();
|
space = plainToInstance(Space, createSpaceDto);
|
||||||
}
|
} else {
|
||||||
|
space = new Space();
|
||||||
|
}
|
||||||
|
|
||||||
space.creatorId = userId;
|
space.creatorId = userId;
|
||||||
space.workspaceId = workspaceId;
|
space.workspaceId = workspaceId;
|
||||||
|
|
||||||
space.name = createSpaceDto?.name ?? 'untitled space';
|
space.name = createSpaceDto?.name ?? 'untitled space';
|
||||||
space.description = createSpaceDto?.description ?? null;
|
space.description = createSpaceDto?.description ?? null;
|
||||||
|
|
||||||
|
space = await manager.save(space);
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
space = await this.spaceRepository.save(space);
|
|
||||||
return space;
|
return space;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,21 +51,32 @@ export class SpaceService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
role: string,
|
role: string,
|
||||||
|
manager?: EntityManager,
|
||||||
): Promise<SpaceUser> {
|
): Promise<SpaceUser> {
|
||||||
const existingSpaceUser = await this.spaceUserRepository.findOne({
|
let addedUser: SpaceUser;
|
||||||
where: { userId: userId, spaceId: spaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingSpaceUser) {
|
await transactionWrapper(
|
||||||
throw new BadRequestException('User already added to this space');
|
async (manager: EntityManager) => {
|
||||||
}
|
const existingSpaceUser = await manager.findOne(SpaceUser, {
|
||||||
|
where: { userId: userId, spaceId: spaceId },
|
||||||
|
});
|
||||||
|
|
||||||
const spaceUser = new SpaceUser();
|
if (existingSpaceUser) {
|
||||||
spaceUser.userId = userId;
|
throw new BadRequestException('User already added to this space');
|
||||||
spaceUser.spaceId = spaceId;
|
}
|
||||||
spaceUser.role = role;
|
|
||||||
|
|
||||||
return this.spaceUserRepository.save(spaceUser);
|
const spaceUser = new SpaceUser();
|
||||||
|
spaceUser.userId = userId;
|
||||||
|
spaceUser.spaceId = spaceId;
|
||||||
|
spaceUser.role = role;
|
||||||
|
|
||||||
|
addedUser = await manager.save(spaceUser);
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
return addedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSpacesInWorkspace(userId: string, workspaceId: string) {
|
async getUserSpacesInWorkspace(userId: string, workspaceId: string) {
|
||||||
|
|||||||
@ -85,3 +85,8 @@ 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;
|
||||||
|
|||||||
@ -11,27 +11,51 @@ import { plainToInstance } from 'class-transformer';
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { WorkspaceService } from '../workspace/services/workspace.service';
|
import { WorkspaceService } from '../workspace/services/workspace.service';
|
||||||
import { Workspace } from '../workspace/entities/workspace.entity';
|
import { Workspace } from '../workspace/entities/workspace.entity';
|
||||||
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
|
import { transactionWrapper } from '../../helpers/db.helper';
|
||||||
|
import { CreateWorkspaceDto } from '../workspace/dto/create-workspace.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
private userRepository: UserRepository,
|
private userRepository: UserRepository,
|
||||||
private workspaceService: WorkspaceService,
|
private workspaceService: WorkspaceService,
|
||||||
|
private dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
async create(createUserDto: CreateUserDto): Promise<User> {
|
async create(
|
||||||
|
createUserDto: CreateUserDto,
|
||||||
|
manager?: EntityManager,
|
||||||
|
): Promise<User> {
|
||||||
|
let user: User;
|
||||||
|
|
||||||
const existingUser: User = await this.findByEmail(createUserDto.email);
|
const existingUser: User = await this.findByEmail(createUserDto.email);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new BadRequestException('A user with this email already exists');
|
throw new BadRequestException('A user with this email already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: User = plainToInstance(User, createUserDto);
|
await transactionWrapper(
|
||||||
user.locale = 'en';
|
async (manager: EntityManager) => {
|
||||||
user.lastLoginAt = new Date();
|
user = plainToInstance(User, createUserDto);
|
||||||
|
user.locale = 'en';
|
||||||
|
user.lastLoginAt = new Date();
|
||||||
|
user.name = createUserDto.email.split('@')[0];
|
||||||
|
|
||||||
user = await this.userRepository.save(user);
|
user = await manager.save(User, user);
|
||||||
|
|
||||||
await this.workspaceService.createOrJoinWorkspace(user.id);
|
const createWorkspaceDto: CreateWorkspaceDto = {
|
||||||
|
name: 'My Workspace',
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.workspaceService.createOrJoinWorkspace(
|
||||||
|
user.id,
|
||||||
|
createWorkspaceDto,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@ -43,12 +67,14 @@ export class UserService {
|
|||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace: Workspace =
|
let workspace;
|
||||||
await this.workspaceService.getUserCurrentWorkspace(userId);
|
|
||||||
|
|
||||||
if (!workspace) {
|
try {
|
||||||
throw new NotFoundException('Workspace not found');
|
workspace = await this.workspaceService.getUserCurrentWorkspace(userId);
|
||||||
|
} catch (error) {
|
||||||
|
//console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, workspace };
|
return { user, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
DefaultValuePipe,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
ParseIntPipe,
|
|
||||||
Post,
|
Post,
|
||||||
Query,
|
|
||||||
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 { JwtGuard } from '../../auth/guards/jwt.guard';
|
||||||
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||||
import { CreateWorkspaceDto } from '../dto/create-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 { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
|
||||||
@ -30,6 +24,17 @@ import { PaginationOptions } from '../../../helpers/pagination/pagination-option
|
|||||||
export class WorkspaceController {
|
export class WorkspaceController {
|
||||||
constructor(private readonly workspaceService: WorkspaceService) {}
|
constructor(private readonly workspaceService: WorkspaceService) {}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('/')
|
||||||
|
async getUserWorkspaces(
|
||||||
|
@Body()
|
||||||
|
pagination: PaginationOptions,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
) {
|
||||||
|
return this.workspaceService.getUserWorkspaces(user.id, pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('create')
|
@Post('create')
|
||||||
async createWorkspace(
|
async createWorkspace(
|
||||||
@ -38,6 +43,7 @@ export class WorkspaceController {
|
|||||||
) {
|
) {
|
||||||
return this.workspaceService.create(user.id, createWorkspaceDto);
|
return this.workspaceService.create(user.id, createWorkspaceDto);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('update')
|
@Post('update')
|
||||||
@ -78,7 +84,7 @@ export class WorkspaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Delete('members/delete')
|
@Post('members/remove')
|
||||||
async removeWorkspaceMember(
|
async removeWorkspaceMember(
|
||||||
@Body() removeWorkspaceUserDto: RemoveWorkspaceUserDto,
|
@Body() removeWorkspaceUserDto: RemoveWorkspaceUserDto,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
@ -93,9 +99,11 @@ export class WorkspaceController {
|
|||||||
@Post('members/role')
|
@Post('members/role')
|
||||||
async updateWorkspaceMemberRole(
|
async updateWorkspaceMemberRole(
|
||||||
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
||||||
|
@AuthUser() authUser: User,
|
||||||
@CurrentWorkspace() workspace: Workspace,
|
@CurrentWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
return this.workspaceService.updateWorkspaceUserRole(
|
return this.workspaceService.updateWorkspaceUserRole(
|
||||||
|
authUser,
|
||||||
workspaceUserRoleDto,
|
workspaceUserRoleDto,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -44,3 +44,9 @@ export class WorkspaceUser {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WorkspaceUserRole {
|
||||||
|
OWNER = 'owner',
|
||||||
|
ADMIN = 'admin',
|
||||||
|
MEMBER = 'member',
|
||||||
|
}
|
||||||
|
|||||||
@ -7,12 +7,14 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
OneToOne,
|
||||||
} 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 { 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';
|
||||||
|
|
||||||
@Entity('workspaces')
|
@Entity('workspaces')
|
||||||
export class Workspace {
|
export class Workspace {
|
||||||
@ -50,6 +52,13 @@ export class Workspace {
|
|||||||
@JoinColumn({ name: 'creatorId' })
|
@JoinColumn({ name: 'creatorId' })
|
||||||
creator: User;
|
creator: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
defaultSpaceId: string;
|
||||||
|
|
||||||
|
@OneToOne(() => Space, { onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'defaultSpaceId' })
|
||||||
|
defaultSpace: Space;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@ -70,4 +79,7 @@ export class Workspace {
|
|||||||
|
|
||||||
@OneToMany(() => Comment, (comment) => comment.workspace)
|
@OneToMany(() => Comment, (comment) => comment.workspace)
|
||||||
comments: Comment[];
|
comments: Comment[];
|
||||||
|
|
||||||
|
@OneToMany(() => Space, (space) => space.workspace)
|
||||||
|
spaces: [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,19 +6,24 @@ import {
|
|||||||
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 { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
|
||||||
import { WorkspaceUser } from '../entities/workspace-user.entity';
|
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 { plainToInstance } from 'class-transformer';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { generateHostname } from '../workspace.util';
|
|
||||||
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 { SpaceService } from '../../space/space.service';
|
import { SpaceService } from '../../space/space.service';
|
||||||
import { UserWithRole } from '../../user/entities/user.entity';
|
|
||||||
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 { DataSource, EntityManager } from 'typeorm';
|
||||||
|
import { transactionWrapper } from '../../../helpers/db.helper';
|
||||||
|
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceService {
|
export class WorkspaceService {
|
||||||
@ -26,6 +31,7 @@ export class WorkspaceService {
|
|||||||
private workspaceRepository: WorkspaceRepository,
|
private workspaceRepository: WorkspaceRepository,
|
||||||
private workspaceUserRepository: WorkspaceUserRepository,
|
private workspaceUserRepository: WorkspaceUserRepository,
|
||||||
private spaceService: SpaceService,
|
private spaceService: SpaceService,
|
||||||
|
private dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findById(workspaceId: string): Promise<Workspace> {
|
async findById(workspaceId: string): Promise<Workspace> {
|
||||||
@ -36,65 +42,107 @@ export class WorkspaceService {
|
|||||||
return this.workspaceRepository.save(workspace);
|
return this.workspaceRepository.save(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrJoinWorkspace(userId) {
|
async createOrJoinWorkspace(
|
||||||
// context:
|
userId,
|
||||||
// only create workspace if it is not a signup to an existing workspace
|
createWorkspaceDto?: CreateWorkspaceDto,
|
||||||
// OS version is limited to one workspace.
|
manager?: EntityManager,
|
||||||
// if there is no existing workspace, create a new workspace
|
) {
|
||||||
// and make first user owner/admin
|
await transactionWrapper(
|
||||||
|
async (manager: EntityManager) => {
|
||||||
|
const workspaceCount = await manager
|
||||||
|
.createQueryBuilder(Workspace, 'workspace')
|
||||||
|
.getCount();
|
||||||
|
|
||||||
// check if workspace already exists in the db
|
if (workspaceCount === 0) {
|
||||||
// if not create default, if yes add the user to existing workspace if signup is open.
|
// create first workspace and add user to workspace as owner
|
||||||
const workspaceCount = await this.workspaceRepository.count();
|
const createdWorkspace = await this.create(
|
||||||
|
userId,
|
||||||
|
createWorkspaceDto ?? null,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
await this.addUserToWorkspace(
|
||||||
|
userId,
|
||||||
|
createdWorkspace.id,
|
||||||
|
WorkspaceUserRole.OWNER,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
if (workspaceCount === 0) {
|
// create default space and add user to it too.
|
||||||
// create first workspace
|
const createdSpace = await this.spaceService.create(
|
||||||
// add user to workspace as admin
|
userId,
|
||||||
|
createdWorkspace.id,
|
||||||
|
{ name: 'General' } as CreateSpaceDto,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
const newWorkspace = await this.create(userId);
|
await this.spaceService.addUserToSpace(
|
||||||
await this.addUserToWorkspace(userId, newWorkspace.id, 'owner');
|
userId,
|
||||||
|
createdSpace.id,
|
||||||
|
WorkspaceUserRole.OWNER,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
// maybe create default space and add user to it too.
|
createdWorkspace.defaultSpaceId = createdSpace.id;
|
||||||
const newSpace = await this.spaceService.create(userId, newWorkspace.id);
|
await manager.save(createdWorkspace);
|
||||||
await this.spaceService.addUserToSpace(userId, newSpace.id, 'owner');
|
} else {
|
||||||
} else {
|
// limited to single workspace
|
||||||
//TODO: accept role as param
|
// fetch the oldest workspace and add user to it
|
||||||
// if no role is passed use default new member role found in workspace settings
|
const firstWorkspace = await manager.find(Workspace, {
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
|
||||||
// fetch the oldest workspace and add user to it
|
// add user to workspace and default space
|
||||||
const firstWorkspace = await this.workspaceRepository.find({
|
|
||||||
order: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
take: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.addUserToWorkspace(userId, firstWorkspace[0].id, 'member');
|
await this.addUserToWorkspace(
|
||||||
// get workspace
|
userId,
|
||||||
// if there is a default space, we should add new users to it.
|
firstWorkspace[0].id,
|
||||||
}
|
WorkspaceUserRole.MEMBER,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.spaceService.addUserToSpace(
|
||||||
|
userId,
|
||||||
|
firstWorkspace[0].defaultSpaceId,
|
||||||
|
WorkspaceUserRole.MEMBER,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
userId: string,
|
userId: string,
|
||||||
createWorkspaceDto?: CreateWorkspaceDto,
|
createWorkspaceDto?: CreateWorkspaceDto,
|
||||||
|
manager?: EntityManager,
|
||||||
): Promise<Workspace> {
|
): Promise<Workspace> {
|
||||||
let workspace: Workspace;
|
let workspace: Workspace;
|
||||||
|
|
||||||
if (createWorkspaceDto) {
|
await transactionWrapper(
|
||||||
workspace = plainToInstance(Workspace, createWorkspaceDto);
|
async (manager) => {
|
||||||
} else {
|
if (createWorkspaceDto) {
|
||||||
workspace = new Workspace();
|
workspace = plainToInstance(Workspace, createWorkspaceDto);
|
||||||
}
|
} else {
|
||||||
|
workspace = new Workspace();
|
||||||
|
}
|
||||||
|
|
||||||
workspace.inviteCode = uuid();
|
workspace.inviteCode = uuid();
|
||||||
workspace.creatorId = userId;
|
workspace.creatorId = userId;
|
||||||
|
|
||||||
if (workspace.name && !workspace.hostname?.trim()) {
|
//if (workspace.name && !workspace.hostname?.trim()) {
|
||||||
workspace.hostname = generateHostname(createWorkspaceDto.name);
|
// workspace.hostname = generateHostname(createWorkspaceDto.name);
|
||||||
}
|
// }
|
||||||
|
|
||||||
workspace = await this.workspaceRepository.save(workspace);
|
workspace = await manager.save(workspace);
|
||||||
|
},
|
||||||
|
this.dataSource,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
@ -136,41 +184,73 @@ export class WorkspaceService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
role: string,
|
role: string,
|
||||||
|
manager?: EntityManager,
|
||||||
): Promise<WorkspaceUser> {
|
): Promise<WorkspaceUser> {
|
||||||
const existingWorkspaceUser = await this.workspaceUserRepository.findOne({
|
let addedUser;
|
||||||
where: { userId: userId, workspaceId: workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingWorkspaceUser) {
|
await transactionWrapper(
|
||||||
throw new BadRequestException('User already added to this workspace');
|
async (manager) => {
|
||||||
}
|
const existingWorkspaceUser = await manager.findOne(WorkspaceUser, {
|
||||||
|
where: { userId: userId, workspaceId: workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
const workspaceUser = new WorkspaceUser();
|
const userExists = await manager.exists(User, {
|
||||||
workspaceUser.userId = userId;
|
where: { id: userId },
|
||||||
workspaceUser.workspaceId = workspaceId;
|
});
|
||||||
workspaceUser.role = role;
|
if (!userExists) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
return this.workspaceUserRepository.save(workspaceUser);
|
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,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
return addedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWorkspaceUserRole(
|
async updateWorkspaceUserRole(
|
||||||
|
authUser: User,
|
||||||
workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const workspaceUser = await this.workspaceUserRepository.findOne({
|
const workspaceUser = await this.getWorkspaceUser(
|
||||||
where: { userId: workspaceUserRoleDto.userId, workspaceId: workspaceId },
|
workspaceUserRoleDto.userId,
|
||||||
});
|
workspaceId,
|
||||||
|
);
|
||||||
if (!workspaceUser) {
|
|
||||||
throw new BadRequestException('user is not a member of this workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workspaceUser.role === workspaceUserRoleDto.role) {
|
if (workspaceUser.role === workspaceUserRoleDto.role) {
|
||||||
return workspaceUser;
|
return workspaceUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
workspaceUser.role = workspaceUserRoleDto.role;
|
workspaceUser.role = workspaceUserRoleDto.role;
|
||||||
// TODO: if there is only one workspace owner, prevent the role change
|
|
||||||
|
|
||||||
return this.workspaceUserRepository.save(workspaceUser);
|
return this.workspaceUserRepository.save(workspaceUser);
|
||||||
}
|
}
|
||||||
@ -179,7 +259,24 @@ export class WorkspaceService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.validateWorkspaceMember(userId, workspaceId);
|
await this.getWorkspaceUser(userId, workspaceId);
|
||||||
|
|
||||||
|
const workspaceUser = await this.getWorkspaceUser(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({
|
await this.workspaceUserRepository.delete({
|
||||||
userId,
|
userId,
|
||||||
@ -188,11 +285,12 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserCurrentWorkspace(userId: string): Promise<Workspace> {
|
async getUserCurrentWorkspace(userId: string): Promise<Workspace> {
|
||||||
// TODO: use workspaceId and fetch workspace based on the id
|
|
||||||
// we currently assume the user belongs to one workspace
|
|
||||||
const userWorkspace = await this.workspaceUserRepository.findOne({
|
const userWorkspace = await this.workspaceUserRepository.findOne({
|
||||||
where: { userId: userId },
|
where: { userId: userId },
|
||||||
relations: ['workspace'],
|
relations: ['workspace'],
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userWorkspace) {
|
if (!userWorkspace) {
|
||||||
@ -202,15 +300,25 @@ export class WorkspaceService {
|
|||||||
return userWorkspace.workspace;
|
return userWorkspace.workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserWorkspaces(userId: string): Promise<Workspace[]> {
|
async getUserWorkspaces(
|
||||||
const workspaces = await this.workspaceUserRepository.find({
|
userId: string,
|
||||||
where: { userId: userId },
|
paginationOptions: PaginationOptions,
|
||||||
relations: ['workspace'],
|
): Promise<PaginatedResult<Workspace>> {
|
||||||
});
|
const [workspaces, count] = await this.workspaceUserRepository.findAndCount(
|
||||||
|
{
|
||||||
|
where: { userId: userId },
|
||||||
|
relations: ['workspace'],
|
||||||
|
take: paginationOptions.limit,
|
||||||
|
skip: paginationOptions.skip,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return workspaces.map(
|
const userWorkspaces = workspaces.map(
|
||||||
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
|
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
|
||||||
|
return new PaginatedResult(userWorkspaces, paginationMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceUsers(
|
async getWorkspaceUsers(
|
||||||
@ -241,16 +349,26 @@ export class WorkspaceService {
|
|||||||
return new PaginatedResult(users, paginationMeta);
|
return new PaginatedResult(users, paginationMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateWorkspaceMember(
|
async getUserRoleInWorkspace(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<void> {
|
): Promise<string> {
|
||||||
|
const workspaceUser = await this.getWorkspaceUser(userId, workspaceId);
|
||||||
|
return workspaceUser.role ? workspaceUser.role : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkspaceUser(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<WorkspaceUser> {
|
||||||
const workspaceUser = await this.workspaceUserRepository.findOne({
|
const workspaceUser = await this.workspaceUserRepository.findOne({
|
||||||
where: { userId, workspaceId },
|
where: { userId, workspaceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workspaceUser) {
|
if (!workspaceUser) {
|
||||||
throw new BadRequestException('User is not a member of this workspace');
|
throw new BadRequestException('Workspace member not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return workspaceUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class DefaultSpace1709215284760 implements MigrationInterface {
|
||||||
|
name = 'DefaultSpace1709215284760'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ADD "defaultSpaceId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ADD CONSTRAINT "UQ_e91d3ec686ece9654aa9f635981" UNIQUE ("defaultSpaceId")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" ADD CONSTRAINT "FK_e91d3ec686ece9654aa9f635981" FOREIGN KEY ("defaultSpaceId") REFERENCES "spaces"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" DROP CONSTRAINT "FK_e91d3ec686ece9654aa9f635981"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" DROP CONSTRAINT "UQ_e91d3ec686ece9654aa9f635981"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "workspaces" DROP COLUMN "defaultSpaceId"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ export const CurrentWorkspace = createParamDecorator(
|
|||||||
const request = ctx.switchToHttp().getRequest();
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
|
||||||
if (!request['user'] || !request['user'].workspace) {
|
if (!request['user'] || !request['user'].workspace) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException('Workspace not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return request['user'] ? request['user'].workspace : undefined;
|
return request['user'] ? request['user'].workspace : undefined;
|
||||||
|
|||||||
17
apps/server/src/helpers/db.helper.ts
Normal file
17
apps/server/src/helpers/db.helper.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
|
|
||||||
|
export async function transactionWrapper(
|
||||||
|
operation: (...args) => any,
|
||||||
|
datasource: DataSource,
|
||||||
|
entityManager: EntityManager,
|
||||||
|
) {
|
||||||
|
if (entityManager) {
|
||||||
|
return await operation(entityManager);
|
||||||
|
} else {
|
||||||
|
return await datasource.manager.transaction(
|
||||||
|
async (manager: EntityManager) => {
|
||||||
|
return await operation(manager);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ export class PaginationOptions {
|
|||||||
@IsPositive()
|
@IsPositive()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@Max(100)
|
@Max(100)
|
||||||
limit = 25;
|
limit = 20;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ -21,3 +21,8 @@ export class PaginationOptions {
|
|||||||
return (this.page - 1) * this.limit;
|
return (this.page - 1) * this.limit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Order {
|
||||||
|
ASC = 'ASC',
|
||||||
|
DESC = 'DESC',
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user