mirror of
https://github.com/documenso/documenso.git
synced 2025-11-27 06:54:01 +10:00
Merge branch 'main' into feat/audit-logs-api
This commit is contained in:
@@ -75,6 +75,7 @@ export function usePageRenderer(renderFunction: RenderFunction) {
|
||||
canvas.style.height = `${Math.floor(scaledViewport.height)}px`;
|
||||
|
||||
const renderContext: RenderParameters = {
|
||||
canvas,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
|
||||
viewport: renderViewport,
|
||||
|
||||
@@ -6,7 +6,7 @@ export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||
export const NEXT_PUBLIC_WEBAPP_URL = () =>
|
||||
env('NEXT_PUBLIC_WEBAPP_URL') ?? 'http://localhost:3000';
|
||||
|
||||
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
|
||||
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL = () =>
|
||||
env('NEXT_PRIVATE_INTERNAL_WEBAPP_URL') ?? NEXT_PUBLIC_WEBAPP_URL();
|
||||
|
||||
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
||||
@@ -15,3 +15,6 @@ export const API_V2_BETA_URL = '/api/v2-beta';
|
||||
export const API_V2_URL = '/api/v2';
|
||||
|
||||
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
|
||||
|
||||
export const USE_INTERNAL_URL_BROWSERLESS = () =>
|
||||
env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Context as HonoContext } from 'hono';
|
||||
import type { Context, Handler, InngestFunction } from 'inngest';
|
||||
import { Inngest as InngestClient } from 'inngest';
|
||||
import type { Logger } from 'inngest';
|
||||
import { serve as createHonoPagesRoute } from 'inngest/hono';
|
||||
import type { Logger } from 'inngest/middleware/logger';
|
||||
|
||||
import { env } from '../../utils/env';
|
||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||
|
||||
@@ -213,7 +213,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
}) {
|
||||
const { jobId, jobDefinitionId, data, isRetry } = options;
|
||||
|
||||
const endpoint = `${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/jobs/${jobDefinitionId}/${jobId}`;
|
||||
const endpoint = `${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/api/jobs/${jobDefinitionId}/${jobId}`;
|
||||
const signature = sign(data);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
@@ -49,7 +49,8 @@ export const run = async ({
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
const rows = parse(csvContent, { columns: true, skip_empty_lines: true });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rows = parse<any>(csvContent, { columns: true, skip_empty_lines: true });
|
||||
|
||||
if (rows.length > 100) {
|
||||
throw new Error('Maximum 100 rows allowed per upload');
|
||||
|
||||
@@ -22,53 +22,51 @@ export const run = async ({
|
||||
|
||||
const { webhookUrl: url, secret } = webhook;
|
||||
|
||||
await io.runTask('execute-webhook', async () => {
|
||||
const payloadData = {
|
||||
event,
|
||||
payload: data,
|
||||
createdAt: new Date().toISOString(),
|
||||
webhookEndpoint: url,
|
||||
};
|
||||
const payloadData = {
|
||||
event,
|
||||
payload: data,
|
||||
createdAt: new Date().toISOString(),
|
||||
webhookEndpoint: url,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payloadData),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Documenso-Secret': secret ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
|
||||
|
||||
try {
|
||||
responseBody = JSON.parse(body);
|
||||
} catch (err) {
|
||||
responseBody = body;
|
||||
}
|
||||
|
||||
await prisma.webhookCall.create({
|
||||
data: {
|
||||
url,
|
||||
event,
|
||||
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
|
||||
requestBody: payloadData as Prisma.InputJsonValue,
|
||||
responseCode: response.status,
|
||||
responseBody,
|
||||
responseHeaders: Object.fromEntries(response.headers.entries()),
|
||||
webhookId: webhook.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Webhook execution failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
status: response.status,
|
||||
};
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payloadData),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Documenso-Secret': secret ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
|
||||
|
||||
try {
|
||||
responseBody = JSON.parse(body);
|
||||
} catch (err) {
|
||||
responseBody = body;
|
||||
}
|
||||
|
||||
await prisma.webhookCall.create({
|
||||
data: {
|
||||
url,
|
||||
event,
|
||||
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
|
||||
requestBody: payloadData as Prisma.InputJsonValue,
|
||||
responseCode: response.status,
|
||||
responseBody,
|
||||
responseHeaders: Object.fromEntries(response.headers.entries()),
|
||||
webhookId: webhook.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Webhook execution failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
status: response.status,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,51 +15,52 @@
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.410.0",
|
||||
"@aws-sdk/client-sesv2": "^3.410.0",
|
||||
"@aws-sdk/cloudfront-signer": "^3.410.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.410.0",
|
||||
"@aws-sdk/client-s3": "^3.936.0",
|
||||
"@aws-sdk/client-sesv2": "^3.936.0",
|
||||
"@aws-sdk/cloudfront-signer": "^3.935.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.936.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.936.0",
|
||||
"@documenso/assets": "*",
|
||||
"@documenso/email": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/signing": "*",
|
||||
"@lingui/core": "^5.2.0",
|
||||
"@lingui/macro": "^5.2.0",
|
||||
"@lingui/react": "^5.2.0",
|
||||
"@noble/ciphers": "0.4.0",
|
||||
"@noble/hashes": "1.3.2",
|
||||
"@node-rs/bcrypt": "^1.10.0",
|
||||
"@lingui/core": "^5.6.0",
|
||||
"@lingui/macro": "^5.6.0",
|
||||
"@lingui/react": "^5.6.0",
|
||||
"@noble/ciphers": "0.6.0",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@node-rs/bcrypt": "^1.10.7",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@team-plain/typescript-sdk": "^5.9.0",
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"inngest": "^3.19.13",
|
||||
"jose": "^6.0.0",
|
||||
"kysely": "0.26.3",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"@scure/base": "^1.2.6",
|
||||
"@sindresorhus/slugify": "^3.0.0",
|
||||
"@team-plain/typescript-sdk": "^5.11.0",
|
||||
"@vvo/tzdb": "^6.196.0",
|
||||
"csv-parse": "^6.1.0",
|
||||
"inngest": "^3.45.1",
|
||||
"jose": "^6.1.2",
|
||||
"konva": "^10.0.9",
|
||||
"kysely": "0.28.8",
|
||||
"luxon": "^3.7.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"oslo": "^0.17.0",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"playwright": "1.52.0",
|
||||
"posthog-js": "^1.245.0",
|
||||
"posthog-node": "^4.17.0",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^9.14.0",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"playwright": "1.56.1",
|
||||
"posthog-js": "^1.297.2",
|
||||
"posthog-node": "4.18.0",
|
||||
"react": "^18",
|
||||
"remeda": "^2.17.3",
|
||||
"sharp": "0.32.6",
|
||||
"react-pdf": "^10.2.0",
|
||||
"remeda": "^2.32.0",
|
||||
"sharp": "0.34.5",
|
||||
"skia-canvas": "^3.0.8",
|
||||
"stripe": "^12.7.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"stripe": "^12.18.0",
|
||||
"ts-pattern": "^5.9.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/browser-chromium": "1.52.0",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/pg": "^8.11.4"
|
||||
"@playwright/browser-chromium": "1.56.1",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@types/pg": "^8.15.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const getMonthlyActiveUsers = async () => {
|
||||
)
|
||||
.as('cume_count'),
|
||||
])
|
||||
.where(sql`type = ${UserSecurityAuditLogType.SIGN_IN}::"UserSecurityAuditLogType"`)
|
||||
.where(() => sql`type = ${UserSecurityAuditLogType.SIGN_IN}::"UserSecurityAuditLogType"`)
|
||||
.groupBy(({ fn }) => fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']))
|
||||
.orderBy('month', 'desc')
|
||||
.limit(12);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { PostHog } from 'posthog-node';
|
||||
|
||||
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
||||
|
||||
export default function PostHogServerClient() {
|
||||
const postHogConfig = extractPostHogConfig();
|
||||
|
||||
if (!postHogConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PostHog(postHogConfig.key, {
|
||||
host: postHogConfig.host,
|
||||
fetch: async (...args) => fetch(...args),
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { Browser } from 'playwright';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import {
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL,
|
||||
NEXT_PUBLIC_WEBAPP_URL,
|
||||
USE_INTERNAL_URL_BROWSERLESS,
|
||||
} from '../../constants/app';
|
||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||
import { env } from '../../utils/env';
|
||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||
@@ -48,14 +52,19 @@ export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfO
|
||||
{
|
||||
name: 'language',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
url: USE_INTERNAL_URL_BROWSERLESS()
|
||||
? NEXT_PUBLIC_WEBAPP_URL()
|
||||
: NEXT_PRIVATE_INTERNAL_WEBAPP_URL(),
|
||||
},
|
||||
]);
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.goto(
|
||||
`${USE_INTERNAL_URL_BROWSERLESS() ? NEXT_PUBLIC_WEBAPP_URL() : NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`,
|
||||
{
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
// !: This is a workaround to ensure the page is loaded correctly.
|
||||
// !: It's not clear why but suddenly browserless cdp connections would
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { Browser } from 'playwright';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import {
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL,
|
||||
NEXT_PUBLIC_WEBAPP_URL,
|
||||
USE_INTERNAL_URL_BROWSERLESS,
|
||||
} from '../../constants/app';
|
||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||
import { env } from '../../utils/env';
|
||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||
@@ -48,14 +52,19 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
|
||||
{
|
||||
name: 'lang',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
url: USE_INTERNAL_URL_BROWSERLESS()
|
||||
? NEXT_PUBLIC_WEBAPP_URL()
|
||||
: NEXT_PRIVATE_INTERNAL_WEBAPP_URL(),
|
||||
},
|
||||
]);
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.goto(
|
||||
`${USE_INTERNAL_URL_BROWSERLESS() ? NEXT_PUBLIC_WEBAPP_URL() : NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`,
|
||||
{
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
// !: This is a workaround to ensure the page is loaded correctly.
|
||||
// !: It's not clear why but suddenly browserless cdp connections would
|
||||
|
||||
22
packages/lib/server-only/site-settings/get-site-setting.ts
Normal file
22
packages/lib/server-only/site-settings/get-site-setting.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { TSiteSettingSchema } from './schema';
|
||||
import { ZSiteSettingSchema } from './schema';
|
||||
|
||||
export const getSiteSetting = async <
|
||||
T extends TSiteSettingSchema['id'],
|
||||
U = Extract<TSiteSettingSchema, { id: T }>,
|
||||
>(options: {
|
||||
id: T;
|
||||
}): Promise<U> => {
|
||||
const { id } = options;
|
||||
|
||||
const setting = await prisma.siteSettings.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return ZSiteSettingSchema.parse(setting) as U;
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZSiteSettingsBannerSchema } from './schemas/banner';
|
||||
import { ZSiteSettingsTelemetrySchema } from './schemas/telemetry';
|
||||
|
||||
// TODO: Use `z.union([...])` once we have more than one setting
|
||||
export const ZSiteSettingSchema = ZSiteSettingsBannerSchema;
|
||||
export const ZSiteSettingSchema = z.union([
|
||||
ZSiteSettingsBannerSchema,
|
||||
ZSiteSettingsTelemetrySchema,
|
||||
]);
|
||||
|
||||
export type TSiteSettingSchema = z.infer<typeof ZSiteSettingSchema>;
|
||||
|
||||
|
||||
14
packages/lib/server-only/site-settings/schemas/telemetry.ts
Normal file
14
packages/lib/server-only/site-settings/schemas/telemetry.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZSiteSettingsBaseSchema } from './_base';
|
||||
|
||||
export const SITE_SETTINGS_TELEMETRY_ID = 'telemetry.installation';
|
||||
|
||||
export const ZSiteSettingsTelemetrySchema = ZSiteSettingsBaseSchema.extend({
|
||||
id: z.literal(SITE_SETTINGS_TELEMETRY_ID),
|
||||
data: z.object({
|
||||
installationId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TSiteSettingsTelemetrySchema = z.infer<typeof ZSiteSettingsTelemetrySchema>;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { TSiteSettingSchema } from './schema';
|
||||
import { type TSiteSettingSchema } from './schema';
|
||||
|
||||
export type UpsertSiteSettingOptions = TSiteSettingSchema & {
|
||||
userId: number;
|
||||
userId?: number | null;
|
||||
};
|
||||
|
||||
export const upsertSiteSetting = async ({
|
||||
|
||||
190
packages/lib/server-only/telemetry/telemetry-client.ts
Normal file
190
packages/lib/server-only/telemetry/telemetry-client.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/* eslint-disable require-atomic-updates */
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { PostHog } from 'posthog-node';
|
||||
|
||||
import { version } from '../../../../package.json';
|
||||
import { prefixedId } from '../../universal/id';
|
||||
import { getSiteSetting } from '../site-settings/get-site-setting';
|
||||
import { SITE_SETTINGS_TELEMETRY_ID } from '../site-settings/schemas/telemetry';
|
||||
import { upsertSiteSetting } from '../site-settings/upsert-site-setting';
|
||||
|
||||
const TELEMETRY_KEY = process.env.NEXT_PRIVATE_TELEMETRY_KEY;
|
||||
const TELEMETRY_HOST = process.env.NEXT_PRIVATE_TELEMETRY_HOST;
|
||||
const TELEMETRY_DISABLED = !!process.env.DOCUMENSO_DISABLE_TELEMETRY;
|
||||
|
||||
const NODE_ID_FILENAME = '.documenso-node-id';
|
||||
const HEARTBEAT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// Version is hardcoded to avoid rollup JSON import issues
|
||||
const APP_VERSION = version;
|
||||
|
||||
export class TelemetryClient {
|
||||
private static instance: TelemetryClient | null = null;
|
||||
|
||||
private client: PostHog | null = null;
|
||||
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private installationId: string | null = null;
|
||||
private nodeId: string | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Start the telemetry client.
|
||||
*
|
||||
* This will initialize the PostHog client, load or create the installation ID and node ID,
|
||||
* capture a startup event, and start a heartbeat interval.
|
||||
*
|
||||
* If telemetry is disabled via `DOCUMENSO_DISABLE_TELEMETRY=true` or credentials are not
|
||||
* provided, this will be a no-op.
|
||||
*/
|
||||
public static async start(): Promise<void> {
|
||||
if (TELEMETRY_DISABLED) {
|
||||
console.log(
|
||||
'[Telemetry] Telemetry is disabled. To enable, remove the DOCUMENSO_DISABLE_TELEMETRY environment variable.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TELEMETRY_KEY || !TELEMETRY_HOST) {
|
||||
console.log('[Telemetry] Telemetry credentials not configured. Telemetry will not be sent.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (TelemetryClient.instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = new TelemetryClient();
|
||||
|
||||
TelemetryClient.instance = instance;
|
||||
|
||||
await instance.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the telemetry client.
|
||||
*
|
||||
* This will clear the heartbeat interval and shutdown the PostHog client.
|
||||
*/
|
||||
public static async stop(): Promise<void> {
|
||||
const instance = TelemetryClient.instance;
|
||||
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.heartbeatInterval) {
|
||||
clearInterval(instance.heartbeatInterval);
|
||||
}
|
||||
|
||||
if (instance.client) {
|
||||
await instance.client.shutdown();
|
||||
}
|
||||
|
||||
TelemetryClient.instance = null;
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
this.client = new PostHog(TELEMETRY_KEY!, {
|
||||
host: TELEMETRY_HOST,
|
||||
disableGeoip: false,
|
||||
});
|
||||
|
||||
// Load or create IDs
|
||||
this.installationId = await this.getOrCreateInstallationId();
|
||||
this.nodeId = await this.getOrCreateNodeId();
|
||||
|
||||
console.log(
|
||||
'[Telemetry] Telemetry is enabled. Documenso collects anonymous usage data to help improve the product.',
|
||||
);
|
||||
console.log(
|
||||
'[Telemetry] We collect: app version, installation ID, and node ID. No personal data, document contents, or user information is collected.',
|
||||
);
|
||||
console.log(
|
||||
'[Telemetry] To disable telemetry, set DOCUMENSO_DISABLE_TELEMETRY=true in your environment variables.',
|
||||
);
|
||||
console.log(
|
||||
'[Telemetry] Learn more: https://documenso.com/docs/developers/self-hosting/telemetry',
|
||||
);
|
||||
|
||||
// Capture startup event
|
||||
this.captureEvent('telemetry_selfhoster_startup');
|
||||
|
||||
// Start heartbeat
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
this.captureEvent('telemetry_selfhoster_heartbeat');
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
private captureEvent(event: string): void {
|
||||
if (!this.client || !this.installationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.capture({
|
||||
distinctId: this.installationId,
|
||||
event,
|
||||
properties: {
|
||||
appVersion: APP_VERSION,
|
||||
installationId: this.installationId,
|
||||
nodeId: this.nodeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async getOrCreateInstallationId(): Promise<string> {
|
||||
try {
|
||||
// Try to get from site settings
|
||||
const existing = await getSiteSetting({ id: SITE_SETTINGS_TELEMETRY_ID }).catch(() => null);
|
||||
|
||||
if (existing) {
|
||||
if (existing.data.installationId) {
|
||||
return existing.data.installationId;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new installation ID
|
||||
const installationId = prefixedId('installation');
|
||||
|
||||
await upsertSiteSetting({
|
||||
id: SITE_SETTINGS_TELEMETRY_ID,
|
||||
data: { installationId },
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
return installationId;
|
||||
} catch {
|
||||
// If database is not available, generate a temporary ID
|
||||
return prefixedId('installation');
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrCreateNodeId(): Promise<string | null> {
|
||||
const nodeIdPath = path.join(os.tmpdir(), NODE_ID_FILENAME);
|
||||
|
||||
try {
|
||||
const existingId = await fs.readFile(nodeIdPath, 'utf-8');
|
||||
|
||||
if (existingId.trim()) {
|
||||
return existingId.trim();
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or can't be read, continue to create
|
||||
}
|
||||
|
||||
// Generate new node ID
|
||||
const nodeId = prefixedId('node');
|
||||
|
||||
try {
|
||||
await fs.writeFile(nodeIdPath, nodeId, 'utf-8');
|
||||
} catch {
|
||||
// Read-only filesystem, use memory for nodeId
|
||||
}
|
||||
|
||||
return nodeId;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export type HandlerTriggerWebhooksResponse =
|
||||
error: string;
|
||||
};
|
||||
|
||||
// Todo: [Webhooks] delete after deployment.
|
||||
export const handlerTriggerWebhooks = async (req: Request) => {
|
||||
const signature = req.headers.get('x-webhook-signature');
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../../constants/app';
|
||||
import { sign } from '../../crypto/sign';
|
||||
import { jobs } from '../../../jobs/client';
|
||||
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
|
||||
|
||||
export type TriggerWebhookOptions = {
|
||||
@@ -13,35 +12,26 @@ export type TriggerWebhookOptions = {
|
||||
|
||||
export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWebhookOptions) => {
|
||||
try {
|
||||
const body = {
|
||||
event,
|
||||
data,
|
||||
userId,
|
||||
teamId,
|
||||
};
|
||||
|
||||
const registeredWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
|
||||
|
||||
if (registeredWebhooks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = sign(body);
|
||||
|
||||
await Promise.race([
|
||||
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/webhook/trigger`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-webhook-signature': signature,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
await Promise.allSettled(
|
||||
registeredWebhooks.map(async (webhook) => {
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.execute-webhook',
|
||||
payload: {
|
||||
event,
|
||||
webhookId: webhook.id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
}),
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Request timeout')), 500);
|
||||
}),
|
||||
]).catch(() => null);
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error(`Failed to trigger webhook`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
||||
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/ciphers/utils';
|
||||
import { managedNonce } from '@noble/ciphers/webcrypto/utils';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { managedNonce } from '@noble/ciphers/webcrypto';
|
||||
import { sha256 } from '@noble/hashes/sha2';
|
||||
|
||||
export type SymmetricEncryptOptions = {
|
||||
key: string;
|
||||
|
||||
@@ -96,6 +96,7 @@ const createFieldSignature = (
|
||||
|
||||
img.onload = () => {
|
||||
image.setAttrs({
|
||||
image: img,
|
||||
...getImageDimensions(img, fieldWidth, fieldHeight),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import type { RenderOptions } from '@documenso/email/render';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { renderWithI18N } from '@documenso/email/render';
|
||||
|
||||
import { getI18nInstance } from '../client-only/providers/i18n-server';
|
||||
import {
|
||||
@@ -26,7 +24,7 @@ export const renderEmailWithI18N = async (
|
||||
|
||||
i18n.activate(lang);
|
||||
|
||||
return render(<I18nProvider i18n={i18n}>{component}</I18nProvider>, otherOptions);
|
||||
return renderWithI18N(component, { i18n, ...otherOptions });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error('Failed to render email');
|
||||
|
||||
Reference in New Issue
Block a user