This commit is contained in:
Philipinho
2026-04-12 17:28:57 +01:00
parent 5a071e5e06
commit 0f75f1197e
3 changed files with 63 additions and 162 deletions
@@ -2,14 +2,15 @@ import { Group, List, Stack, Table, Text, ThemeIcon } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
const enterpriseFeatures = [
"SSO (SAML, OIDC, LDAP)",
"AI Integration (Search & Assistant)",
"Page-level Permissions",
"Audit Logs",
"API Keys",
"AI Integration (Chat, Search & Assistant)",
"MCP Support",
"SSO (SAML, OIDC, LDAP)",
"Multi-factor Authentication (2FA)",
"Page-level Permissions",
"Page verification & approval workflow",
"Audit Logs",
"Enterprise Controls",
"API Keys",
"Advanced Search Engine Support",
"Full-text Search in Attachments (PDF, DOCX)",
"Resolve Comments",
@@ -68,11 +69,31 @@ export default function OssDetails() {
</List>
<Text size="sm" c="dimmed">
Get an enterprise trial key at <a href="https://customers.docmost.com/" target="_blank" rel="noopener noreferrer">customers.docmost.com</a>.
Get an enterprise trial key at{" "}
<a
href="https://customers.docmost.com/"
target="_blank"
rel="noopener noreferrer"
>
customers.docmost.com
</a>
.
</Text>
<Text size="sm" c="dimmed">
Visit <a href="https://docmost.com/pricing" target="_blank" rel="noopener noreferrer">docmost.com/pricing</a> to purchase an enterprise license.
Visit{" "}
<a
href="https://docmost.com/pricing"
target="_blank"
rel="noopener noreferrer"
>
docmost.com/pricing
</a>{" "}
to purchase an enterprise license.
</Text>
<Text size="sm" c="dimmed">
For inquiries, contact{" "}
<a href="mailto:sales@docmost.com">sales@docmost.com</a>
</Text>
</Stack>
</Stack>
@@ -8,7 +8,6 @@ import { WsGateway } from '../../ws/ws.gateway';
import { MailService } from '../../integrations/mail/mail.service';
import { NotificationTab, NotificationType, NotificationTypeToSettingKey } from './notification.constants';
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
@Injectable()
export class NotificationService {
@@ -17,108 +16,11 @@ export class NotificationService {
constructor(
private readonly notificationRepo: NotificationRepo,
private readonly pagePermissionRepo: PagePermissionRepo,
private readonly spaceMemberRepo: SpaceMemberRepo,
private readonly wsGateway: WsGateway,
private readonly mailService: MailService,
@InjectKysely() private readonly db: KyselyDB,
) {}
/**
* Returns the subset of `ids` pointing to notifications the user can
* currently see. Enforces the same dual gate as `findByUserId`:
* 1. `spaceId IS NULL` or user is a current member of `spaceId`.
* 2. `pageId IS NULL` or user has page-level access to `pageId`.
*
* Returning an empty array when `ids` is empty is a shortcut callers use to
* make the mark/count paths no-ops.
*/
private async filterAccessibleNotificationIds(
ids: string[],
userId: string,
): Promise<string[]> {
if (ids.length === 0) return [];
const rows = await this.db
.selectFrom('notifications')
.select(['id', 'pageId'])
.where('id', 'in', ids)
.where('userId', '=', userId)
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb(
'spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
),
]),
)
.execute();
if (rows.length === 0) return [];
const pageIds = rows
.map((r) => r.pageId)
.filter((p): p is string => !!p);
if (pageIds.length === 0) {
return rows.map((r) => r.id);
}
const accessiblePageIds =
await this.pagePermissionRepo.filterAccessiblePageIds({
pageIds,
userId,
});
const accessibleSet = new Set(accessiblePageIds);
return rows
.filter((r) => !r.pageId || accessibleSet.has(r.pageId))
.map((r) => r.id);
}
private async listUnreadAccessibleNotificationIds(
userId: string,
): Promise<string[]> {
const rows = await this.db
.selectFrom('notifications')
.select(['id', 'pageId'])
.where('userId', '=', userId)
.where('readAt', 'is', null)
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb(
'spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
),
]),
)
.execute();
if (rows.length === 0) return [];
const pageIds = rows
.map((r) => r.pageId)
.filter((p): p is string => !!p);
if (pageIds.length === 0) {
return rows.map((r) => r.id);
}
const accessiblePageIds =
await this.pagePermissionRepo.filterAccessiblePageIds({
pageIds,
userId,
});
const accessibleSet = new Set(accessiblePageIds);
return rows
.filter((r) => !r.pageId || accessibleSet.has(r.pageId))
.map((r) => r.id);
}
async create(data: InsertableNotification) {
const user = await this.db
.selectFrom('users')
@@ -171,34 +73,19 @@ export class NotificationService {
}
async getUnreadCount(userId: string) {
const accessibleIds =
await this.listUnreadAccessibleNotificationIds(userId);
return accessibleIds.length;
return this.notificationRepo.getUnreadCount(userId);
}
async markAsRead(notificationId: string, userId: string) {
const accessibleIds = await this.filterAccessibleNotificationIds(
[notificationId],
userId,
);
if (accessibleIds.length === 0) return;
return this.notificationRepo.markAsRead(accessibleIds[0], userId);
return this.notificationRepo.markAsRead(notificationId, userId);
}
async markMultipleAsRead(notificationIds: string[], userId: string) {
const accessibleIds = await this.filterAccessibleNotificationIds(
notificationIds,
userId,
);
if (accessibleIds.length === 0) return;
return this.notificationRepo.markMultipleAsRead(accessibleIds, userId);
return this.notificationRepo.markMultipleAsRead(notificationIds, userId);
}
async markAllAsRead(userId: string) {
const accessibleIds =
await this.listUnreadAccessibleNotificationIds(userId);
if (accessibleIds.length === 0) return;
return this.notificationRepo.markMultipleAsRead(accessibleIds, userId);
return this.notificationRepo.markAllAsRead(userId);
}
async queueEmail(
@@ -11,7 +11,10 @@ import { ExpressionBuilder } from 'kysely';
import { DB } from '@docmost/db/types/db';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { NotificationTab, NotificationType } from '../../../core/notification/notification.constants';
import {
NotificationTab,
NotificationType,
} from '../../../core/notification/notification.constants';
@Injectable()
export class NotificationRepo {
@@ -43,7 +46,11 @@ export class NotificationRepo {
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
eb(
'spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
),
]),
);
@@ -62,6 +69,14 @@ export class NotificationRepo {
});
}
async insert(notification: InsertableNotification): Promise<Notification> {
return this.db
.insertInto('notifications')
.values(notification)
.returningAll()
.executeTakeFirst();
}
async getUnreadCount(userId: string): Promise<number> {
const result = await this.db
.selectFrom('notifications')
@@ -71,7 +86,11 @@ export class NotificationRepo {
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
eb(
'spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
),
]),
)
.executeTakeFirst();
@@ -79,14 +98,6 @@ export class NotificationRepo {
return Number(result?.count ?? 0);
}
async insert(notification: InsertableNotification): Promise<Notification> {
return this.db
.insertInto('notifications')
.values(notification)
.returningAll()
.executeTakeFirst();
}
async markAsRead(notificationId: string, userId: string): Promise<void> {
await this.db
.updateTable('notifications')
@@ -94,12 +105,6 @@ export class NotificationRepo {
.where('id', '=', notificationId)
.where('userId', '=', userId)
.where('readAt', 'is', null)
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
]),
)
.execute();
}
@@ -116,21 +121,6 @@ export class NotificationRepo {
.where('id', 'in', notificationIds)
.where('userId', '=', userId)
.where('readAt', 'is', null)
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
]),
)
.execute();
}
async markAsEmailed(notificationId: string): Promise<void> {
await this.db
.updateTable('notifications')
.set({ emailedAt: new Date() })
.where('id', '=', notificationId)
.where('emailedAt', 'is', null)
.execute();
}
@@ -140,12 +130,15 @@ export class NotificationRepo {
.set({ readAt: new Date() })
.where('userId', '=', userId)
.where('readAt', 'is', null)
.where((eb) =>
eb.or([
eb('spaceId', 'is', null),
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
]),
)
.execute();
}
async markAsEmailed(notificationId: string): Promise<void> {
await this.db
.updateTable('notifications')
.set({ emailedAt: new Date() })
.where('id', '=', notificationId)
.where('emailedAt', 'is', null)
.execute();
}