mirror of
https://github.com/documenso/documenso.git
synced 2026-07-08 20:14:58 +10:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b1e3d197b | |||
| a276e18e1f |
@@ -76,6 +76,8 @@ The Enterprise Edition is required when you:
|
||||
4. Restart your Documenso instance
|
||||
5. Verify the license is active in the **Admin Panel** under the **Stats** section
|
||||
|
||||
See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed.
|
||||
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
@@ -197,7 +199,7 @@ See [Support](/docs/policies/support) for complete support options.
|
||||
1. Sign the Enterprise license agreement
|
||||
2. Receive license key and access credentials
|
||||
3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud
|
||||
4. Configure Enterprise features with support assistance
|
||||
4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
@@ -238,6 +240,7 @@ See [Support](/docs/policies/support) for complete support options.
|
||||
|
||||
## Related
|
||||
|
||||
- [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation
|
||||
- [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license
|
||||
- [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ
|
||||
- [Support](/docs/policies/support) - Support channels and response times
|
||||
|
||||
@@ -443,11 +443,11 @@ Telemetry collects only: app version, installation ID, and node ID. No personal
|
||||
|
||||
## Enterprise Features
|
||||
|
||||
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance.
|
||||
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup.
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------ | ------------------------------------------------ |
|
||||
| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features |
|
||||
| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it |
|
||||
| `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing |
|
||||
| `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret |
|
||||
| `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification |
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Apply Your License Key
|
||||
description: Activate your Enterprise license key to unlock enterprise features on your self-hosted instance.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance.
|
||||
|
||||
<Callout type="info">
|
||||
The license key applies to your **whole instance**, not an individual user account. There's one
|
||||
key per deployment.
|
||||
</Callout>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an
|
||||
Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com).
|
||||
See [Enterprise Edition](/docs/policies/enterprise-edition) for details.
|
||||
- A running self-hosted Documenso instance that you're able to restart
|
||||
|
||||
## Step 1: Set the environment variable
|
||||
|
||||
Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key.
|
||||
|
||||
<Tabs items={['Docker Compose', 'docker run', '.env']}>
|
||||
<Tab value="Docker Compose">
|
||||
|
||||
Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`):
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
|
||||
```
|
||||
|
||||
Then apply it:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="docker run">
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name documenso \
|
||||
-e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \
|
||||
documenso/documenso:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value=".env">
|
||||
|
||||
If you're running Documenso directly (not in a container), add the variable to your `.env` file:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Step 2: Restart the instance
|
||||
|
||||
The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts.
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose restart documenso
|
||||
|
||||
# Docker
|
||||
docker restart documenso
|
||||
```
|
||||
|
||||
On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out.
|
||||
|
||||
## What the license enables
|
||||
|
||||
A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature:
|
||||
|
||||
- **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup.
|
||||
- **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="Enterprise features are still unavailable after applying the key">
|
||||
- Confirm the key is present in the environment the running process actually reads — `docker
|
||||
exec` into the container and check `env | grep LICENSE` if unsure.
|
||||
- Confirm the instance was fully restarted after the variable was set, not just reloaded.
|
||||
- Re-copy the key to rule out truncation or accidental whitespace.
|
||||
</Accordion>
|
||||
<Accordion title="A specific feature still isn't working">
|
||||
Instance-wide features (like CSC signing) also need their own configuration — an active license
|
||||
alone isn't enough. Check that feature's guide to confirm the required settings are in place.
|
||||
Per-organisation features additionally need to be provisioned for the organisation that's using
|
||||
them.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
## See Also
|
||||
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
|
||||
- [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license
|
||||
- [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing
|
||||
@@ -2,6 +2,7 @@
|
||||
"title": "Configuration",
|
||||
"pages": [
|
||||
"environment",
|
||||
"license",
|
||||
"database",
|
||||
"email",
|
||||
"storage",
|
||||
|
||||
@@ -49,7 +49,7 @@ The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL`
|
||||
|
||||
### Enterprise Edition license
|
||||
|
||||
CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`).
|
||||
CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
@@ -141,7 +141,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for
|
||||
|
||||
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
|
||||
|
||||
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison.
|
||||
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE, type TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match, P } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
export type AuditLogDataTableProps = {
|
||||
logs: TDocumentAuditLog[];
|
||||
};
|
||||
|
||||
const dateFormat: DateTimeFormatOptions = {
|
||||
...DateTime.DATETIME_SHORT,
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the color indicator for the audit log type
|
||||
*/
|
||||
|
||||
const getAuditLogIndicatorColor = (type: string) =>
|
||||
match(type)
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => 'bg-green-500')
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => 'bg-red-500')
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, () => 'bg-orange-500')
|
||||
.with(
|
||||
P.union(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED),
|
||||
() => 'bg-blue-500',
|
||||
)
|
||||
.otherwise(() => 'bg-muted');
|
||||
|
||||
/**
|
||||
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
|
||||
*/
|
||||
|
||||
const formatUserAgent = (userAgent: string, userAgentInfo: UAParser.IResult) => {
|
||||
const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UAParser.IResult) => {
|
||||
if (!userAgent) {
|
||||
return msg`N/A`;
|
||||
}
|
||||
|
||||
const browser = userAgentInfo.browser.name;
|
||||
const version = userAgentInfo.browser.version;
|
||||
const os = userAgentInfo.os.name;
|
||||
@@ -35,45 +63,74 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
||||
const parser = new UAParser();
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-border">
|
||||
<div className="space-y-4">
|
||||
{logs.map((log, index) => {
|
||||
parser.setUA(log.userAgent || '');
|
||||
const formattedAction = formatDocumentAuditLogAction(i18n, log);
|
||||
const userAgentInfo = parser.getResult();
|
||||
|
||||
const createdAt = DateTime.fromJSDate(log.createdAt).setLocale(APP_I18N_OPTIONS.defaultLocale);
|
||||
|
||||
const metaSegments = [
|
||||
log.email,
|
||||
log.ipAddress,
|
||||
log.userAgent ? _(formatUserAgent(log.userAgent, userAgentInfo)) : null,
|
||||
].filter((segment): segment is string => Boolean(segment));
|
||||
|
||||
return (
|
||||
<div
|
||||
<Card
|
||||
key={index}
|
||||
className="flex gap-5 py-2.5"
|
||||
// Add top margin for the first card to ensure it's not cut off from the 2nd page onwards
|
||||
className={`border shadow-sm ${index > 0 ? 'print:mt-8' : ''}`}
|
||||
style={{
|
||||
pageBreakInside: 'avoid',
|
||||
breakInside: 'avoid',
|
||||
}}
|
||||
>
|
||||
<div className="w-[5.5rem] shrink-0">
|
||||
<div className="text-foreground text-xs">{createdAt.toFormat('yyyy-MM-dd')}</div>
|
||||
<CardContent className="p-4">
|
||||
{/* Header Section with indicator, event type, and timestamp */}
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<div className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))} />
|
||||
|
||||
<div className="mt-0.5 text-[0.6875rem] text-muted-foreground">{createdAt.toFormat('hh:mm:ss a')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-sm uppercase tracking-wide print:text-[8pt]">
|
||||
{log.type.replace(/_/g, ' ')}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-pretty text-foreground text-sm print:text-xs">{formattedAction.description}</div>
|
||||
|
||||
{metaSegments.length > 0 && (
|
||||
<div className="mt-1 break-words text-muted-foreground text-xs print:text-[0.6875rem]">
|
||||
{metaSegments.join(' · ')}
|
||||
<div className="font-medium text-foreground text-sm print:text-[8pt]">
|
||||
{formattedAction.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-sm print:text-[8pt]">
|
||||
{DateTime.fromJSDate(log.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toLocaleString(dateFormat)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
{/* Details Section - Two column layout */}
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">{_(msg`User`)}</div>
|
||||
|
||||
<div className="mt-1 font-mono text-foreground">{log.email || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
|
||||
{_(msg`IP Address`)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 font-mono text-foreground">{log.ipAddress || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
|
||||
{_(msg`User Agent`)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-foreground">{_(formatUserAgent(log.userAgent, userAgentInfo))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
@@ -105,86 +106,82 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
return (
|
||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
||||
<header>
|
||||
<h1 className="font-semibold text-lg tracking-tight">{_(msg`Audit Log`)}</h1>
|
||||
<div className="mb-6 border-b pb-4">
|
||||
<h1 className="font-semibold text-xl">{_(msg`Audit Log`)}</h1>
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-pretty text-muted-foreground text-sm">{document.title}</p>
|
||||
</header>
|
||||
<Card>
|
||||
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Envelope ID`)}</span>
|
||||
|
||||
<dl className="mt-6 grid grid-cols-2 gap-x-8 gap-y-5">
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Envelope ID`)}</dt>
|
||||
<span className="mt-1 block break-words">{document.envelopeId}</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 break-all text-foreground text-sm print:text-xs">{document.envelopeId}</dd>
|
||||
</div>
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Enclosed Document`)}</span>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Owner`)}</dt>
|
||||
<span className="mt-1 block break-words">{document.title}</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">
|
||||
{document.user.name} ({document.user.email})
|
||||
</dd>
|
||||
</div>
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Status`)}</span>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Status`)}</dt>
|
||||
<span className="mt-1 block">
|
||||
{_(document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description).toUpperCase()}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
{_(document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description)}
|
||||
</dd>
|
||||
</div>
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Owner`)}</span>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Time Zone`)}</dt>
|
||||
<span className="mt-1 block break-words">
|
||||
{document.user.name} ({document.user.email})
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">
|
||||
{document.documentMeta?.timezone ?? 'N/A'}
|
||||
</dd>
|
||||
</div>
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Created At`)}</span>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Created At`)}</dt>
|
||||
<span className="mt-1 block">
|
||||
{DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
{DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</dd>
|
||||
</div>
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Last Updated`)}</span>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Last Updated`)}</dt>
|
||||
<span className="mt-1 block">
|
||||
{DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
{DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</dd>
|
||||
</div>
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Time Zone`)}</span>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Enclosed Document`)}</dt>
|
||||
<span className="mt-1 block break-words">{document.documentMeta?.timezone ?? 'N/A'}</span>
|
||||
</p>
|
||||
|
||||
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">{document.title}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{_(msg`Recipients`)}</p>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Recipients`)}</dt>
|
||||
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
<ul className="space-y-0.5">
|
||||
<ul className="mt-1 list-inside list-disc">
|
||||
{document.recipients.map((recipient) => (
|
||||
<li key={recipient.id} className="break-words">
|
||||
{recipient.name} ({recipient.email}) ·{' '}
|
||||
<li key={recipient.id}>
|
||||
<span className="text-muted-foreground">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</span>
|
||||
[{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]
|
||||
</span>{' '}
|
||||
{recipient.name} ({recipient.email})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8">
|
||||
<InternalAuditLogTable logs={auditLogs} />
|
||||
|
||||
@@ -9,8 +9,11 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@documenso/ui/primitives/table';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType, FieldType, SigningStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { redirect } from 'react-router';
|
||||
@@ -200,178 +203,165 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
|
||||
return (
|
||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
||||
<header>
|
||||
<h1 className="font-semibold text-lg tracking-tight">{_(msg`Signing Certificate`)}</h1>
|
||||
<div className="flex items-center">
|
||||
<h1 className="my-8 font-bold text-2xl">{_(msg`Signing Certificate`)}</h1>
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-pretty text-muted-foreground text-sm">{document.title}</p>
|
||||
</header>
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table overflowHidden>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{_(msg`Signer Events`)}</TableHead>
|
||||
<TableHead>{_(msg`Signature`)}</TableHead>
|
||||
<TableHead>{_(msg`Details`)}</TableHead>
|
||||
{/* <TableHead>Security</TableHead> */}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<table className="mt-6 w-full">
|
||||
<thead>
|
||||
<tr className="border-border border-b">
|
||||
<th className="w-[30%] pr-4 pb-2 text-left font-medium text-muted-foreground text-xs">
|
||||
{_(msg`Signer Events`)}
|
||||
</th>
|
||||
<TableBody className="print:text-xs">
|
||||
{document.recipients.map((recipient, i) => {
|
||||
const logs = getRecipientAuditLogs(recipient.id);
|
||||
const signature = getRecipientSignatureField(recipient.id);
|
||||
|
||||
<th className="w-[30%] pr-4 pb-2 text-left font-medium text-muted-foreground text-xs">
|
||||
{_(msg`Signature`)}
|
||||
</th>
|
||||
return (
|
||||
<TableRow key={i} className="print:break-inside-avoid">
|
||||
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
||||
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
||||
<div className="break-all">{recipient.email}</div>
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</p>
|
||||
|
||||
<th className="pb-2 text-left font-medium text-muted-foreground text-xs">{_(msg`Details`)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
|
||||
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<tbody className="divide-y divide-border">
|
||||
{document.recipients.map((recipient, i) => {
|
||||
const logs = getRecipientAuditLogs(recipient.id);
|
||||
const signature = getRecipientSignatureField(recipient.id);
|
||||
const isRejected = Boolean(logs.DOCUMENT_RECIPIENT_REJECTED[0]);
|
||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||
{signature ? (
|
||||
<>
|
||||
<div
|
||||
className="inline-block rounded-lg p-1"
|
||||
style={{
|
||||
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
|
||||
}}
|
||||
>
|
||||
{signature.signature?.signatureImageAsBase64 && (
|
||||
<img
|
||||
src={`${signature.signature?.signatureImageAsBase64}`}
|
||||
alt="Signature"
|
||||
className="max-h-12 max-w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
return (
|
||||
<tr key={i} className="align-top print:break-inside-avoid">
|
||||
<td className="py-3 pr-4">
|
||||
<div className="hyphens-auto break-words font-medium text-sm print:text-xs">{recipient.name}</div>
|
||||
{signature.signature?.typedSignature && (
|
||||
<p className="text-center font-signature text-sm">
|
||||
{signature.signature?.typedSignature}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="break-all text-muted-foreground text-xs">{recipient.email}</div>
|
||||
|
||||
<div className="mt-0.5 text-muted-foreground text-xs">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5">
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Authentication Level`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{getAuthenticationLevel(recipient.id)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-3 pr-4">
|
||||
{signature ? (
|
||||
<div className="space-y-2.5">
|
||||
{!isRejected && (
|
||||
<div
|
||||
className="inline-block rounded-lg p-1"
|
||||
style={{
|
||||
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
|
||||
}}
|
||||
>
|
||||
{signature.signature?.signatureImageAsBase64 && (
|
||||
<img
|
||||
src={`${signature.signature?.signatureImageAsBase64}`}
|
||||
alt="Signature"
|
||||
className="max-h-12 max-w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{signature.signature?.typedSignature && (
|
||||
<p className="text-center font-signature text-sm">{signature.signature?.typedSignature}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
|
||||
<span className="block font-mono uppercase">{signature.secondaryId}</span>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
<Trans>N/A</Trans>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Signature ID`)}</div>
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="mt-0.5 break-all font-mono text-xs uppercase">{signature.secondaryId}</div>
|
||||
<p className="mt-1 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||
<div className="space-y-1">
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.EMAIL_SENT[0]
|
||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: logs.DOCUMENT_SENT[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_OPENED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{recipient.signingStatus === SigningStatus.REJECTED
|
||||
? recipient.rejectionReason
|
||||
: _(
|
||||
isOwner(recipient.email)
|
||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
||||
: FRIENDLY_SIGNING_REASONS[recipient.role],
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">{_(msg`N/A`)}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2.5">
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`IP Address`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5">
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Device`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-3">
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Sent`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.EMAIL_SENT[0]
|
||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: logs.DOCUMENT_SENT[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Viewed`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.DOCUMENT_OPENED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||
<div>
|
||||
<div className="font-medium text-red-600 text-xs">{_(msg`Rejected`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-red-600 text-sm print:text-xs">
|
||||
{DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Signed`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Reason`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{recipient.signingStatus === SigningStatus.REJECTED
|
||||
? recipient.rejectionReason
|
||||
: _(
|
||||
isOwner(recipient.email)
|
||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
||||
: FRIENDLY_SIGNING_REASONS[recipient.role],
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="my-8 flex-row-reverse space-y-4">
|
||||
@@ -387,7 +377,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<p className="flex-shrink-0 font-medium text-muted-foreground text-xs">
|
||||
<p className="flex-shrink-0 font-medium text-sm print:text-xs">
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
"@lingui/react": "^5.6.0",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@react-router/serve": "^7.12.0",
|
||||
"@react-router/node": "^7.18.1",
|
||||
"@react-router/serve": "^7.18.1",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tanstack/react-query": "5.90.10",
|
||||
@@ -81,8 +81,8 @@
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.6.0",
|
||||
"@lingui/vite-plugin": "^5.6.0",
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@react-router/remix-routes-option-adapter": "^7.12.0",
|
||||
"@react-router/dev": "^7.18.1",
|
||||
"@react-router/remix-routes-option-adapter": "^7.18.1",
|
||||
"@rollup/plugin-babel": "^6.1.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.9",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
|
||||
Generated
+2048
-1264
File diff suppressed because it is too large
Load Diff
@@ -137,7 +137,6 @@ export const generateCertificatePdf = async (options: GenerateCertificatePdfOpti
|
||||
}),
|
||||
envelopeOwner,
|
||||
envelopeId: envelope.id,
|
||||
envelopeTitle: envelope.title,
|
||||
qrToken: envelope.qrToken,
|
||||
hidePoweredBy: organisationClaim.flags.hidePoweredBy ?? false,
|
||||
pageWidth,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user