mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-18 10:41:04 +10:00
updates and fixes
* seo friendly urls * custom client serve-static module * database fixes * fix recent pages * other fixes
This commit is contained in:
@ -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 = {
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class PageIdDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsJSON, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class CreateCommentDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
|
||||
@IsJSON()
|
||||
|
||||
@ -10,7 +10,7 @@ export class CreatePageDto {
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
parentPageId?: string;
|
||||
|
||||
@IsUUID()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import { Page } from '@docmost/db/types/entity.types';
|
||||
|
||||
export type PageWithOrderingDto = Page & { childrenIds?: string[] };
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class PageIdDto {
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user