Compare commits

..

9 Commits

Author SHA1 Message Date
Peter Tripp c9fa6e20b3 Add alias: /toc and /ol (#2161) 2026-05-08 01:20:27 +01:00
Philipinho ec51ca7815 fix request ip 2026-05-07 22:09:32 +01:00
Philipinho 2b63137217 mail 2026-05-07 18:13:24 +01:00
Philipinho 3227bc6059 fix: a11y 2026-05-04 23:04:26 +01:00
Philip Okugbe 73dc62bca3 update react-email (#2149) 2026-05-04 22:26:53 +01:00
Philipinho 3c74bb3dee update package 2026-05-04 22:09:19 +01:00
Philip Okugbe dbe6c2d6ba feat: A11y fixes (#2148) 2026-05-04 21:21:37 +01:00
Sarthak Chaturvedi fe18f22dc6 fix: prevent code block deletion when adding inline comments in read mode (#2146) 2026-05-04 21:14:21 +01:00
Philipinho fcef0c6b96 fix: S3 2026-05-04 20:57:35 +01:00
33 changed files with 322 additions and 622 deletions
@@ -870,6 +870,8 @@
"Previous 7 days": "Previous 7 days", "Previous 7 days": "Previous 7 days",
"Previous 30 days": "Previous 30 days", "Previous 30 days": "Previous 30 days",
"Search chats...": "Search chats...", "Search chats...": "Search chats...",
"Search chats": "Search chats",
"Ask anything... Use @ to mention pages": "Ask anything... Use @ to mention pages",
"Start a new chat to see it here.": "Start a new chat to see it here.", "Start a new chat to see it here.": "Start a new chat to see it here.",
"Summarize this page": "Summarize this page", "Summarize this page": "Summarize this page",
"Toggle AI Chat": "Toggle AI Chat", "Toggle AI Chat": "Toggle AI Chat",
@@ -137,7 +137,8 @@ export default function AiChatSidebar() {
<TextInput <TextInput
className={classes.searchInput} className={classes.searchInput}
placeholder="Search chats..." placeholder={t("Search chats...")}
aria-label={t("Search chats")}
leftSection={<IconSearch size={14} />} leftSection={<IconSearch size={14} />}
size="xs" size="xs"
value={search} value={search}
@@ -178,6 +178,7 @@ export default function AsideChatPanel() {
href="/ai" href="/ai"
variant="subtle" variant="subtle"
color="dark" color="dark"
aria-label={t("New chat")}
onClick={handleNewChat} onClick={handleNewChat}
> >
<IconPlus size={20} stroke={1.75} /> <IconPlus size={20} stroke={1.75} />
@@ -185,13 +186,23 @@ export default function AsideChatPanel() {
</Tooltip> </Tooltip>
<Tooltip label={t("Open full page")} openDelay={250}> <Tooltip label={t("Open full page")} openDelay={250}>
<ActionIcon variant="subtle" color="dark" onClick={handleExpand}> <ActionIcon
variant="subtle"
color="dark"
aria-label={t("Open full page")}
onClick={handleExpand}
>
<IconArrowsDiagonal size={18} stroke={1.5} /> <IconArrowsDiagonal size={18} stroke={1.5} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("Close")} openDelay={250}> <Tooltip label={t("Close")} openDelay={250}>
<ActionIcon variant="subtle" color="dark" onClick={handleClose}> <ActionIcon
variant="subtle"
color="dark"
aria-label={t("Close")}
onClick={handleClose}
>
<IconX size={20} stroke={1.75} /> <IconX size={20} stroke={1.75} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@@ -225,6 +225,10 @@ export default function ChatInput({
}), }),
], ],
editorProps: { editorProps: {
attributes: {
"aria-label": placeholder || t("Ask anything... Use @ to mention pages"),
"aria-multiline": "true",
},
handleDOMEvents: { handleDOMEvents: {
keydown: (_view, event) => { keydown: (_view, event) => {
if ( if (
@@ -275,6 +279,8 @@ export default function ChatInput({
type="file" type="file"
accept={ACCEPTED_FILE_TYPES} accept={ACCEPTED_FILE_TYPES}
multiple multiple
aria-label={t("Add files")}
tabIndex={-1}
style={{ display: "none" }} style={{ display: "none" }}
onChange={(e) => handleFileSelect(e.target.files)} onChange={(e) => handleFileSelect(e.target.files)}
/> />
@@ -31,7 +31,16 @@ export default function ChatToolGroup({ toolCalls, isStreaming }: Props) {
<div className={classes.toolGroup}> <div className={classes.toolGroup}>
<div <div
className={classes.toolGroupHeader} className={classes.toolGroupHeader}
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded((prev) => !prev)} onClick={() => setExpanded((prev) => !prev)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setExpanded((prev) => !prev);
}
}}
> >
{activeLabel ? ( {activeLabel ? (
<IconLoader2 size={12} className={classes.processingSpinner} /> <IconLoader2 size={12} className={classes.processingSpinner} />
@@ -98,7 +98,7 @@
font-weight: 600; font-weight: 600;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); color: var(--mantine-color-dimmed);
margin-bottom: var(--mantine-spacing-xs); margin-bottom: var(--mantine-spacing-xs);
} }
@@ -125,7 +125,7 @@
.suggestionsLabel { .suggestionsLabel {
font-size: var(--mantine-font-size-xs); font-size: var(--mantine-font-size-xs);
font-weight: 500; font-weight: 500;
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); color: var(--mantine-color-dimmed);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
margin-bottom: var(--mantine-spacing-sm); margin-bottom: var(--mantine-spacing-sm);
@@ -43,7 +43,7 @@
margin-top: 6px; margin-top: 6px;
text-align: center; text-align: center;
font-size: var(--mantine-font-size-xs); font-size: var(--mantine-font-size-xs);
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); color: var(--mantine-color-dimmed);
} }
.attachmentChips { .attachmentChips {
@@ -36,7 +36,7 @@
padding: 4px var(--mantine-spacing-xs); padding: 4px var(--mantine-spacing-xs);
font-size: var(--mantine-font-size-xs); font-size: var(--mantine-font-size-xs);
font-weight: 600; font-weight: 600;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); color: var(--mantine-color-dimmed);
user-select: none; user-select: none;
} }
@@ -104,7 +104,7 @@
.chatItemDate { .chatItemDate {
font-size: var(--mantine-font-size-xs); font-size: var(--mantine-font-size-xs);
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); color: var(--mantine-color-dimmed);
white-space: nowrap; white-space: nowrap;
transition: opacity 150ms; transition: opacity 150ms;
} }
@@ -132,7 +132,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
{ {
title: "Numbered list", title: "Numbered list",
description: "Create a list with numbering.", description: "Create a list with numbering.",
searchTerms: ["numbered", "ordered", "list"], searchTerms: ["numbered", "ordered", "list", "ol"],
icon: IconListNumbers, icon: IconListNumbers,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run(); editor.chain().focus().deleteRange(range).toggleOrderedList().run();
@@ -471,7 +471,14 @@ const CommandGroups: SlashMenuGroupedItemsType = {
{ {
title: "Subpages (Child pages)", title: "Subpages (Child pages)",
description: "List all subpages of the current page", description: "List all subpages of the current page",
searchTerms: ["subpages", "child", "children", "nested", "hierarchy"], searchTerms: [
"subpages",
"child",
"children",
"nested",
"hierarchy",
"toc",
],
icon: IconSitemap, icon: IconSitemap,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).insertSubpages().run(); editor.chain().focus().deleteRange(range).insertSubpages().run();
@@ -16,7 +16,7 @@
.subtitle { .subtitle {
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); color: var(--mantine-color-dimmed);
text-align: center; text-align: center;
margin-top: 6px; margin-top: 6px;
margin-bottom: var(--mantine-spacing-lg); margin-bottom: var(--mantine-spacing-lg);
+1 -3
View File
@@ -63,8 +63,6 @@
"@nestjs/throttler": "^6.5.0", "@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.1.19", "@nestjs/websockets": "^11.1.19",
"@node-saml/passport-saml": "^5.1.0", "@node-saml/passport-saml": "^5.1.0",
"@react-email/components": "1.0.10",
"@react-email/render": "2.0.4",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"ai": "^6.0.134", "ai": "^6.0.134",
"ai-sdk-ollama": "^3.8.1", "ai-sdk-ollama": "^3.8.1",
@@ -108,6 +106,7 @@
"postgres": "^3.4.8", "postgres": "^3.4.8",
"postmark": "^4.0.7", "postmark": "^4.0.7",
"react": "^18.3.1", "react": "^18.3.1",
"react-email": "6.0.8",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
@@ -146,7 +145,6 @@
"jest": "^30.3.0", "jest": "^30.3.0",
"kysely-codegen": "^0.20.0", "kysely-codegen": "^0.20.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"react-email": "5.2.10",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.2.2", "supertest": "^7.2.2",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
+3 -3
View File
@@ -62,14 +62,14 @@ function applyMarkToYFragment(
) { ) {
let pos = 0; let pos = 0;
const processItem = (item: any): boolean => { const processItem = (item: any, parentNodeName?: string): boolean => {
if (pos >= to) return false; if (pos >= to) return false;
if (item instanceof Y.XmlText) { if (item instanceof Y.XmlText) {
const textLength = item.length; const textLength = item.length;
const itemEnd = pos + textLength; const itemEnd = pos + textLength;
if (itemEnd > from && pos < to) { if (itemEnd > from && pos < to && parentNodeName !== 'codeBlock') {
const formatFrom = Math.max(0, from - pos); const formatFrom = Math.max(0, from - pos);
const formatTo = Math.min(textLength, to - pos); const formatTo = Math.min(textLength, to - pos);
const formatLength = formatTo - formatFrom; const formatLength = formatTo - formatFrom;
@@ -82,7 +82,7 @@ function applyMarkToYFragment(
} else if (item instanceof Y.XmlElement) { } else if (item instanceof Y.XmlElement) {
pos++; // Opening tag pos++; // Opening tag
for (let i = 0; i < item.length; i++) { for (let i = 0; i < item.length; i++) {
if (!processItem(item.get(i))) return false; if (!processItem(item.get(i), item.nodeName)) return false;
} }
pos++; // Closing tag pos++; // Closing tag
} }
@@ -112,7 +112,10 @@ export class EnvironmentService {
} }
getAwsS3ForcePathStyle(): boolean { getAwsS3ForcePathStyle(): boolean {
return this.configService.get<boolean>('AWS_S3_FORCE_PATH_STYLE'); const forcePathStyle = this.configService
.get<string>('AWS_S3_FORCE_PATH_STYLE', 'false')
.toLowerCase();
return forcePathStyle === 'true';
} }
getAwsS3Url(): string { getAwsS3Url(): string {
@@ -131,6 +134,17 @@ export class EnvironmentService {
return this.configService.get<string>('MAIL_FROM_NAME', 'Docmost'); return this.configService.get<string>('MAIL_FROM_NAME', 'Docmost');
} }
getMailBlockedRecipientDomains(): string[] {
const raw = this.configService.get<string>(
'MAIL_BLOCKED_RECIPIENT_DOMAINS',
'',
);
return raw
.split(',')
.map((d) => d.trim().toLowerCase())
.filter(Boolean);
}
getSmtpHost(): string { getSmtpHost(): string {
return this.configService.get<string>('SMTP_HOST'); return this.configService.get<string>('SMTP_HOST');
} }
@@ -6,7 +6,7 @@ import { EnvironmentService } from '../environment/environment.service';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { QueueName, QueueJob } from '../queue/constants'; import { QueueName, QueueJob } from '../queue/constants';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { render } from '@react-email/render'; import { render } from 'react-email';
@Injectable() @Injectable()
export class MailService { export class MailService {
@@ -17,6 +17,10 @@ export class MailService {
) {} ) {}
async sendEmail(message: MailMessage): Promise<void> { async sendEmail(message: MailMessage): Promise<void> {
if (this.isRecipientBlocked(message.to)) {
return;
}
if (message.template) { if (message.template) {
// in case this method is used directly. we do not send the tsx template from queue // in case this method is used directly. we do not send the tsx template from queue
message.html = await render(message.template, { message.html = await render(message.template, {
@@ -35,6 +39,10 @@ export class MailService {
} }
async sendToQueue(message: MailMessage): Promise<void> { async sendToQueue(message: MailMessage): Promise<void> {
if (this.isRecipientBlocked(message.to)) {
return;
}
if (message.template) { if (message.template) {
// transform the React object because it gets lost when sent via the queue // transform the React object because it gets lost when sent via the queue
message.html = await render(message.template, { message.html = await render(message.template, {
@@ -47,4 +55,11 @@ export class MailService {
} }
await this.emailQueue.add(QueueJob.SEND_EMAIL, message); await this.emailQueue.add(QueueJob.SEND_EMAIL, message);
} }
private isRecipientBlocked(to: string): boolean {
const blocked = this.environmentService.getMailBlockedRecipientDomains();
if (blocked.length === 0) return false;
const domain = to?.split('@')[1]?.toLowerCase();
return !!domain && blocked.includes(domain);
}
} }
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { MailBody } from '../partials/partials'; import { MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Button, Link, Section, Text } from '@react-email/components'; import { Button, Link, Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { button, content, paragraph } from '../css/styles'; import { button, content, paragraph } from '../css/styles';
import { MailBody } from '../partials/partials'; import { MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { MailBody } from '../partials/partials'; import { MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Link, Section, Text } from '@react-email/components'; import { Link, Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, link, paragraph } from '../css/styles'; import { content, link, paragraph } from '../css/styles';
import { getGreetingName, MailBody } from '../partials/partials'; import { getGreetingName, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Link, Section, Text } from '@react-email/components'; import { Link, Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, link, paragraph } from '../css/styles'; import { content, link, paragraph } from '../css/styles';
import { EmailButton, getGreetingName, MailBody } from '../partials/partials'; import { EmailButton, getGreetingName, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -1,4 +1,4 @@
import { Section, Text } from '@react-email/components'; import { Section, Text } from 'react-email';
import * as React from 'react'; import * as React from 'react';
import { content, paragraph } from '../css/styles'; import { content, paragraph } from '../css/styles';
import { EmailButton, MailBody } from '../partials/partials'; import { EmailButton, MailBody } from '../partials/partials';
@@ -7,7 +7,7 @@ import {
Row, Row,
Section, Section,
Text, Text,
} from '@react-email/components'; } from 'react-email';
import * as React from 'react'; import * as React from 'react';
interface MailBodyProps { interface MailBodyProps {
+8
View File
@@ -50,6 +50,14 @@ async function bootstrap() {
await app.register(fastifyMultipart); await app.register(fastifyMultipart);
await app.register(fastifyCookie); await app.register(fastifyCookie);
app
.getHttpAdapter()
.getInstance()
.addHook('onRequest', (request, _reply, done) => {
(request.raw as any).ip = request.ip;
done();
});
app app
.getHttpAdapter() .getHttpAdapter()
.getInstance() .getInstance()
+215 -586
View File
File diff suppressed because it is too large Load Diff