updates and fixes

* seo friendly urls
* custom client serve-static module
* database fixes
* fix recent pages
* other fixes
This commit is contained in:
Philipinho
2024-05-18 03:19:42 +01:00
parent eefe63d1cd
commit 9c7c2f1163
102 changed files with 921 additions and 536 deletions

View File

@ -9,6 +9,7 @@ import { executeTx } from '@docmost/db/utils';
import { InjectKysely } from 'nestjs-kysely';
import { User } from '@docmost/db/types/entity.types';
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
import { UserRole } from '../../../helpers/types/permission';
@Injectable()
export class SignupService {
@ -75,7 +76,11 @@ export class SignupService {
this.db,
async (trx) => {
// create user
const user = await this.userRepo.insertUser(createAdminUserDto, trx);
const user = await this.userRepo.insertUser(
{ ...createAdminUserDto, role: UserRole.OWNER },
trx,
);
// create workspace with full setup
const workspaceData: CreateWorkspaceDto = {

View File

@ -1,4 +1,8 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import {
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import {
AbilityBuilder,
createMongoAbility,
@ -33,9 +37,7 @@ export default class SpaceAbilityFactory {
case SpaceRole.READER:
return buildSpaceReaderAbility();
default:
throw new ForbiddenException(
'You do not have permission to access this space',
);
throw new NotFoundException('Space permissions not found');
}
}
}

View File

@ -52,7 +52,12 @@ export class CommentController {
throw new ForbiddenException();
}
return this.commentService.create(user.id, workspace.id, createCommentDto);
return this.commentService.create(
user.id,
page.id,
workspace.id,
createCommentDto,
);
}
@HttpCode(HttpStatus.OK)
@ -73,7 +78,7 @@ export class CommentController {
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.commentService.findByPageId(input.pageId, pagination);
return this.commentService.findByPageId(page.id, pagination);
}
@HttpCode(HttpStatus.OK)
@ -84,7 +89,6 @@ export class CommentController {
throw new NotFoundException('Comment not found');
}
// TODO: add spaceId to comment entity.
const page = await this.pageRepo.findById(comment.pageId);
if (!page) {
throw new NotFoundException('Page not found');
@ -104,6 +108,7 @@ export class CommentController {
return this.commentService.update(
updateCommentDto.commentId,
updateCommentDto,
user,
);
}
@ -111,6 +116,6 @@ export class CommentController {
@Post('delete')
remove(@Body() input: CommentIdDto, @AuthUser() user: User) {
// TODO: only comment creators and admins can delete their comments
return this.commentService.remove(input.commentId);
return this.commentService.remove(input.commentId, user);
}
}

View File

@ -1,12 +1,13 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateCommentDto } from './dto/create-comment.dto';
import { UpdateCommentDto } from './dto/update-comment.dto';
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
import { Comment } from '@docmost/db/types/entity.types';
import { Comment, User } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { PaginationResult } from '@docmost/db/pagination/pagination';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
@ -30,24 +31,18 @@ export class CommentService {
async create(
userId: string,
pageId: string,
workspaceId: string,
createCommentDto: CreateCommentDto,
) {
const commentContent = JSON.parse(createCommentDto.content);
const page = await this.pageRepo.findById(createCommentDto.pageId);
// const spaceId = null; // todo, get from page
if (!page) {
throw new BadRequestException('Page not found');
}
if (createCommentDto.parentCommentId) {
const parentComment = await this.commentRepo.findById(
createCommentDto.parentCommentId,
);
if (!parentComment) {
if (!parentComment || parentComment.pageId !== pageId) {
throw new BadRequestException('Parent comment not found');
}
@ -57,10 +52,10 @@ export class CommentService {
}
const createdComment = await this.commentRepo.insertComment({
pageId: createCommentDto.pageId,
pageId: pageId,
content: commentContent,
selection: createCommentDto?.selection?.substring(0, 250),
type: 'inline', // for now
type: 'inline',
parentCommentId: createCommentDto?.parentCommentId,
creatorId: userId,
workspaceId: workspaceId,
@ -90,6 +85,7 @@ export class CommentService {
async update(
commentId: string,
updateCommentDto: UpdateCommentDto,
authUser: User,
): Promise<Comment> {
const commentContent = JSON.parse(updateCommentDto.content);
@ -98,6 +94,10 @@ export class CommentService {
throw new NotFoundException('Comment not found');
}
if (comment.creatorId !== authUser.id) {
throw new ForbiddenException('You can only edit your own comments');
}
const editedAt = new Date();
await this.commentRepo.updateComment(
@ -113,12 +113,17 @@ export class CommentService {
return comment;
}
async remove(commentId: string): Promise<void> {
async remove(commentId: string, authUser: User): Promise<void> {
const comment = await this.commentRepo.findById(commentId);
if (!comment) {
throw new NotFoundException('Comment not found');
}
if (comment.creatorId !== authUser.id) {
throw new ForbiddenException('You can only delete your own comments');
}
await this.commentRepo.deleteComment(commentId);
}
}

View File

@ -1,7 +1,7 @@
import { IsUUID } from 'class-validator';
import { IsString, IsUUID } from 'class-validator';
export class PageIdDto {
@IsUUID()
@IsString()
pageId: string;
}

View File

@ -1,7 +1,7 @@
import { IsJSON, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateCommentDto {
@IsUUID()
@IsString()
pageId: string;
@IsJSON()

View File

@ -10,7 +10,7 @@ export class CreatePageDto {
icon?: string;
@IsOptional()
@IsUUID()
@IsString()
parentPageId?: string;
@IsUUID()

View File

@ -1,13 +1,7 @@
import {
IsString,
IsUUID,
IsOptional,
MinLength,
MaxLength,
} from 'class-validator';
import { IsString, IsOptional, MinLength, MaxLength } from 'class-validator';
export class MovePageDto {
@IsUUID()
@IsString()
pageId: string;
@IsString()

View File

@ -1,3 +0,0 @@
import { Page } from '@docmost/db/types/entity.types';
export type PageWithOrderingDto = Page & { childrenIds?: string[] };

View File

@ -1,7 +1,7 @@
import { IsUUID } from 'class-validator';
import { IsString, IsUUID } from 'class-validator';
export class PageIdDto {
@IsUUID()
@IsString()
pageId: string;
}

View File

@ -1,8 +1,8 @@
import { IsOptional, IsUUID } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
import { SpaceIdDto } from './page.dto';
export class SidebarPageDto extends SpaceIdDto {
@IsOptional()
@IsUUID()
@IsString()
pageId: string;
}

View File

@ -1,8 +1,8 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreatePageDto } from './create-page.dto';
import { IsUUID } from 'class-validator';
import { IsString } from 'class-validator';
export class UpdatePageDto extends PartialType(CreatePageDto) {
@IsUUID()
@IsString()
pageId: string;
}

View File

@ -12,7 +12,7 @@ import { PageService } from './services/page.service';
import { CreatePageDto } from './dto/create-page.dto';
import { UpdatePageDto } from './dto/update-page.dto';
import { MovePageDto } from './dto/move-page.dto';
import { PageHistoryIdDto, PageIdDto, SpaceIdDto } from './dto/page.dto';
import { PageHistoryIdDto, PageIdDto } from './dto/page.dto';
import { PageHistoryService } from './services/page-history.service';
import { AuthUser } from '../../decorators/auth-user.decorator';
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
@ -118,18 +118,23 @@ export class PageController {
@HttpCode(HttpStatus.OK)
@Post('recent')
async getRecentSpacePages(
@Body() spaceIdDto: SpaceIdDto,
@Body() pagination: PaginationOptions,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const ability = await this.spaceAbility.createForUser(
user,
spaceIdDto.spaceId,
workspace.defaultSpaceId,
);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.pageService.getRecentSpacePages(spaceIdDto.spaceId, pagination);
return this.pageService.getRecentSpacePages(
workspace.defaultSpaceId,
pagination,
);
}
// TODO: scope to workspaces
@ -146,7 +151,7 @@ export class PageController {
throw new ForbiddenException();
}
return this.pageHistoryService.findHistoryByPageId(dto.pageId, pagination);
return this.pageHistoryService.findHistoryByPageId(page.id, pagination);
}
@HttpCode(HttpStatus.OK)
@ -181,7 +186,17 @@ export class PageController {
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.pageService.getSidebarPages(dto, pagination);
let pageId = null;
if (dto.pageId) {
const page = await this.pageRepo.findById(dto.pageId);
if (page.spaceId !== dto.spaceId) {
throw new ForbiddenException();
}
pageId = page.id;
}
return this.pageService.getSidebarPages(dto.spaceId, pagination, pageId);
}
@HttpCode(HttpStatus.OK)
@ -207,10 +222,14 @@ export class PageController {
@Post('/breadcrumbs')
async getPageBreadcrumbs(@Body() dto: PageIdDto, @AuthUser() user: User) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.pageService.getPageBreadCrumbs(dto.pageId);
return this.pageService.getPageBreadCrumbs(page.id);
}
}

View File

@ -18,7 +18,7 @@ import { generateJitteredKeyBetween } from 'fractional-indexing-jittered';
import { MovePageDto } from '../dto/move-page.dto';
import { ExpressionBuilder } from 'kysely';
import { DB } from '@docmost/db/types/db';
import { SidebarPageDto } from '../dto/sidebar-page.dto';
import { genPageShortId } from '../../../helpers/nanoid.utils';
@Injectable()
export class PageService {
@ -40,14 +40,19 @@ export class PageService {
workspaceId: string,
createPageDto: CreatePageDto,
): Promise<Page> {
let parentPageId = undefined;
// check if parent page exists
if (createPageDto.parentPageId) {
const parentPage = await this.pageRepo.findById(
createPageDto.parentPageId,
);
if (!parentPage || parentPage.spaceId !== createPageDto.spaceId)
if (!parentPage || parentPage.spaceId !== createPageDto.spaceId) {
throw new NotFoundException('Parent page not found');
}
parentPageId = parentPage.id;
}
let pagePosition: string;
@ -59,10 +64,10 @@ export class PageService {
.orderBy('position', 'desc')
.limit(1);
if (createPageDto.parentPageId) {
if (parentPageId) {
// check for children of this page
const lastPage = await lastPageQuery
.where('parentPageId', '=', createPageDto.parentPageId)
.where('parentPageId', '=', parentPageId)
.executeTakeFirst();
if (!lastPage) {
@ -87,10 +92,11 @@ export class PageService {
}
const createdPage = await this.pageRepo.insertPage({
slugId: genPageShortId(),
title: createPageDto.title,
position: pagePosition,
icon: createPageDto.icon,
parentPageId: createPageDto.parentPageId,
parentPageId: parentPageId,
spaceId: createPageDto.spaceId,
creatorId: userId,
workspaceId: workspaceId,
@ -110,6 +116,7 @@ export class PageService {
title: updatePageDto.title,
icon: updatePageDto.icon,
lastUpdatedById: userId,
updatedAt: new Date(),
},
pageId,
);
@ -135,13 +142,15 @@ export class PageService {
}
async getSidebarPages(
dto: SidebarPageDto,
spaceId: string,
pagination: PaginationOptions,
pageId?: string,
): Promise<any> {
let query = this.db
.selectFrom('pages')
.select([
'id',
'slugId',
'title',
'icon',
'position',
@ -151,10 +160,10 @@ export class PageService {
])
.select((eb) => this.withHasChildren(eb))
.orderBy('position', 'asc')
.where('spaceId', '=', dto.spaceId);
.where('spaceId', '=', spaceId);
if (dto.pageId) {
query = query.where('parentPageId', '=', dto.pageId);
if (pageId) {
query = query.where('parentPageId', '=', pageId);
} else {
query = query.where('parentPageId', 'is', null);
}
@ -185,8 +194,8 @@ export class PageService {
if (!parentPage || parentPage.spaceId !== movedPage.spaceId) {
throw new NotFoundException('Parent page not found');
}
parentPageId = parentPage.id;
}
parentPageId = dto.parentPageId;
}
await this.pageRepo.updatePage(
@ -205,6 +214,7 @@ export class PageService {
.selectFrom('pages')
.select([
'id',
'slugId',
'title',
'icon',
'position',
@ -218,6 +228,7 @@ export class PageService {
.selectFrom('pages as p')
.select([
'p.id',
'p.slugId',
'p.title',
'p.icon',
'p.position',
@ -255,10 +266,7 @@ export class PageService {
spaceId: string,
pagination: PaginationOptions,
): Promise<PaginationResult<Page>> {
const pages = await this.pageRepo.getRecentPagesInSpace(
spaceId,
pagination,
);
const pages = await this.pageRepo.getRecentPageUpdates(spaceId, pagination);
return pages;
}
@ -267,6 +275,7 @@ export class PageService {
await this.pageRepo.deletePage(pageId);
}
}
/*
// TODO: page deletion and restoration
async delete(pageId: string): Promise<void> {

View File

@ -5,12 +5,13 @@ import {
} from '@nestjs/common';
import { CreateSpaceDto } from '../dto/create-space.dto';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import slugify from 'slugify';
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
import { Space } from '@docmost/db/types/entity.types';
import { PaginationResult } from '@docmost/db/pagination/pagination';
import { UpdateSpaceDto } from '../dto/update-space.dto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { slugify } = require('fix-esm').require('@sindresorhus/slugify');
@Injectable()
export class SpaceService {

View File

@ -36,12 +36,16 @@ export class WorkspaceController {
private readonly workspaceInvitationService: WorkspaceInvitationService,
) {}
@Public()
@HttpCode(HttpStatus.OK)
@Post('/public')
async getWorkspacePublicInfo(@Req() req) {
return this.workspaceService.getWorkspacePublicData(req.raw.workspaceId);
}
@HttpCode(HttpStatus.OK)
@Post('/info')
async getWorkspace(
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
async getWorkspace(@AuthWorkspace() workspace: Workspace) {
return this.workspaceService.getWorkspaceInfo(workspace.id);
}

View File

@ -46,6 +46,19 @@ export class WorkspaceService {
return workspace;
}
async getWorkspacePublicData(workspaceId: string) {
const workspace = await this.db
.selectFrom('workspaces')
.select(['id'])
.where('id', '=', workspaceId)
.executeTakeFirst();
if (!workspace) {
throw new NotFoundException('Workspace not found');
}
return workspace;
}
async create(
user: User,
createWorkspaceDto: CreateWorkspaceDto,