- update templates

- update migration and notification
This commit is contained in:
Philipinho
2026-04-13 19:35:36 +01:00
parent b6350f3a2f
commit 8235980eeb
8 changed files with 80 additions and 76 deletions
@@ -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
View File
@@ -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,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>
); );
}; };