mirror of
https://github.com/docmost/docmost.git
synced 2025-11-19 02:11:21 +10:00
Share - WIP
This commit is contained in:
@ -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 {
|
||||
|
||||
6
apps/server/src/core/share/dto/create-share.dto.ts
Normal file
6
apps/server/src/core/share/dto/create-share.dto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class CreateShareDto {
|
||||
@IsString()
|
||||
pageId: string;
|
||||
}
|
||||
28
apps/server/src/core/share/dto/share.dto.ts
Normal file
28
apps/server/src/core/share/dto/share.dto.ts
Normal 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;
|
||||
}
|
||||
8
apps/server/src/core/share/dto/update-page.dto.ts
Normal file
8
apps/server/src/core/share/dto/update-page.dto.ts
Normal 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;
|
||||
}
|
||||
105
apps/server/src/core/share/share.controller.ts
Normal file
105
apps/server/src/core/share/share.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
10
apps/server/src/core/share/share.module.ts
Normal file
10
apps/server/src/core/share/share.module.ts
Normal 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 {}
|
||||
65
apps/server/src/core/share/share.service.ts
Normal file
65
apps/server/src/core/share/share.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
13
apps/server/src/database/types/db.d.ts
vendored
13
apps/server/src/database/types/db.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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'>>;
|
||||
|
||||
Reference in New Issue
Block a user