mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +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";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user