Compare commits

..

8 Commits

Author SHA1 Message Date
Philipinho 2fe2c0e6c1 don't zip sinple page exports 2026-03-29 02:02:53 +01:00
Philipinho 388572f689 fix extra paragraph in task list 2026-03-29 00:34:21 +00:00
Philipinho a8335475fd Merge branch 'main' into editor-972 2026-03-29 00:11:06 +00:00
Philipinho 6a90b318e5 cleanup two preceeding spaces in ordered lists item 2026-03-29 00:05:40 +00:00
Philipinho 8b4cc82e5a fix clipboardTextSerializer for single lines 2026-03-28 23:59:46 +00:00
Philipinho cda7cc9a57 return clipboardTextSerializer as markdown 2026-03-28 22:59:18 +00:00
Philipinho 59c5f25502 fix marked 2026-03-28 21:52:28 +00:00
Philipinho b9d58081b8 autojoiner 2026-03-28 21:52:04 +00:00
21 changed files with 98 additions and 536 deletions
@@ -733,5 +733,7 @@
"Publish": "Publish.",
"Security": "Security.",
"Enforce SSO": "Enforce SSO.",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password."
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password.",
"Uploading {{name}}": "Uploading {{name}}",
"Uploading file": "Uploading file"
}
@@ -75,7 +75,7 @@ function CommentMenu({
{isResolved ? t("Re-open comment") : t("Resolve comment")}
</Menu.Item>
) : (
<Tooltip label={upgradeLabel} position="left" withinPortal={false}>
<Tooltip label={upgradeLabel} position="left" withPortal={false}>
<Menu.Item disabled leftSection={<IconCircleCheck size={14} />}>
{t("Resolve comment")}
</Menu.Item>
@@ -10,7 +10,7 @@ import { useCallback } from "react";
export default function AttachmentView(props: NodeViewProps) {
const { t } = useTranslation();
const { editor, node, getPos, selected } = props;
const { url, name, size, mime, attachmentId, placeholder } = node.attrs;
const { url, name, size, mime, attachmentId } = node.attrs;
const { hovered, ref } = useHover();
const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf");
@@ -49,14 +49,14 @@ export default function AttachmentView(props: NodeViewProps) {
h={25}
>
<Group wrap="nowrap" gap="sm" style={{ minWidth: 0, flex: 1 }}>
{!url && placeholder ? (
<Loader size={20} style={{ flexShrink: 0 }} />
) : (
{url ? (
<IconPaperclip size={20} style={{ flexShrink: 0 }} />
) : (
<Loader size={20} style={{ flexShrink: 0 }} />
)}
<Text component="span" size="md" truncate="end" style={{ minWidth: 0 }}>
{!url && placeholder ? t("Uploading {{name}}", { name }) : name}
{url ? name : t("Uploading {{name}}", { name })}
</Text>
<Text component="span" size="sm" c="dimmed" style={{ flexShrink: 0 }}>
@@ -29,7 +29,7 @@ export default function AudioView(props: NodeViewProps) {
return (
<NodeViewWrapper data-drag-handle>
<div className={`${classes.audioWrapper} ${!safeSrc && placeholder ? classes.skeleton : ''}`}>
<div className={`${classes.audioWrapper} ${!safeSrc ? classes.skeleton : ''}`}>
{safeSrc && (
<audio
className={classes.audio}
@@ -49,7 +49,7 @@ export default function AudioView(props: NodeViewProps) {
<Loader size={20} pos="absolute" top={6} right={6} />
</Group>
)}
{!safeSrc && !previewSrc && placeholder && (
{!safeSrc && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md" h={54}>
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
@@ -59,9 +59,6 @@ export default function AudioView(props: NodeViewProps) {
</Text>
</Group>
)}
{!safeSrc && !previewSrc && !placeholder && (
<audio className={classes.audio} controls />
)}
</div>
</NodeViewWrapper>
);
@@ -33,7 +33,7 @@ export default function ImageView(props: NodeViewProps) {
className={clsx(
selected && "ProseMirror-selectednode",
classes.imageWrapper,
!src && placeholder && classes.skeleton,
!src && classes.skeleton,
alignClass,
)}
style={{
@@ -55,7 +55,7 @@ export default function ImageView(props: NodeViewProps) {
<Loader size={20} pos="absolute" bottom={6} right={6} />
</Group>
)}
{!src && !previewSrc && placeholder && (
{!src && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
@@ -73,17 +73,15 @@ export default function PdfView(props: NodeViewProps) {
if (!src || !safeSrc) {
return (
<NodeViewWrapper data-drag-handle>
<div className={`${classes.pdfWrapper} ${placeholder ? classes.skeleton : ''}`} style={{ height: placeholder ? 600 : undefined }}>
{placeholder && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
{placeholder?.name
? t("Uploading {{name}}", { name: placeholder.name })
: t("Uploading file")}
</Text>
</Group>
)}
<div className={`${classes.pdfWrapper} ${classes.skeleton}`} style={{ height: 600 }}>
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
{placeholder?.name
? t("Uploading {{name}}", { name: placeholder.name })
: t("Uploading file")}
</Text>
</Group>
</div>
</NodeViewWrapper>
);
@@ -33,7 +33,7 @@ export default function VideoView(props: NodeViewProps) {
className={clsx(
selected && "ProseMirror-selectednode",
classes.videoWrapper,
!src && placeholder && classes.skeleton,
!src && classes.skeleton,
alignClass,
)}
style={{
@@ -60,7 +60,7 @@ export default function VideoView(props: NodeViewProps) {
<Loader size={20} pos="absolute" top={6} right={6} />
</Group>
)}
{!src && !previewSrc && placeholder && (
{!src && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
@@ -70,9 +70,6 @@ export default function VideoView(props: NodeViewProps) {
</Text>
</Group>
)}
{!src && !previewSrc && !placeholder && (
<video className={classes.video} controls />
)}
</div>
</NodeViewWrapper>
);
@@ -253,8 +253,8 @@ export const mainExtensions = [
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 24,
minHeight: 16,
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createImageHandle,
@@ -266,8 +266,8 @@ export const mainExtensions = [
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 24,
minHeight: 16,
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createResizeHandle,
@@ -297,8 +297,8 @@ export const mainExtensions = [
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 24,
minHeight: 16,
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createResizeHandle,
@@ -310,8 +310,8 @@ export const mainExtensions = [
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 24,
minHeight: 16,
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createResizeHandle,
@@ -1,6 +1,6 @@
// adapted from: https://github.com/aguingand/tiptap-markdown/blob/main/src/extensions/tiptap/clipboard.js - MIT
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { DOMParser, DOMSerializer, Fragment, Slice } from "@tiptap/pm/model";
import { find } from "linkifyjs";
import { markdownToHtml, htmlToMarkdown } from "@docmost/editor-ext";
@@ -50,46 +50,26 @@ export const MarkdownClipboard = Extension.create({
}
const text = event.clipboardData.getData("text/plain");
const html = event.clipboardData.getData("text/html");
const vscode = event.clipboardData.getData("vscode-editor-data");
const vscodeData = vscode ? JSON.parse(vscode) : undefined;
const language = vscodeData?.mode;
const isVscodeMarkdown = language === "markdown";
const isPlainTextOnly = !html && !vscode && !!text;
if (!isVscodeMarkdown && !isPlainTextOnly) {
if (language !== "markdown") {
return false;
}
if (isPlainTextOnly) {
if ((view as any).input?.shiftKey || !this.options.transformPastedText) {
return false;
}
const link = find(text, {
defaultProtocol: "http",
}).find((item) => item.isLink && item.value === text);
if (link) {
return false;
}
}
const { tr } = view.state;
const { from, to } = view.state.selection;
const parsed = markdownToHtml(text.replace(/\n+$/, ""));
const html = markdownToHtml(text.replace(/\n+$/, ""));
const contentNodes = DOMParser.fromSchema(
this.editor.schema,
).parseSlice(elementFromString(parsed), {
).parseSlice(elementFromString(html), {
preserveWhitespace: true,
});
tr.replaceRange(from, to, contentNodes);
const insertEnd = tr.mapping.map(from, 1);
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(from, insertEnd - 2)), -1));
tr.setMeta('paste', true)
view.dispatch(tr);
return true;
@@ -125,6 +105,26 @@ export const MarkdownClipboard = Extension.create({
return slice;
},
clipboardTextParser: (text, context, plainText) => {
const link = find(text, {
defaultProtocol: "http",
}).find((item) => item.isLink && item.value === text);
if (plainText || !this.options.transformPastedText || link) {
// don't parse plaintext link to allow link paste handler to work
// pasting with shift key prevents formatting
return null;
}
const parsed = markdownToHtml(text.replace(/\n+$/, ""));
return DOMParser.fromSchema(this.editor.schema).parseSlice(
elementFromString(parsed),
{
preserveWhitespace: true,
context,
},
);
},
},
}),
];
-3
View File
@@ -44,7 +44,6 @@
"@langchain/core": "1.1.34",
"@langchain/textsplitters": "1.0.1",
"@modelcontextprotocol/sdk": "^1.27.1",
"@nest-lab/throttler-storage-redis": "^1.2.0",
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.1.0",
@@ -59,7 +58,6 @@
"@nestjs/platform-socket.io": "^11.1.17",
"@nestjs/schedule": "^6.1.1",
"@nestjs/terminus": "^11.1.1",
"@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.1.17",
"@node-saml/passport-saml": "^5.1.0",
"@react-email/components": "1.0.10",
@@ -75,7 +73,6 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"cookie": "^1.1.1",
"fastify-ip": "^2.0.0",
"fs-extra": "^11.3.4",
"happy-dom": "20.8.9",
"ioredis": "^5.10.1",
-2
View File
@@ -26,7 +26,6 @@ import KeyvRedis from '@keyv/redis';
import { LoggerModule } from './common/logger/logger.module';
import { ClsModule } from 'nestjs-cls';
import { NoopAuditModule } from './integrations/audit/audit.module';
import { ThrottleModule } from './integrations/throttle/throttle.module';
const enterpriseModules = [];
try {
@@ -84,7 +83,6 @@ try {
EventEmitterModule.forRoot(),
SecurityModule,
TelemetryModule,
ThrottleModule,
...enterpriseModules,
],
controllers: [AppController],
+14 -6
View File
@@ -50,12 +50,20 @@ export function createPinoConfig(): Params {
},
},
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
ip: req.ip || req.remoteAddress,
userAgent: req.headers?.['user-agent'],
}),
req: (req) => {
const forwardedFor = req.headers?.['x-forwarded-for'];
const ip =
req.headers?.['cf-connecting-ip'] ||
(typeof forwardedFor === 'string' ? forwardedFor.split(',')[0]?.trim() : undefined) ||
req.remoteAddress;
return {
method: req.method,
url: req.url,
ip,
userAgent: req.headers?.['user-agent'],
};
},
res: (res) => ({
statusCode: res.statusCode,
}),
@@ -18,8 +18,7 @@ export class AuditContextMiddleware implements NestMiddleware {
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
const workspaceId = (req as any).workspaceId ?? null;
const ipAddress = (req as any).ip ?? (req as any).socket?.remoteAddress ?? null;
const ipAddress = this.extractIpAddress(req);
const userAgent =
(req.headers['user-agent'] as string) ?? null;
@@ -36,4 +35,21 @@ export class AuditContextMiddleware implements NestMiddleware {
next();
}
private extractIpAddress(req: FastifyRequest['raw']): string | null {
const xForwardedFor = req.headers['x-forwarded-for'];
if (xForwardedFor) {
const ips = Array.isArray(xForwardedFor)
? xForwardedFor[0]
: xForwardedFor.split(',')[0];
return ips?.trim() ?? null;
}
const xRealIp = req.headers['x-real-ip'];
if (xRealIp) {
return Array.isArray(xRealIp) ? xRealIp[0] : xRealIp;
}
return (req as any).socket?.remoteAddress ?? null;
}
}
@@ -10,7 +10,6 @@ import {
UseGuards,
Logger,
} from '@nestjs/common';
import { SkipThrottle, ThrottlerGuard } from '@nestjs/throttler';
import { LoginDto } from './dto/login.dto';
import { AuthService } from './services/auth.service';
import { SessionService } from '../session/session.service';
@@ -34,7 +33,6 @@ import {
IAuditService,
} from '../../integrations/audit/audit.service';
@UseGuards(ThrottlerGuard)
@Controller('auth')
export class AuthController {
private readonly logger = new Logger(AuthController.name);
@@ -113,7 +111,6 @@ export class AuthController {
return workspace;
}
@SkipThrottle()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('change-password')
@@ -176,7 +173,6 @@ export class AuthController {
return this.authService.verifyUserToken(verifyUserTokenDto, workspace.id);
}
@SkipThrottle()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('collab-token')
@@ -187,7 +183,6 @@ export class AuthController {
return this.authService.getCollabToken(user, workspace.id);
}
@SkipThrottle()
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('logout')
@@ -1,333 +0,0 @@
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createIndex('idx_group_users_user_id')
.ifNotExists()
.on('group_users')
.column('user_id')
.execute();
await db.schema
.createIndex('idx_space_members_user_id')
.ifNotExists()
.on('space_members')
.column('user_id')
.execute();
await db.schema
.createIndex('idx_space_members_group_id')
.ifNotExists()
.on('space_members')
.column('group_id')
.execute();
// Page tree
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_space_parent_position
ON pages (space_id, parent_page_id, position COLLATE "C")
WHERE deleted_at IS NULL
`.execute(db);
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_parent_page_id
ON pages (parent_page_id)
WHERE deleted_at IS NULL
`.execute(db);
// Recent pages query
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_space_updated
ON pages (space_id, updated_at DESC)
WHERE deleted_at IS NULL
`.execute(db);
// Trash view
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_space_deleted
ON pages (space_id, deleted_at DESC)
WHERE deleted_at IS NOT NULL
`.execute(db);
await sql`
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_hostname_lower
ON workspaces (LOWER(hostname))
`.execute(db);
await db.schema
.createIndex('idx_workspaces_created_at')
.ifNotExists()
.on('workspaces')
.column('created_at')
.execute();
await db.schema
.createIndex('idx_users_workspace_deleted')
.ifNotExists()
.on('users')
.columns(['workspace_id', 'deleted_at'])
.execute();
await sql`
CREATE UNIQUE INDEX IF NOT EXISTS idx_spaces_slug_lower_workspace
ON spaces (LOWER(slug), workspace_id)
`.execute(db);
await db.schema
.createIndex('idx_spaces_workspace_id')
.ifNotExists()
.on('spaces')
.column('workspace_id')
.execute();
await sql`
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_lower_workspace
ON groups (LOWER(name), workspace_id)
`.execute(db);
await db.schema
.createIndex('idx_groups_workspace_id')
.ifNotExists()
.on('groups')
.column('workspace_id')
.execute();
await db.schema
.createIndex('idx_shares_page_id')
.ifNotExists()
.on('shares')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_attachments_page_id')
.ifNotExists()
.on('attachments')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_attachments_space_id')
.ifNotExists()
.on('attachments')
.column('space_id')
.execute();
await db.schema
.createIndex('idx_comments_page_id')
.ifNotExists()
.on('comments')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_comments_parent_comment_id')
.ifNotExists()
.on('comments')
.column('parent_comment_id')
.execute();
await sql`
CREATE INDEX IF NOT EXISTS idx_page_history_page_created
ON page_history (page_id, created_at DESC)
`.execute(db);
await db.schema
.createIndex('idx_attachments_workspace_id')
.ifNotExists()
.on('attachments')
.column('workspace_id')
.execute();
await db.schema
.createIndex('idx_backlinks_target_page_id')
.ifNotExists()
.on('backlinks')
.column('target_page_id')
.execute();
await db.schema
.createIndex('idx_pages_workspace_id')
.ifNotExists()
.on('pages')
.column('workspace_id')
.execute();
await db.schema
.createIndex('idx_pages_creator_id')
.ifNotExists()
.on('pages')
.column('creator_id')
.execute();
// Notifications: FK cascade from pages, spaces, comments
await db.schema
.createIndex('idx_notifications_page_id')
.ifNotExists()
.on('notifications')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_notifications_space_id')
.ifNotExists()
.on('notifications')
.column('space_id')
.execute();
await db.schema
.createIndex('idx_notifications_comment_id')
.ifNotExists()
.on('notifications')
.column('comment_id')
.execute();
// Watchers: cleanup queries and FK cascade
await db.schema
.createIndex('idx_watchers_user_workspace')
.ifNotExists()
.on('watchers')
.columns(['user_id', 'workspace_id'])
.execute();
await db.schema
.createIndex('idx_watchers_space_id')
.ifNotExists()
.on('watchers')
.column('space_id')
.execute();
// Auth providers: all queries filter by workspaceId
await db.schema
.createIndex('idx_auth_providers_workspace_id')
.ifNotExists()
.on('auth_providers')
.column('workspace_id')
.execute();
// Auth accounts: SSO login lookup by provider user
await db.schema
.createIndex('idx_auth_accounts_provider_user_id')
.ifNotExists()
.on('auth_accounts')
.columns(['provider_user_id', 'auth_provider_id'])
.execute();
// Workspace invitations: listing and SSO lookup
await db.schema
.createIndex('idx_workspace_invitations_workspace_id')
.ifNotExists()
.on('workspace_invitations')
.column('workspace_id')
.execute();
// API keys: query and FK cascade
await db.schema
.createIndex('idx_api_keys_workspace_id')
.ifNotExists()
.on('api_keys')
.column('workspace_id')
.execute();
// User sessions: delete queries and FK cascade on all session states
await db.schema
.createIndex('idx_user_sessions_user_workspace')
.ifNotExists()
.on('user_sessions')
.columns(['user_id', 'workspace_id'])
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropIndex('idx_group_users_user_id').ifExists().execute();
await db.schema.dropIndex('idx_space_members_user_id').ifExists().execute();
await db.schema.dropIndex('idx_space_members_group_id').ifExists().execute();
await db.schema
.dropIndex('idx_pages_space_parent_position')
.ifExists()
.execute();
await db.schema.dropIndex('idx_pages_parent_page_id').ifExists().execute();
await db.schema.dropIndex('idx_pages_space_updated').ifExists().execute();
await db.schema.dropIndex('idx_pages_space_deleted').ifExists().execute();
await db.schema
.dropIndex('idx_workspaces_hostname_lower')
.ifExists()
.execute();
await db.schema.dropIndex('idx_workspaces_created_at').ifExists().execute();
await db.schema
.dropIndex('idx_users_workspace_deleted')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_spaces_slug_lower_workspace')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_spaces_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_groups_name_lower_workspace')
.ifExists()
.execute();
await db.schema.dropIndex('idx_groups_workspace_id').ifExists().execute();
await db.schema.dropIndex('idx_shares_page_id').ifExists().execute();
await db.schema.dropIndex('idx_attachments_page_id').ifExists().execute();
await db.schema.dropIndex('idx_attachments_space_id').ifExists().execute();
await db.schema.dropIndex('idx_comments_page_id').ifExists().execute();
await db.schema
.dropIndex('idx_comments_parent_comment_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_page_history_page_created')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_attachments_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_backlinks_target_page_id')
.ifExists()
.execute();
await db.schema.dropIndex('idx_pages_workspace_id').ifExists().execute();
await db.schema.dropIndex('idx_pages_creator_id').ifExists().execute();
await db.schema
.dropIndex('idx_notifications_page_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_notifications_space_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_notifications_comment_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_watchers_user_workspace')
.ifExists()
.execute();
await db.schema.dropIndex('idx_watchers_space_id').ifExists().execute();
await db.schema
.dropIndex('idx_auth_providers_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_auth_accounts_provider_user_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_workspace_invitations_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_api_keys_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_user_sessions_user_workspace')
.ifExists()
.execute();
}
@@ -193,8 +193,6 @@ export class ImportAttachmentService {
// Build a map from resolved archive path → real filename from Confluence
// metadata. Confluence Server archives often store files under numeric IDs
// (e.g. "attachments/65601/65602") instead of the original filename.
// Also register aliases so HTML references using the original filename
// (e.g. "attachments/pageId/original.mp3") resolve to the numeric path.
const pageDir = path.dirname(pageRelativePath);
const attachmentNameByRelPath = new Map<string, string>();
for (const attachment of pageAttachments) {
@@ -205,13 +203,6 @@ export class ImportAttachmentService {
);
if (relPath && attachment.fileName) {
attachmentNameByRelPath.set(relPath, attachment.fileName);
const dir = path.posix.dirname(relPath);
const aliasKey = `${dir}/${attachment.fileName}`;
if (!attachmentCandidates.has(aliasKey)) {
attachmentCandidates.set(aliasKey, attachmentCandidates.get(relPath)!);
attachmentNameByRelPath.set(aliasKey, attachment.fileName);
}
}
}
@@ -571,31 +562,18 @@ export class ImportAttachmentService {
continue;
}
// Resolve the metadata href to the actual archive path
const resolvedHref = resolveRelativeAttachmentPath(
href,
pageDir,
attachmentCandidates,
);
if (!resolvedHref) continue;
// Check if already processed (was referenced in HTML)
if (processed.has(href)) {
continue;
}
// Check if already processed (was referenced in HTML).
// Inline elements may have been processed under an alias key (original
// filename) rather than the numeric archive path, so also check whether
// the underlying absolute file path has already been uploaded.
const absPath = attachmentCandidates.get(resolvedHref);
const alreadyProcessed =
processed.has(resolvedHref) ||
(absPath &&
Array.from(processed.values()).some(
(entry) => entry.abs === absPath,
));
if (alreadyProcessed) {
// Skip if the file doesn't exist
if (!attachmentCandidates.has(href)) {
continue;
}
// This attachment was in the list but not referenced in HTML - add it
const { attachmentId, apiFilePath, abs } = processFile(resolvedHref);
const { attachmentId, apiFilePath, abs } = processFile(href);
const mime = mimeType || getMimeType(abs);
// Add as attachment node at the end
@@ -1,35 +0,0 @@
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import { EnvironmentService } from '../environment/environment.service';
import { EnvironmentModule } from '../environment/environment.module';
import { parseRedisUrl } from '../../common/helpers';
import Redis from 'ioredis';
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [EnvironmentModule],
useFactory: (environmentService: EnvironmentService) => {
const redisConfig = parseRedisUrl(environmentService.getRedisUrl());
return {
throttlers: [{ name: 'auth', ttl: 60_000, limit: 10 }],
errorMessage: 'Too many requests',
storage: new ThrottlerStorageRedisService(
new Redis({
host: redisConfig.host,
port: redisConfig.port,
password: redisConfig.password,
db: redisConfig.db,
family: redisConfig.family,
keyPrefix: 'throttle:',
}),
),
};
},
inject: [EnvironmentService],
}),
],
})
export class ThrottleModule {}
-2
View File
@@ -10,7 +10,6 @@ import { TransformHttpResponseInterceptor } from './common/interceptors/http-res
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
import fastifyMultipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import fastifyIp from 'fastify-ip';
import { InternalLogFilter } from './common/logger/internal-log-filter';
async function bootstrap() {
@@ -46,7 +45,6 @@ async function bootstrap() {
app.useWebSocketAdapter(redisIoAdapter);
await app.register(fastifyIp);
await app.register(fastifyMultipart);
await app.register(fastifyCookie);
+1 -3
View File
@@ -9,7 +9,5 @@
"main": "dist/index.js",
"module": "./src/index.ts",
"types": "dist/index.d.ts",
"dependencies": {
"marked": "17.0.5"
}
"dependencies": {}
}
+1 -53
View File
@@ -493,9 +493,6 @@ importers:
'@modelcontextprotocol/sdk':
specifier: ^1.27.1
version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6)
'@nest-lab/throttler-storage-redis':
specifier: ^1.2.0
version: 1.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)
'@nestjs-labs/nestjs-ioredis':
specifier: ^11.0.4
version: 11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1)
@@ -538,9 +535,6 @@ importers:
'@nestjs/terminus':
specifier: ^11.1.1
version: 11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/throttler':
specifier: ^6.5.0
version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)
'@nestjs/websockets':
specifier: ^11.1.17
version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -586,9 +580,6 @@ importers:
cookie:
specifier: ^1.1.1
version: 1.1.1
fastify-ip:
specifier: ^2.0.0
version: 2.0.0
fs-extra:
specifier: ^11.3.4
version: 11.3.4
@@ -810,11 +801,7 @@ importers:
specifier: ^8.57.1
version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)
packages/editor-ext:
dependencies:
marked:
specifier: 17.0.5
version: 17.0.5
packages/editor-ext: {}
packages:
@@ -2934,15 +2921,6 @@ packages:
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
'@nest-lab/throttler-storage-redis@1.2.0':
resolution: {integrity: sha512-tMkUyo68NCKTR+zILk+EC35SMYBtDPZY2mCj7ZaCietWGVTnuP4zwq9ERYfvU6kJv6h8teNZrC6MJCmY6/dljw==}
peerDependencies:
'@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
'@nestjs/throttler': '>=6.0.0'
ioredis: '>=5.0.0'
reflect-metadata: ^0.2.1
'@nestjs-labs/nestjs-ioredis@11.0.4':
resolution: {integrity: sha512-4jPNOrxDiwNMIN5OLmsMWhA782kxv/ZBxkySX9l8n6sr55acHX/BciaFsOXVa/ILsm+Y7893y98/6WNhmEoiNQ==}
engines: {node: '>=16'}
@@ -3145,13 +3123,6 @@ packages:
'@nestjs/platform-express':
optional: true
'@nestjs/throttler@6.5.0':
resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==}
peerDependencies:
'@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
reflect-metadata: ^0.1.13 || ^0.2.0
'@nestjs/websockets@11.1.17':
resolution: {integrity: sha512-YbwQ0QfVj0lxkKQhdIIgk14ZSVWDqGk1J8nNSN6SLjf36sVv58Ma5ro+dtQua8wj3l2Ub7JJCVFixEhKtYc/rQ==}
peerDependencies:
@@ -7041,10 +7012,6 @@ packages:
resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==}
hasBin: true
fastify-ip@2.0.0:
resolution: {integrity: sha512-7mQyAc7sapawpiriEFoJyQIs41nNIO42UCzgMKrjNGsIegnevj2VhOlXLLTa+q7cxXfJ5fDGmOAdQpaIgA9ObA==}
engines: {node: '>=20.x'}
fastify-plugin@5.0.1:
resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==}
@@ -13492,15 +13459,6 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)
ioredis: 5.10.1
reflect-metadata: 0.2.2
tslib: 2.8.1
'@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(ioredis@5.10.1)':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -13681,12 +13639,6 @@ snapshots:
'@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
tslib: 2.8.1
'@nestjs/throttler@6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
reflect-metadata: 0.2.2
'@nestjs/websockets@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -18116,10 +18068,6 @@ snapshots:
path-expression-matcher: 1.2.0
strnum: 2.2.1
fastify-ip@2.0.0:
dependencies:
fastify-plugin: 5.1.0
fastify-plugin@5.0.1: {}
fastify-plugin@5.1.0: {}