Refactoring

* Refactor workspace membership system
* Create setup endpoint
* Use Passport.js
* Several updates and fixes
This commit is contained in:
Philipinho
2024-03-16 22:58:12 +00:00
parent b42fe48e9b
commit a821e37028
87 changed files with 2703 additions and 2307 deletions

View File

@ -9,4 +9,8 @@ export class CreateSpaceDto {
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
slug?: string;
}

View File

@ -0,0 +1,8 @@
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class SpaceIdDto {
@IsString()
@IsNotEmpty()
@IsUUID()
spaceId: string;
}

View File

@ -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 })

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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,
);
}
}

View File

@ -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],

View File

@ -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);
}
}