mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
- update templates
- update migration and notification
This commit is contained in:
@@ -27,6 +27,19 @@ export class VerificationNotificationService {
|
|||||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private async getAlreadyNotifiedUserIds(
|
||||||
|
pageVerificationId: string,
|
||||||
|
type: string,
|
||||||
|
): Promise<Set<string>> {
|
||||||
|
const rows = await this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select('userId')
|
||||||
|
.where('pageVerificationId', '=', pageVerificationId)
|
||||||
|
.where('type', '=', type)
|
||||||
|
.execute();
|
||||||
|
return new Set(rows.map((r) => r.userId));
|
||||||
|
}
|
||||||
|
|
||||||
private async filterAccessibleRecipients(
|
private async filterAccessibleRecipients(
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
pageId: string,
|
pageId: string,
|
||||||
@@ -74,6 +87,15 @@ export class VerificationNotificationService {
|
|||||||
);
|
);
|
||||||
if (accessibleVerifierIds.length === 0) return;
|
if (accessibleVerifierIds.length === 0) return;
|
||||||
|
|
||||||
|
const alreadyNotified = await this.getAlreadyNotifiedUserIds(
|
||||||
|
verification.id,
|
||||||
|
NotificationType.PAGE_VERIFICATION_EXPIRING,
|
||||||
|
);
|
||||||
|
const recipients = accessibleVerifierIds.filter(
|
||||||
|
(id) => !alreadyNotified.has(id),
|
||||||
|
);
|
||||||
|
if (recipients.length === 0) return;
|
||||||
|
|
||||||
const context = await this.getPageContext(
|
const context = await this.getPageContext(
|
||||||
verification.pageId,
|
verification.pageId,
|
||||||
verification.spaceId,
|
verification.spaceId,
|
||||||
@@ -81,16 +103,17 @@ export class VerificationNotificationService {
|
|||||||
);
|
);
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
const { pageTitle, basePageUrl } = context;
|
const { pageTitle, spaceName, basePageUrl } = context;
|
||||||
const expiresAtIso = new Date(verification.expiresAt).toISOString();
|
const expiresAtIso = new Date(verification.expiresAt).toISOString();
|
||||||
|
|
||||||
for (const userId of accessibleVerifierIds) {
|
for (const userId of recipients) {
|
||||||
const notification = await this.notificationService.create({
|
const notification = await this.notificationService.create({
|
||||||
userId,
|
userId,
|
||||||
workspaceId: verification.workspaceId,
|
workspaceId: verification.workspaceId,
|
||||||
type: NotificationType.PAGE_VERIFICATION_EXPIRING,
|
type: NotificationType.PAGE_VERIFICATION_EXPIRING,
|
||||||
pageId: verification.pageId,
|
pageId: verification.pageId,
|
||||||
spaceId: verification.spaceId,
|
spaceId: verification.spaceId,
|
||||||
|
pageVerificationId: verification.id,
|
||||||
data: { expiresAt: expiresAtIso },
|
data: { expiresAt: expiresAtIso },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,6 +125,7 @@ export class VerificationNotificationService {
|
|||||||
subject,
|
subject,
|
||||||
VerificationExpiringEmail({
|
VerificationExpiringEmail({
|
||||||
pageTitle,
|
pageTitle,
|
||||||
|
spaceName,
|
||||||
pageUrl: basePageUrl,
|
pageUrl: basePageUrl,
|
||||||
expiresAt: new Date(verification.expiresAt).toLocaleDateString(),
|
expiresAt: new Date(verification.expiresAt).toLocaleDateString(),
|
||||||
}),
|
}),
|
||||||
@@ -139,6 +163,15 @@ export class VerificationNotificationService {
|
|||||||
);
|
);
|
||||||
if (accessibleVerifierIds.length === 0) return;
|
if (accessibleVerifierIds.length === 0) return;
|
||||||
|
|
||||||
|
const alreadyNotified = await this.getAlreadyNotifiedUserIds(
|
||||||
|
verification.id,
|
||||||
|
NotificationType.PAGE_VERIFICATION_EXPIRED,
|
||||||
|
);
|
||||||
|
const recipients = accessibleVerifierIds.filter(
|
||||||
|
(id) => !alreadyNotified.has(id),
|
||||||
|
);
|
||||||
|
if (recipients.length === 0) return;
|
||||||
|
|
||||||
const context = await this.getPageContext(
|
const context = await this.getPageContext(
|
||||||
verification.pageId,
|
verification.pageId,
|
||||||
verification.spaceId,
|
verification.spaceId,
|
||||||
@@ -146,15 +179,16 @@ export class VerificationNotificationService {
|
|||||||
);
|
);
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
const { pageTitle, basePageUrl } = context;
|
const { pageTitle, spaceName, basePageUrl } = context;
|
||||||
|
|
||||||
for (const userId of accessibleVerifierIds) {
|
for (const userId of recipients) {
|
||||||
const notification = await this.notificationService.create({
|
const notification = await this.notificationService.create({
|
||||||
userId,
|
userId,
|
||||||
workspaceId: verification.workspaceId,
|
workspaceId: verification.workspaceId,
|
||||||
type: NotificationType.PAGE_VERIFICATION_EXPIRED,
|
type: NotificationType.PAGE_VERIFICATION_EXPIRED,
|
||||||
pageId: verification.pageId,
|
pageId: verification.pageId,
|
||||||
spaceId: verification.spaceId,
|
spaceId: verification.spaceId,
|
||||||
|
pageVerificationId: verification.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const subject = `"${pageTitle}" verification has expired`;
|
const subject = `"${pageTitle}" verification has expired`;
|
||||||
@@ -165,6 +199,7 @@ export class VerificationNotificationService {
|
|||||||
subject,
|
subject,
|
||||||
VerificationExpiredEmail({
|
VerificationExpiredEmail({
|
||||||
pageTitle,
|
pageTitle,
|
||||||
|
spaceName,
|
||||||
pageUrl: basePageUrl,
|
pageUrl: basePageUrl,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -305,7 +340,7 @@ export class VerificationNotificationService {
|
|||||||
.executeTakeFirst(),
|
.executeTakeFirst(),
|
||||||
this.db
|
this.db
|
||||||
.selectFrom('spaces')
|
.selectFrom('spaces')
|
||||||
.select(['id', 'slug'])
|
.select(['id', 'slug', 'name'])
|
||||||
.where('id', '=', spaceId)
|
.where('id', '=', spaceId)
|
||||||
.executeTakeFirst(),
|
.executeTakeFirst(),
|
||||||
]);
|
]);
|
||||||
@@ -313,6 +348,6 @@ export class VerificationNotificationService {
|
|||||||
if (!page || !space) return null;
|
if (!page || !space) return null;
|
||||||
|
|
||||||
const basePageUrl = `${appUrl}/s/${space.slug}/p/${page.slugId}`;
|
const basePageUrl = `${appUrl}/s/${space.slug}/p/${page.slugId}`;
|
||||||
return { pageTitle: getPageTitle(page.title), basePageUrl };
|
return { pageTitle: getPageTitle(page.title), spaceName: space.name ?? space.slug, basePageUrl };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,9 +98,20 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
.on('page_verifiers')
|
.on('page_verifiers')
|
||||||
.column('user_id')
|
.column('user_id')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('notifications')
|
||||||
|
.addColumn('page_verification_id', 'uuid', (col) =>
|
||||||
|
col.references('page_verifications.id').onDelete('cascade'),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('notifications')
|
||||||
|
.dropColumn('page_verification_id')
|
||||||
|
.execute();
|
||||||
await db.schema.dropTable('page_verifiers').ifExists().execute();
|
await db.schema.dropTable('page_verifiers').ifExists().execute();
|
||||||
await db.schema.dropTable('page_verifications').ifExists().execute();
|
await db.schema.dropTable('page_verifications').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -400,6 +400,7 @@ export interface Notifications {
|
|||||||
pageId: string | null;
|
pageId: string | null;
|
||||||
spaceId: string | null;
|
spaceId: string | null;
|
||||||
commentId: string | null;
|
commentId: string | null;
|
||||||
|
pageVerificationId: string | null;
|
||||||
data: Json | null;
|
data: Json | null;
|
||||||
readAt: Timestamp | null;
|
readAt: Timestamp | null;
|
||||||
emailedAt: Timestamp | null;
|
emailedAt: Timestamp | null;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: d7274d6e59...385f36bbae
@@ -1,7 +1,7 @@
|
|||||||
import { Section, Text, Button } from '@react-email/components';
|
import { Section, Text } from '@react-email/components';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { button, content, paragraph } from '../css/styles';
|
import { content, paragraph } from '../css/styles';
|
||||||
import { MailBody } from '../partials/partials';
|
import { EmailButton, MailBody } from '../partials/partials';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
actorName: string;
|
actorName: string;
|
||||||
@@ -30,19 +30,7 @@ export const ApprovalRejectedEmail = ({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
<Section
|
<EmailButton href={pageUrl}>View page</EmailButton>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: '15px',
|
|
||||||
paddingBottom: '15px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button href={pageUrl} style={button}>
|
|
||||||
View page
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</MailBody>
|
</MailBody>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Section, Text, Button } from '@react-email/components';
|
import { Section, Text } from '@react-email/components';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { button, content, paragraph } from '../css/styles';
|
import { content, paragraph } from '../css/styles';
|
||||||
import { MailBody } from '../partials/partials';
|
import { EmailButton, MailBody } from '../partials/partials';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
actorName: string;
|
actorName: string;
|
||||||
@@ -23,19 +23,7 @@ export const ApprovalRequestedEmail = ({
|
|||||||
<strong>{pageTitle}</strong> for your approval.
|
<strong>{pageTitle}</strong> for your approval.
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Section
|
<EmailButton href={pageUrl}>Review page</EmailButton>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: '15px',
|
|
||||||
paddingBottom: '15px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button href={pageUrl} style={button}>
|
|
||||||
Review page
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</MailBody>
|
</MailBody>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +1,26 @@
|
|||||||
import { Section, Text, Button } from '@react-email/components';
|
import { Section, Text } from '@react-email/components';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { button, content, paragraph } from '../css/styles';
|
import { content, paragraph } from '../css/styles';
|
||||||
import { MailBody } from '../partials/partials';
|
import { EmailButton, MailBody } from '../partials/partials';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
|
spaceName: string;
|
||||||
pageUrl: string;
|
pageUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VerificationExpiredEmail = ({ pageTitle, pageUrl }: Props) => {
|
export const VerificationExpiredEmail = ({ pageTitle, spaceName, pageUrl }: Props) => {
|
||||||
return (
|
return (
|
||||||
<MailBody>
|
<MailBody>
|
||||||
<Section style={content}>
|
<Section style={content}>
|
||||||
<Text style={paragraph}>Hi there,</Text>
|
<Text style={paragraph}>Hi there,</Text>
|
||||||
<Text style={paragraph}>
|
<Text style={paragraph}>
|
||||||
The verification for <strong>{pageTitle}</strong> has expired. Please
|
The verification for <strong>{pageTitle}</strong> in{' '}
|
||||||
re-verify the page to confirm it is still accurate.
|
<strong>{spaceName}</strong> has expired. Please re-verify the page to
|
||||||
|
confirm it is still accurate.
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Section
|
<EmailButton href={pageUrl}>Re-verify page</EmailButton>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: '15px',
|
|
||||||
paddingBottom: '15px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button href={pageUrl} style={button}>
|
|
||||||
Re-verify page
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</MailBody>
|
</MailBody>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Section, Text, Button } from '@react-email/components';
|
import { Section, Text } from '@react-email/components';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { button, content, paragraph } from '../css/styles';
|
import { content, paragraph } from '../css/styles';
|
||||||
import { MailBody } from '../partials/partials';
|
import { EmailButton, MailBody } from '../partials/partials';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
|
spaceName: string;
|
||||||
pageUrl: string;
|
pageUrl: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VerificationExpiringEmail = ({
|
export const VerificationExpiringEmail = ({
|
||||||
pageTitle,
|
pageTitle,
|
||||||
|
spaceName,
|
||||||
pageUrl,
|
pageUrl,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -19,23 +21,12 @@ export const VerificationExpiringEmail = ({
|
|||||||
<Section style={content}>
|
<Section style={content}>
|
||||||
<Text style={paragraph}>Hi there,</Text>
|
<Text style={paragraph}>Hi there,</Text>
|
||||||
<Text style={paragraph}>
|
<Text style={paragraph}>
|
||||||
The page <strong>{pageTitle}</strong> needs to be re-verified. The
|
The page <strong>{pageTitle}</strong> in{' '}
|
||||||
verification expires on <strong>{expiresAt}</strong>.
|
<strong>{spaceName}</strong> needs to be re-verified. The verification
|
||||||
|
expires on <strong>{expiresAt}</strong>.
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Section
|
<EmailButton href={pageUrl}>Review page</EmailButton>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: '15px',
|
|
||||||
paddingBottom: '15px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button href={pageUrl} style={button}>
|
|
||||||
Review page
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
</MailBody>
|
</MailBody>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user