mirror of
https://github.com/docmost/docmost.git
synced 2026-05-25 12:04:35 +08:00
fix
This commit is contained in:
@@ -2,14 +2,15 @@ import { Group, List, Stack, Table, Text, ThemeIcon } from "@mantine/core";
|
|||||||
import { IconCheck } from "@tabler/icons-react";
|
import { IconCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
const enterpriseFeatures = [
|
const enterpriseFeatures = [
|
||||||
"SSO (SAML, OIDC, LDAP)",
|
"AI Integration (Chat, Search & Assistant)",
|
||||||
"AI Integration (Search & Assistant)",
|
|
||||||
"Page-level Permissions",
|
|
||||||
"Audit Logs",
|
|
||||||
"API Keys",
|
|
||||||
"MCP Support",
|
"MCP Support",
|
||||||
|
"SSO (SAML, OIDC, LDAP)",
|
||||||
"Multi-factor Authentication (2FA)",
|
"Multi-factor Authentication (2FA)",
|
||||||
|
"Page-level Permissions",
|
||||||
|
"Page verification & approval workflow",
|
||||||
|
"Audit Logs",
|
||||||
"Enterprise Controls",
|
"Enterprise Controls",
|
||||||
|
"API Keys",
|
||||||
"Advanced Search Engine Support",
|
"Advanced Search Engine Support",
|
||||||
"Full-text Search in Attachments (PDF, DOCX)",
|
"Full-text Search in Attachments (PDF, DOCX)",
|
||||||
"Resolve Comments",
|
"Resolve Comments",
|
||||||
@@ -68,11 +69,31 @@ export default function OssDetails() {
|
|||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Text size="sm" c="dimmed">
|
<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>
|
||||||
|
|
||||||
<Text size="sm" c="dimmed">
|
<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>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { WsGateway } from '../../ws/ws.gateway';
|
|||||||
import { MailService } from '../../integrations/mail/mail.service';
|
import { MailService } from '../../integrations/mail/mail.service';
|
||||||
import { NotificationTab, NotificationType, NotificationTypeToSettingKey } from './notification.constants';
|
import { NotificationTab, NotificationType, NotificationTypeToSettingKey } from './notification.constants';
|
||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService {
|
export class NotificationService {
|
||||||
@@ -17,108 +16,11 @@ export class NotificationService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly notificationRepo: NotificationRepo,
|
private readonly notificationRepo: NotificationRepo,
|
||||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
|
||||||
private readonly wsGateway: WsGateway,
|
private readonly wsGateway: WsGateway,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
@InjectKysely() private readonly db: KyselyDB,
|
@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) {
|
async create(data: InsertableNotification) {
|
||||||
const user = await this.db
|
const user = await this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
@@ -171,34 +73,19 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUnreadCount(userId: string) {
|
async getUnreadCount(userId: string) {
|
||||||
const accessibleIds =
|
return this.notificationRepo.getUnreadCount(userId);
|
||||||
await this.listUnreadAccessibleNotificationIds(userId);
|
|
||||||
return accessibleIds.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAsRead(notificationId: string, userId: string) {
|
async markAsRead(notificationId: string, userId: string) {
|
||||||
const accessibleIds = await this.filterAccessibleNotificationIds(
|
return this.notificationRepo.markAsRead(notificationId, userId);
|
||||||
[notificationId],
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
if (accessibleIds.length === 0) return;
|
|
||||||
return this.notificationRepo.markAsRead(accessibleIds[0], userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async markMultipleAsRead(notificationIds: string[], userId: string) {
|
async markMultipleAsRead(notificationIds: string[], userId: string) {
|
||||||
const accessibleIds = await this.filterAccessibleNotificationIds(
|
return this.notificationRepo.markMultipleAsRead(notificationIds, userId);
|
||||||
notificationIds,
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
if (accessibleIds.length === 0) return;
|
|
||||||
return this.notificationRepo.markMultipleAsRead(accessibleIds, userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAllAsRead(userId: string) {
|
async markAllAsRead(userId: string) {
|
||||||
const accessibleIds =
|
return this.notificationRepo.markAllAsRead(userId);
|
||||||
await this.listUnreadAccessibleNotificationIds(userId);
|
|
||||||
if (accessibleIds.length === 0) return;
|
|
||||||
return this.notificationRepo.markMultipleAsRead(accessibleIds, userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueEmail(
|
async queueEmail(
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { ExpressionBuilder } from 'kysely';
|
|||||||
import { DB } from '@docmost/db/types/db';
|
import { DB } from '@docmost/db/types/db';
|
||||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
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()
|
@Injectable()
|
||||||
export class NotificationRepo {
|
export class NotificationRepo {
|
||||||
@@ -43,7 +46,11 @@ export class NotificationRepo {
|
|||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([
|
eb.or([
|
||||||
eb('spaceId', 'is', null),
|
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> {
|
async getUnreadCount(userId: string): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.selectFrom('notifications')
|
.selectFrom('notifications')
|
||||||
@@ -71,7 +86,11 @@ export class NotificationRepo {
|
|||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([
|
eb.or([
|
||||||
eb('spaceId', 'is', null),
|
eb('spaceId', 'is', null),
|
||||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
eb(
|
||||||
|
'spaceId',
|
||||||
|
'in',
|
||||||
|
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -79,14 +98,6 @@ export class NotificationRepo {
|
|||||||
return Number(result?.count ?? 0);
|
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> {
|
async markAsRead(notificationId: string, userId: string): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.updateTable('notifications')
|
.updateTable('notifications')
|
||||||
@@ -94,12 +105,6 @@ export class NotificationRepo {
|
|||||||
.where('id', '=', notificationId)
|
.where('id', '=', notificationId)
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.where('readAt', 'is', null)
|
.where('readAt', 'is', null)
|
||||||
.where((eb) =>
|
|
||||||
eb.or([
|
|
||||||
eb('spaceId', 'is', null),
|
|
||||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,21 +121,6 @@ export class NotificationRepo {
|
|||||||
.where('id', 'in', notificationIds)
|
.where('id', 'in', notificationIds)
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.where('readAt', 'is', null)
|
.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();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +130,15 @@ export class NotificationRepo {
|
|||||||
.set({ readAt: new Date() })
|
.set({ readAt: new Date() })
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.where('readAt', 'is', null)
|
.where('readAt', 'is', null)
|
||||||
.where((eb) =>
|
.execute();
|
||||||
eb.or([
|
}
|
||||||
eb('spaceId', 'is', null),
|
|
||||||
eb('spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId)),
|
async markAsEmailed(notificationId: string): Promise<void> {
|
||||||
]),
|
await this.db
|
||||||
)
|
.updateTable('notifications')
|
||||||
|
.set({ emailedAt: new Date() })
|
||||||
|
.where('id', '=', notificationId)
|
||||||
|
.where('emailedAt', 'is', null)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user