mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 11:01:08 +10:00
Refactoring
* Refactor workspace membership system * Create setup endpoint * Use Passport.js * Several updates and fixes
This commit is contained in:
@ -9,4 +9,8 @@ export class CreateSpaceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
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()
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.spaceUsers, {
|
||||
@ManyToOne(() => User, (user) => user.spaces, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'userId' })
|
||||
@ -32,7 +32,6 @@ export class SpaceUser {
|
||||
@ManyToOne(() => Space, (space) => space.spaceUsers, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'spaceId' })
|
||||
space: Space;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
|
||||
@ -6,14 +6,17 @@ import {
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||
import { SpaceUser } from './space-user.entity';
|
||||
import { Page } from '../../page/entities/page.entity';
|
||||
import { SpacePrivacy, SpaceRole } from '../../../helpers/types/permission';
|
||||
|
||||
@Entity('spaces')
|
||||
@Unique(['slug', 'workspaceId'])
|
||||
export class Space {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@ -24,11 +27,17 @@ export class Space {
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
slug: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
icon: string;
|
||||
|
||||
@Column({ length: 255, nullable: true, unique: true })
|
||||
hostname: string;
|
||||
@Column({ length: 100, default: SpacePrivacy.OPEN })
|
||||
privacy: string;
|
||||
|
||||
@Column({ length: 100, default: SpaceRole.WRITER })
|
||||
defaultRole: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
@ -46,7 +55,7 @@ export class Space {
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
workspace: Workspace;
|
||||
|
||||
@OneToMany(() => SpaceUser, (workspaceUser) => workspaceUser.space)
|
||||
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.space)
|
||||
spaceUsers: SpaceUser[];
|
||||
|
||||
@OneToMany(() => Page, (page) => page.space)
|
||||
|
||||
@ -8,7 +8,11 @@ export class SpaceRepository extends Repository<Space> {
|
||||
super(Space, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
async findById(spaceId: string) {
|
||||
return this.findOneBy({ id: spaceId });
|
||||
async findById(spaceId: string, workspaceId: string): Promise<Space> {
|
||||
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 {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtGuard } from '../auth/guards/jwt.guard';
|
||||
import { SpaceService } from './space.service';
|
||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||
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 { 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')
|
||||
export class SpaceController {
|
||||
constructor(private readonly spaceService: SpaceService) {}
|
||||
|
||||
// get all spaces user is a member of
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/')
|
||||
async getUserSpaces(
|
||||
async getWorkspaceSpaces(
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
@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 { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Space } from './entities/space.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { SpaceUser } from './entities/space-user.entity';
|
||||
import { SpaceRepository } from './repositories/space.repository';
|
||||
import { SpaceUserRepository } from './repositories/space-user.repository';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Space, SpaceUser]), AuthModule],
|
||||
imports: [TypeOrmModule.forFeature([Space, SpaceUser])],
|
||||
controllers: [SpaceController],
|
||||
providers: [SpaceService, SpaceRepository, SpaceUserRepository],
|
||||
exports: [SpaceService, SpaceRepository, SpaceUserRepository],
|
||||
|
||||
@ -5,14 +5,15 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { CreateSpaceDto } from './dto/create-space.dto';
|
||||
import { Space } from './entities/space.entity';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { SpaceRepository } from './repositories/space.repository';
|
||||
import { SpaceUserRepository } from './repositories/space-user.repository';
|
||||
import { SpaceUser } from './entities/space-user.entity';
|
||||
import { transactionWrapper } from '../../helpers/db.helper';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
import { WorkspaceUser } from '../workspace/entities/workspace-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()
|
||||
export class SpaceService {
|
||||
@ -24,33 +25,26 @@ export class SpaceService {
|
||||
|
||||
async create(
|
||||
userId: string,
|
||||
workspaceId,
|
||||
workspaceId: string,
|
||||
createSpaceDto?: CreateSpaceDto,
|
||||
manager?: EntityManager,
|
||||
) {
|
||||
let space: Space;
|
||||
|
||||
await transactionWrapper(
|
||||
): Promise<Space> {
|
||||
return await transactionWrapper(
|
||||
async (manager: EntityManager) => {
|
||||
if (createSpaceDto) {
|
||||
space = plainToInstance(Space, createSpaceDto);
|
||||
} else {
|
||||
space = new Space();
|
||||
}
|
||||
|
||||
const space = new Space();
|
||||
space.name = createSpaceDto.name ?? 'untitled space ';
|
||||
space.description = createSpaceDto.description ?? '';
|
||||
space.creatorId = userId;
|
||||
space.workspaceId = workspaceId;
|
||||
|
||||
space.name = createSpaceDto?.name ?? 'untitled space';
|
||||
space.description = createSpaceDto?.description ?? null;
|
||||
space.slug = space.name.toLowerCase(); // TODO: fix
|
||||
|
||||
space = await manager.save(space);
|
||||
await manager.save(space);
|
||||
return space;
|
||||
},
|
||||
this.dataSource,
|
||||
manager,
|
||||
);
|
||||
|
||||
return space;
|
||||
}
|
||||
|
||||
async addUserToSpace(
|
||||
@ -60,27 +54,15 @@ export class SpaceService {
|
||||
workspaceId,
|
||||
manager?: EntityManager,
|
||||
): Promise<SpaceUser> {
|
||||
let addedUser: SpaceUser;
|
||||
|
||||
await transactionWrapper(
|
||||
return await transactionWrapper(
|
||||
async (manager: EntityManager) => {
|
||||
const userExists = await manager.exists(User, {
|
||||
where: { id: userId },
|
||||
where: { id: userId, workspaceId },
|
||||
});
|
||||
if (!userExists) {
|
||||
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, {
|
||||
userId: userId,
|
||||
spaceId: spaceId,
|
||||
@ -94,27 +76,106 @@ export class SpaceService {
|
||||
spaceUser.userId = userId;
|
||||
spaceUser.spaceId = spaceId;
|
||||
spaceUser.role = role;
|
||||
await manager.save(spaceUser);
|
||||
|
||||
addedUser = await manager.save(spaceUser);
|
||||
return spaceUser;
|
||||
},
|
||||
this.dataSource,
|
||||
manager,
|
||||
);
|
||||
|
||||
return addedUser;
|
||||
}
|
||||
|
||||
async getUserSpacesInWorkspace(userId: string, workspaceId: string) {
|
||||
const spaces = await this.spaceUserRepository.find({
|
||||
relations: ['space'],
|
||||
async getSpaceInfo(spaceId: string, workspaceId: string): Promise<Space> {
|
||||
const space = await this.spaceRepository
|
||||
.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: {
|
||||
userId: userId,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user