Share - WIP

This commit is contained in:
Philipinho
2025-04-09 13:26:50 +01:00
parent a9f370660b
commit 18e8c4cbaf
19 changed files with 514 additions and 6 deletions

View File

@ -15,6 +15,7 @@ import { SpaceModule } from './space/space.module';
import { GroupModule } from './group/group.module';
import { CaslModule } from './casl/casl.module';
import { DomainMiddleware } from '../common/middlewares/domain.middleware';
import { ShareModule } from './share/share.module';
@Module({
imports: [
@ -28,6 +29,7 @@ import { DomainMiddleware } from '../common/middlewares/domain.middleware';
SpaceModule,
GroupModule,
CaslModule,
ShareModule,
],
})
export class CoreModule implements NestModule {

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class CreateShareDto {
@IsString()
pageId: string;
}

View File

@ -0,0 +1,28 @@
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
export class ShareIdDto {
@IsString()
@IsNotEmpty()
shareId: string;
}
export class SpaceIdDto {
@IsUUID()
spaceId: string;
}
export class ShareInfoDto extends ShareIdDto {
@IsString()
@IsOptional()
pageId: string;
// @IsOptional()
// @IsBoolean()
// includeContent: boolean;
}

View File

@ -0,0 +1,8 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateShareDto } from './create-share.dto';
import { IsString } from 'class-validator';
export class UpdateShareDto extends PartialType(CreateShareDto) {
//@IsString()
//pageId: string;
}

View File

@ -0,0 +1,105 @@
import {
Body,
Controller,
ForbiddenException,
HttpCode,
HttpStatus,
NotFoundException,
Post,
UseGuards,
} from '@nestjs/common';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { User, Workspace } from '@docmost/db/types/entity.types';
import {
SpaceCaslAction,
SpaceCaslSubject,
} from '../casl/interfaces/space-ability.type';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import { ShareService } from './share.service';
import { UpdateShareDto } from './dto/update-page.dto';
import { CreateShareDto } from './dto/create-share.dto';
import { ShareIdDto, ShareInfoDto } from './dto/share.dto';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { Public } from '../../common/decorators/public.decorator';
@UseGuards(JwtAuthGuard)
@Controller('shares')
export class ShareController {
constructor(
private readonly shareService: ShareService,
private readonly spaceAbility: SpaceAbilityFactory,
private readonly pageRepo: PageRepo,
) {}
@Public()
@HttpCode(HttpStatus.OK)
@Post('/info')
async getPage(@Body() dto: ShareInfoDto) {
return this.shareService.getShare(dto);
}
@HttpCode(HttpStatus.OK)
@Post('create')
async create(
@Body() createShareDto: CreateShareDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const page = await this.pageRepo.findById(createShareDto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.shareService.createShare({
pageId: page.id,
authUserId: user.id,
workspaceId: workspace.id,
});
}
@HttpCode(HttpStatus.OK)
@Post('update')
async update(@Body() updatePageDto: UpdateShareDto, @AuthUser() user: User) {
/* const page = await this.pageRepo.findById(updatePageDto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
//return this.shareService.update(page, updatePageDto, user.id);
*/
}
@HttpCode(HttpStatus.OK)
@Post('delete')
async delete(@Body() shareIdDto: ShareIdDto, @AuthUser() user: User) {
/* const page = await this.pageRepo.findById(pageIdDto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
*/
// await this.shareService.forceDelete(pageIdDto.pageId);
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ShareController } from './share.controller';
import { ShareService } from './share.service';
@Module({
controllers: [ShareController],
providers: [ShareService],
exports: [ShareService],
})
export class ShareModule {}

View File

@ -0,0 +1,65 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ShareInfoDto } from './dto/share.dto';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types';
import { generateSlugId } from '../../common/helpers';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
@Injectable()
export class ShareService {
constructor(
private readonly pageRepo: PageRepo,
@InjectKysely() private readonly db: KyselyDB,
) {}
async createShare(opts: {
authUserId: string;
workspaceId: string;
pageId: string;
}) {
const { authUserId, workspaceId, pageId } = opts;
const slugId = generateSlugId(); // or custom slug
const share = this.db
.insertInto('shares')
.values({ slugId: slugId, pageId, creatorId: authUserId, workspaceId })
.returningAll()
.executeTakeFirst();
return share;
}
async getShare(dto: ShareInfoDto) {
// for now only single page share
// if only share Id is provided, return
// if share id is pass with page id, what to do?
// if uuid is used, use Id
const share = await this.db
.selectFrom('shares')
.selectAll()
.where('slugId', '=', dto.shareId)
.executeTakeFirst();
if (!share) {
throw new NotFoundException('Share not found');
}
const page = await this.pageRepo.findById(share.pageId, {
includeContent: true,
includeCreator: true,
});
// cleanup json content
// remove comments mark
// make sure attachments work (videos, images, excalidraw, drawio)
// figure out internal links?
if (!page) {
throw new NotFoundException('Page not found');
}
return page;
}
}

View File

@ -0,0 +1,41 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('shares')
.addColumn('id', 'uuid', (col) =>
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
)
.addColumn('slug_id', 'varchar', (col) => col.notNull())
.addColumn('page_id', 'varchar', (col) => col.notNull())
.addColumn('include_sub_pages', 'varchar', (col) => col)
.addColumn('creator_id', 'uuid', (col) => col.references('users.id'))
// pageSlug
//.addColumn('space_id', 'uuid', (col) =>
// col.references('spaces.id').onDelete('cascade').notNull(),
// )
.addColumn('workspace_id', 'uuid', (col) =>
col.references('workspaces.id').onDelete('cascade').notNull(),
)
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('updated_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('deleted_at', 'timestamptz', (col) => col)
.addUniqueConstraint('shares_slug_id_unique', ['slug_id'])
.execute();
await db.schema
.createIndex('shares_slug_id_idx')
.on('shares')
.column('slug_id')
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('shares').execute();
}

View File

@ -183,6 +183,18 @@ export interface Pages {
ydoc: Buffer | null;
}
export interface Shares {
createdAt: Generated<Timestamp>;
creatorId: string | null;
deletedAt: Timestamp | null;
id: Generated<string>;
includeSubPages: string | null;
pageId: string;
slugId: string;
updatedAt: Generated<Timestamp>;
workspaceId: string;
}
export interface SpaceMembers {
addedById: string | null;
createdAt: Generated<Timestamp>;
@ -288,6 +300,7 @@ export interface DB {
groupUsers: GroupUsers;
pageHistory: PageHistory;
pages: Pages;
shares: Shares;
spaceMembers: SpaceMembers;
spaces: Spaces;
users: Users;

View File

@ -16,6 +16,7 @@ import {
Billing as BillingSubscription,
AuthProviders,
AuthAccounts,
Shares,
} from './db';
// Workspace
@ -101,3 +102,8 @@ export type UpdatableAuthProvider = Updateable<Omit<AuthProviders, 'id'>>;
export type AuthAccount = Selectable<AuthAccounts>;
export type InsertableAuthAccount = Insertable<AuthAccounts>;
export type UpdatableAuthAccount = Updateable<Omit<AuthAccounts, 'id'>>;
// Share
export type Share = Selectable<Shares>;
export type InsertableShare = Insertable<Shares>;
export type UpdatableShare = Updateable<Omit<Shares, 'id'>>;