import { AccountCreatedDto } from "../dtos/AccountCreatedDto"; import { AccountDeletedDto } from "../dtos/AccountDeletedDto"; import { AccountUpdatedDto } from "../dtos/AccountUpdatedDto"; import { ApiKeyCreatedDto } from "../dtos/ApiKeyCreatedDto"; import { ApiKeyDeletedDto } from "../dtos/ApiKeyDeletedDto"; import { ApiKeyUpdatedDto } from "../dtos/ApiKeyUpdatedDto"; import { EmailReceivedDto } from "../dtos/EmailReceivedDto"; import { MemberDeletedDto } from "../dtos/MemberDeletedDto"; import { MemberInvitationAcceptedDto } from "../dtos/MemberInvitationAcceptedDto"; import { MemberInvitationCreatedDto } from "../dtos/MemberInvitationCreatedDto"; import { RoleAssignedDto } from "../dtos/RoleAssignedDto"; import { RoleUnassignedDto } from "../dtos/RoleUnassignedDto"; import { RowCreatedDto } from "../dtos/RowCreatedDto"; import { RowDeletedDto } from "../dtos/RowDeletedDto"; import { RowSharedDto } from "../dtos/RowSharedDto"; import { RowUpdatedDto } from "../dtos/RowUpdatedDto"; import { SubscriptionCancelledDto } from "../dtos/SubscriptionCancelledDto"; import { SubscriptionEndedDto } from "../dtos/SubscriptionEndedDto"; import { SubscriptionSubscribedDto } from "../dtos/SubscriptionSubscribedDto"; import { SubscriptionUpgradedDto } from "../dtos/SubscriptionUpgradedDto"; import { UserPasswordUpdatedDto } from "../dtos/UserPasswordUpdatedDto"; import { UserPreferencesUpdatedDto } from "../dtos/UserPreferencesUpdatedDto"; import { UserProfileDeletedDto } from "../dtos/UserProfileDeletedDto"; import { UserProfileUpdatedDto } from "../dtos/UserProfileUpdatedDto"; import { ApplicationEvent, getApplicationEventNameKey, isApplicationEvent } from "../types/ApplicationEvent"; import { Event } from "@toolctrl/db/prisma-browser"; import { RowTasksCreatedDto } from "../dtos/RowTasksCreatedDto"; import { RowTasksUpdatedDto } from "../dtos/RowTasksUpdatedDto"; import { RowTasksDeletedDto } from "../dtos/RowTasksDeletedDto"; import { RowCommentsCreatedDto } from "../dtos/RowCommentsCreatedDto"; import { RowCommentsDeletedDto } from "../dtos/RowCommentsDeletedDto"; import { RowCommentsReactedDto } from "../dtos/RowCommentsReactedDto"; import { OnboardingStartedDto } from "../dtos/OnboardingStartedDto"; import { OnboardingDismissedDto } from "../dtos/OnboardingDismissedDto"; import { OnboardingCompletedDto } from "../dtos/OnboardingCompletedDto"; import { SubscriptionDowngradedDto } from "../dtos/SubscriptionDowngradedDto"; import type { TFunction } from "i18next"; type EventDescriptionFormatters = { dateTime?: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string; }; function parseDescription(event: Event, t?: TFunction, formatters?: EventDescriptionFormatters) { if (event.description && !t) { return event.description; } try { const data = JSON.parse(event.data); return getDescription(event.name as ApplicationEvent, data, t, formatters); } catch { if (event.description) { return event.description; } return translateEventDescription(t, "events.descriptions.unknownWithName", `Unknown event: ${event.name}`, { name: event.name }); } } function parseName(eventName: string, t?: TFunction) { if (!isApplicationEvent(eventName)) { return translateEventDescription(t, "events.names.unmapped", eventName, { name: eventName }); } return t ? t(getApplicationEventNameKey(eventName)) : eventName; } function translateEventDescription( t: TFunction | undefined, key: string, defaultValue: string, values: Record = {} ) { return t ? t(key, { defaultValue, ...values }) : defaultValue; } function translateEventValue(t: TFunction | undefined, key: string, defaultValue: string) { return t ? t(key, { defaultValue }) : defaultValue; } function getDescription(event: ApplicationEvent, data: any, t?: TFunction, formatters?: EventDescriptionFormatters) { if (!isApplicationEvent(event)) { return translateEventDescription(t, "events.descriptions.unknown", "Unknown event"); } try { switch (event) { case "user.profile.updated": { const payload = data as UserProfileUpdatedDto; return translateEventDescription(t, "events.descriptions.userProfile.updated", `${payload.email} updated their profile`, { email: payload.email }); } case "user.profile.deleted": { const payload = data as UserProfileDeletedDto; return translateEventDescription(t, "events.descriptions.userProfile.deleted", `${payload.email} deleted their profile`, { email: payload.email }); } case "user.password.updated": { const payload = data as UserPasswordUpdatedDto; if (payload.fromUser) { return translateEventDescription( t, "events.descriptions.userPassword.updatedByUser", `${payload.fromUser.email} updated ${payload.user.email}'s password`, { fromEmail: payload.fromUser.email, email: payload.user.email } ); } return translateEventDescription(t, "events.descriptions.userPassword.updated", `${payload.user.email} updated their password`, { email: payload.user.email }); } case "user.mfa.totp.enrollment.started": { return translateEventDescription(t, "events.descriptions.userMfa.enrollmentStarted", `${data.user.email} started authenticator enrollment`, { email: data.user.email }); } case "user.mfa.totp.enabled": { return translateEventDescription(t, "events.descriptions.userMfa.enabled", `${data.user.email} enabled authenticator MFA`, { email: data.user.email }); } case "user.mfa.totp.disabled": { return translateEventDescription(t, "events.descriptions.userMfa.disabled", `${data.user.email} disabled authenticator MFA`, { email: data.user.email }); } case "user.preferences.updated": { const payload = data as UserPreferencesUpdatedDto; if (payload.old.locale !== undefined || payload.new.locale !== undefined) { const oldLocale = payload.old.locale ?? ""; const newLocale = payload.new.locale ?? ""; const oldLocaleLabel = oldLocale ? translateEventValue(t, `shared.locales.${oldLocale}`, oldLocale) : ""; const newLocaleLabel = newLocale ? translateEventValue(t, `shared.locales.${newLocale}`, newLocale) : ""; return translateEventDescription( t, "events.descriptions.userPreferences.localeUpdated", `${payload.user.email} updated their language: ${oldLocale} -> ${newLocale}`, { email: payload.user.email, oldLocale: oldLocaleLabel, newLocale: newLocaleLabel, } ); } if (payload.old.lightOrDarkMode !== undefined || payload.new.lightOrDarkMode !== undefined) { const oldTheme = payload.old.lightOrDarkMode ?? "light"; const newTheme = payload.new.lightOrDarkMode ?? "light"; const oldThemeLabel = translateEventValue(t, `shared.${oldTheme}`, oldTheme); const newThemeLabel = translateEventValue(t, `shared.${newTheme}`, newTheme); return translateEventDescription( t, "events.descriptions.userPreferences.themeUpdated", `${payload.user.email} updated their theme mode: ${oldTheme} -> ${newTheme}`, { email: payload.user.email, oldTheme: oldThemeLabel, newTheme: newThemeLabel, } ); } return translateEventDescription(t, "events.descriptions.userPreferences.updated", `${payload.user.email} updated their preferences`, { email: payload.user.email, }); } case "account.created": { const payload = data as AccountCreatedDto; return translateEventDescription(t, "events.descriptions.account.created", `Account created: ${payload.tenant.name}`, { tenant: payload.tenant.name }); } case "account.updated": { const payload = data as AccountUpdatedDto; return translateEventDescription( t, "events.descriptions.account.updated", `${payload.user.email} updated account: ${payload.old.name} (/${payload.old.slug}) -> ${payload.new.name} (/${payload.new.slug})`, { email: payload.user.email, oldName: payload.old.name, oldSlug: payload.old.slug, newName: payload.new.name, newSlug: payload.new.slug } ); } case "account.deleted": { const payload = data as AccountDeletedDto; let subscription = ""; if (payload.subscription) { subscription = `, ${payload.subscription.product.title} - $${payload.subscription.price.amount}`; } return translateEventDescription(t, "events.descriptions.account.deleted", `${payload.user.email} deleted the account '${payload.tenant.name}'${subscription}.`, { email: payload.user.email, tenant: payload.tenant.name, subscription, }); } case "member.invitation.created": { const payload = data as MemberInvitationCreatedDto; return translateEventDescription(t, "events.descriptions.memberInvitation.created", `${payload.fromUser.email} invited ${payload.user.email} to ${payload.tenant.name}`, { fromEmail: payload.fromUser.email, email: payload.user.email, tenant: payload.tenant.name, }); } case "member.invitation.accepted": { const payload = data as MemberInvitationAcceptedDto; const defaultPrefix = payload.newUser ? "(New user) " : ""; const prefix = payload.newUser ? translateEventDescription(t, "events.descriptions.memberInvitation.newUserPrefix", defaultPrefix) : ""; if (payload.fromUser) { return translateEventDescription( t, "events.descriptions.memberInvitation.acceptedFromUser", `${defaultPrefix}${payload.user.email} accepted the invitation to ${payload.tenant.name} from user ${payload.fromUser.email}`, { prefix, email: payload.user.email, tenant: payload.tenant.name, fromEmail: payload.fromUser.email } ); } return translateEventDescription(t, "events.descriptions.memberInvitation.accepted", `${defaultPrefix}${payload.user.email} accepted the invitation to ${payload.tenant.name}`, { prefix, email: payload.user.email, tenant: payload.tenant.name, }); } case "member.deleted": { const payload = data as MemberDeletedDto; return translateEventDescription(t, "events.descriptions.member.deleted", `${payload.fromUser.email} deleted ${payload.user.email} from ${payload.tenant.name}`, { fromEmail: payload.fromUser.email, email: payload.user.email, tenant: payload.tenant.name, }); } case "role.assigned": { const payload = data as RoleAssignedDto; return translateEventDescription(t, "events.descriptions.role.assigned", `${payload.fromUser.email} assigned the role "${payload.role.name}" to ${payload.toUser.email}`, { fromEmail: payload.fromUser.email, role: payload.role.name, email: payload.toUser.email, }); } case "role.unassigned": { const payload = data as RoleUnassignedDto; return translateEventDescription(t, "events.descriptions.role.unassigned", `${payload.fromUser.email} unassigned the role "${payload.role.name}" from ${payload.toUser.email}`, { fromEmail: payload.fromUser.email, role: payload.role.name, email: payload.toUser.email, }); } case "subscription.subscribed": { const payload = data as SubscriptionSubscribedDto; if (payload.user) { return translateEventDescription(t, "events.descriptions.subscription.subscribedByUser", `${payload.user.email} subscribed ${payload.tenant.name} to ${payload.subscription.product.title}`, { email: payload.user.email, tenant: payload.tenant.name, product: payload.subscription.product.title, }); } return translateEventDescription(t, "events.descriptions.subscription.subscribed", `${payload.tenant.name} subscribed to ${payload.subscription.product.title}`, { tenant: payload.tenant.name, product: payload.subscription.product.title, }); } case "subscription.upgraded": { const payload = data as SubscriptionUpgradedDto; const product = payload.subscription?.product.title ?? ""; if (payload.user) { return translateEventDescription(t, "events.descriptions.subscription.upgradedByUser", `Subscription upgraded: ${payload.tenant.name} (${product}), by ${payload.user.email}`, { tenant: payload.tenant.name, product, email: payload.user.email, }); } return translateEventDescription(t, "events.descriptions.subscription.upgraded", `Subscription upgraded: ${payload.tenant.name} (${product})`, { tenant: payload.tenant.name, product }); } case "subscription.downgraded": { const payload = data as SubscriptionDowngradedDto; const product = payload.subscription?.product.title ?? ""; if (payload.user) { return translateEventDescription(t, "events.descriptions.subscription.downgradedByUser", `Subscription downgraded: ${payload.tenant.name} (${product}), by ${payload.user.email}`, { tenant: payload.tenant.name, product, email: payload.user.email, }); } return translateEventDescription(t, "events.descriptions.subscription.downgraded", `Subscription downgraded: ${payload.tenant.name} (${product})`, { tenant: payload.tenant.name, product }); } case "subscription.cancelled": { const payload = data as SubscriptionCancelledDto; const product = payload.subscription?.product.title ?? ""; if (payload.user) { return translateEventDescription(t, "events.descriptions.subscription.cancelledByUser", `Subscription cancelled: ${payload.tenant.name} (${product}), by ${payload.user.email}`, { tenant: payload.tenant.name, product, email: payload.user.email, }); } return translateEventDescription(t, "events.descriptions.subscription.cancelled", `Subscription cancelled: ${payload.tenant.name} (${product})`, { tenant: payload.tenant.name, product }); } case "subscription.ended": { const payload = data as SubscriptionEndedDto; const product = payload.subscription?.product.title ?? ""; return translateEventDescription(t, "events.descriptions.subscription.ended", `Subscription ended: ${payload.tenant.name} (${product})`, { tenant: payload.tenant.name, product }); } case "api_key.created": { const payload = data as ApiKeyCreatedDto; return translateEventDescription(t, "events.descriptions.apiKey.created", `${payload.user.email} created API key: ${payload.alias}`, { email: payload.user.email, alias: payload.alias }); } case "api_key.updated": { const payload = data as ApiKeyUpdatedDto; return translateEventDescription(t, "events.descriptions.apiKey.updated", `${payload.user.email} updated API key: ${payload.old.alias} -> ${payload.new.alias}`, { email: payload.user.email, oldAlias: payload.old.alias, newAlias: payload.new.alias, }); } case "api_key.deleted": { const payload = data as ApiKeyDeletedDto; return translateEventDescription(t, "events.descriptions.apiKey.deleted", `${payload.user.email} deleted API key: ${payload.alias}`, { email: payload.user.email, alias: payload.alias }); } case "email.received": { const payload = data as EmailReceivedDto; return translateEventDescription(t, "events.descriptions.email.received", `New email from ${payload.fromEmail}: ${payload.subject}`, { fromEmail: payload.fromEmail, subject: payload.subject, }); } case "row.created": { const payload = data as RowCreatedDto; const actor = getUserOrApiKey(payload, t); return translateEventDescription(t, "events.descriptions.row.created", `New ${payload.entity.name}: ${payload.title}, by ${actor}`, { entity: payload.entity.name, title: payload.title, actor, }); } case "row.updated": { const payload = data as RowUpdatedDto; const actor = getUserOrApiKey(payload, t); return translateEventDescription(t, "events.descriptions.row.updated", `Updated ${payload.entity.name}: ${payload.title}, by ${actor}`, { entity: payload.entity.name, title: payload.title, actor, }); } case "row.deleted": { const payload = data as RowDeletedDto; const actor = getUserOrApiKey(payload, t); return translateEventDescription(t, "events.descriptions.row.deleted", `Deleted ${payload.entity.name}: ${payload.title}, by ${actor}`, { entity: payload.entity.name, title: payload.title, actor, }); } case "row.shared": { const payload = data as RowSharedDto; const actor = getUserOrApiKey(payload, t); return translateEventDescription( t, "events.descriptions.row.shared", `${payload.entity.name}: ${payload.title} shared with ${payload.type} "${payload.to}" (${payload.access}) by ${actor}`, { entity: payload.entity.name, title: payload.title, type: payload.type, to: payload.to, access: payload.access, actor } ); } case "row.tasks.created": { const payload = data as RowTasksCreatedDto; const actor = getUserOrApiKey(payload, t); return translateEventDescription(t, "events.descriptions.rowTasks.created", `Row ${payload.rowId} task created '${payload.task.name}' by ${actor}`, { rowId: payload.rowId, task: payload.task.name, actor, }); } case "row.tasks.updated": { const payload = data as RowTasksUpdatedDto; let change = ""; if (payload.new.completedAt) { const completedAt = formatters?.dateTime ? formatters.dateTime(payload.new.completedAt) : String(payload.new.completedAt); change = translateEventDescription( t, "events.descriptions.rowTasks.changes.completedAt", `completed at ${completedAt}`, { date: completedAt } ); } else { change = translateEventDescription(t, "events.descriptions.rowTasks.changes.incomplete", "set as incomplete"); } const actor = getUserOrApiKey(payload, t); return translateEventDescription(t, "events.descriptions.rowTasks.updated", `Row ${payload.rowId} task updated '${payload.new.name}', ${change}, by ${actor}`, { rowId: payload.rowId, task: payload.new.name, change, actor, }); } case "row.tasks.deleted": { const payload = data as RowTasksDeletedDto; const actor = getUserOrApiKey(payload, t); return translateEventDescription(t, "events.descriptions.rowTasks.deleted", `Row ${payload.rowId} task deleted '${payload.task.name}' by ${actor}`, { rowId: payload.rowId, task: payload.task.name, actor, }); } case "row.comments.created": { const payload = data as RowCommentsCreatedDto; const actor = payload.user ? payload.user.email : ""; return translateEventDescription(t, "events.descriptions.rowComments.created", `Row ${payload.rowId} commented '${payload.comment.text}' by ${actor}`, { rowId: payload.rowId, comment: payload.comment.text, actor, }); } case "row.comments.deleted": { const payload = data as RowCommentsDeletedDto; const actor = payload.user ? payload.user.email : ""; return translateEventDescription(t, "events.descriptions.rowComments.deleted", `Row ${payload.rowId} comment deleted '${payload.comment.text}' by ${actor}`, { rowId: payload.rowId, comment: payload.comment.text, actor, }); } case "row.comments.reacted": { const payload = data as RowCommentsReactedDto; const actor = payload.user ? payload.user.email : ""; return translateEventDescription(t, "events.descriptions.rowComments.reacted", `Row ${payload.rowId} comment reaction (${payload.reaction}) '${payload.comment.text}' by ${actor}`, { rowId: payload.rowId, reaction: payload.reaction, comment: payload.comment.text, actor, }); } case "onboarding.started": { const payload = data as OnboardingStartedDto; return translateEventDescription(t, "events.descriptions.onboarding.started", `Onboarding started by ${payload.user.email}: ${payload.onboarding.title}`, { email: payload.user.email, title: payload.onboarding.title, }); } case "onboarding.dismissed": { const payload = data as OnboardingDismissedDto; return translateEventDescription(t, "events.descriptions.onboarding.dismissed", `Onboarding dismissed by ${payload.user.email}: ${payload.onboarding.title}`, { email: payload.user.email, title: payload.onboarding.title, }); } case "onboarding.completed": { const payload = data as OnboardingCompletedDto; return translateEventDescription(t, "events.descriptions.onboarding.completed", `Onboarding completed by ${payload.user.email}: ${payload.onboarding.title}`, { email: payload.user.email, title: payload.onboarding.title, }); } default: return assertNever(event); } } catch { return parseName(event, t); } } function assertNever(x: never): never { throw new Error("Unexpected event: " + x); } function getUserOrApiKey(item: { user?: { id: string; email: string } | null; apiKey?: { id: string; alias: string } | null }, t?: TFunction) { if (item.user) { return item.user.email; } else if (item.apiKey) { return translateEventDescription(t, "events.actors.apiKey", "API Key {{alias}}", { alias: item.apiKey.alias }); } return ""; } export default { parseName, parseDescription, getDescription, };