Compare commits

...

15 Commits

Author SHA1 Message Date
1a1a30791e v1.12.0 2025-07-03 10:01:03 +10:00
ea1cf481eb chore: extract translations 2025-07-01 21:27:35 +10:00
eda0d5eeb6 fix: open advanced settings when fields are added to templates (#1855) 2025-07-01 21:21:13 +10:00
8da4ab533f fix(add-subject): remove superfluous word (#1866) 2025-07-01 12:34:14 +10:00
8695ef766e v1.12.0-rc.8 2025-06-30 19:47:37 +10:00
7487399123 feat: add more api logs (#1870)
Adds more detailed API logging using Pino
2025-06-30 19:46:32 +10:00
0cc729e9bd feat: add sequential document view logs (#1871)
## Description

Add a new document audit log to detect when the document is viewed. This
should only be visible in the document audit log page

Notes:
1. I wanted to reuse the `DOCUMENT_OPENED` event and add an additional
paramter to track sequential views, but it's not query-able
2. This will log "DOCUMENT_VIEWED" before "DOCUMENT_OPENED" but i don't
think it matters
2025-06-30 19:11:16 +10:00
58d97518c8 v1.12.0-rc.7 2025-06-27 22:17:45 +10:00
20c8969272 fix: get real ip for rate limit key 2025-06-27 22:17:02 +10:00
85ac65e405 v1.12.0-rc.6 2025-06-27 21:46:16 +10:00
e07a497b69 feat: api logging by pino (#1865)
experiemental
2025-06-27 21:44:51 +10:00
21dc4eee62 v1.12.0-rc.5 2025-06-27 18:53:45 +10:00
dc2042a1ee fix: rate limit api endpoints (#1863)
Rate limit API endpoint
2025-06-27 18:50:22 +10:00
bb9ba80edb fix: duplicate fields and recipients when you duplicate a document (#1852) 2025-06-23 16:43:07 +10:00
bfe8c674f2 fix: globalAccessAuth error (#1851) 2025-06-23 10:10:57 +10:00
99 changed files with 1973 additions and 1177 deletions

View File

@ -127,4 +127,6 @@ E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# [[LOGGER]]
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY=
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
NEXT_PRIVATE_LOGGER_FILE_PATH=

3
.gitignore vendored
View File

@ -50,3 +50,6 @@ yarn-error.log*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# logs
logs.json

View File

@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
@ -12,6 +13,7 @@ import {
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -175,13 +177,17 @@ export const DocumentEditForm = ({
try {
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
const parsedGlobalAccessAuth = z
.array(ZDocumentAccessAuthTypesSchema)
.safeParse(data.globalAccessAuth);
await updateDocument({
documentId: document.id,
data: {
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? [],
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
globalActionAuth: data.globalActionAuth ?? [],
},
meta: {

View File

@ -1,26 +0,0 @@
import React from 'react';
import { Badge } from '@documenso/ui/primitives/badge';
export type DocumentHistorySheetChangesProps = {
values: {
key: string | React.ReactNode;
value: string | React.ReactNode;
}[];
};
export const DocumentHistorySheetChanges = ({ values }: DocumentHistorySheetChangesProps) => {
return (
<Badge
className="text-muted-foreground mt-3 block w-full space-y-0.5 text-xs"
variant="neutral"
>
{values.map(({ key, value }, i) => (
<p key={typeof key === 'string' ? key : i}>
<span>{key}: </span>
<span className="font-normal">{value}</span>
</p>
))}
</Badge>
);
};

View File

@ -1,410 +0,0 @@
import { useMemo, useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ArrowRightIcon, Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { DOCUMENT_AUDIT_LOG_EMAIL_FORMAT } from '@documenso/lib/constants/document-audit-logs';
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
export type DocumentHistorySheetProps = {
documentId: number;
userId: number;
isMenuOpen?: boolean;
onMenuOpenChange?: (_value: boolean) => void;
children?: React.ReactNode;
};
export const DocumentHistorySheet = ({
documentId,
userId,
isMenuOpen,
onMenuOpenChange,
children,
}: DocumentHistorySheetProps) => {
const { _, i18n } = useLingui();
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
const {
data,
isLoading,
isLoadingError,
refetch,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
{
documentId,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: (previousData) => previousData,
},
);
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
const extractBrowser = (userAgent?: string | null) => {
if (!userAgent) {
return 'Unknown';
}
const parser = new UAParser(userAgent);
parser.setUA(userAgent);
const result = parser.getResult();
return result.browser.name;
};
/**
* Applies the following formatting for a given text:
* - Uppercase first lower, lowercase rest
* - Replace _ with spaces
*
* @param text The text to format
* @returns The formatted text
*/
const formatGenericText = (text?: string | string[] | null): string => {
if (!text) {
return '';
}
if (Array.isArray(text)) {
return text.map((t) => formatGenericText(t)).join(', ');
}
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
};
return (
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
{children && <SheetTrigger asChild>{children}</SheetTrigger>}
<SheetContent
sheetClass="backdrop-blur-none"
className="flex w-full max-w-[500px] flex-col overflow-y-auto p-0"
>
<div className="text-foreground px-6 pt-6">
<h1 className="text-lg font-medium">
<Trans>Document history</Trans>
</h1>
<button
className="text-muted-foreground text-sm"
onClick={() => setIsUserDetailsVisible(!isUserDetailsVisible)}
>
{isUserDetailsVisible ? (
<Trans>Hide additional information</Trans>
) : (
<Trans>Show additional information</Trans>
)}
</button>
</div>
{isLoading && (
<div className="flex h-full items-center justify-center">
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
</div>
)}
{isLoadingError && (
<div className="flex h-full flex-col items-center justify-center">
<p className="text-foreground/80 text-sm">
<Trans>Unable to load document history</Trans>
</p>
<button
onClick={async () => refetch()}
className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
>
<Trans>Click here to retry</Trans>
</button>
</div>
)}
{data && (
<ul
className={cn('divide-y border-t', {
'mb-4 border-b': !hasNextPage,
})}
>
{documentAuditLogs.map((auditLog) => (
<li className="px-4 py-2.5" key={auditLog.id}>
<div className="flex flex-row items-center">
<Avatar className="mr-2 h-9 w-9">
<AvatarFallback className="text-xs text-gray-400">
{(auditLog?.email ?? auditLog?.name ?? '?').slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="text-foreground text-xs font-bold">
{formatDocumentAuditLogAction(_, auditLog, userId).description}
</p>
<p className="text-foreground/50 text-xs">
{DateTime.fromJSDate(auditLog.createdAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toFormat('d MMM, yyyy HH:MM a')}
</p>
</div>
</div>
{match(auditLog)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM },
() => null,
)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED },
({ data }) => {
const values = [
{
key: 'Email',
value: data.recipientEmail,
},
{
key: 'Role',
value: formatGenericText(data.recipientRole),
},
];
// Insert the name to the start of the array if available.
if (data.recipientName) {
values.unshift({
key: 'Name',
value: data.recipientName,
});
}
return <DocumentHistorySheetChanges values={values} />;
},
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, ({ data }) => {
if (data.changes.length === 0) {
return null;
}
return (
<DocumentHistorySheetChanges
values={data.changes.map(({ type, from, to }) => ({
key: formatGenericText(type),
value: (
<span className="inline-flex flex-row items-center">
<span>{type === 'ROLE' ? formatGenericText(from) : from}</span>
<ArrowRightIcon className="h-4 w-4" />
<span>{type === 'ROLE' ? formatGenericText(to) : to}</span>
</span>
),
}))}
/>
);
})
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field',
value: formatGenericText(data.fieldType),
},
{
key: 'Recipient',
value: formatGenericText(data.fieldRecipientEmail),
},
]}
/>
),
)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: Array.isArray(data.from)
? data.from
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
.join(', ')
: DOCUMENT_AUTH_TYPES[data.from || '']?.value || 'None',
},
{
key: 'New',
value: Array.isArray(data.to)
? data.to
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
.join(', ')
: DOCUMENT_AUTH_TYPES[data.to || '']?.value || 'None',
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, ({ data }) => {
if (data.changes.length === 0) {
return null;
}
return (
<DocumentHistorySheetChanges
values={data.changes.map((change) => ({
key: formatGenericText(change.type),
value: change.type === 'PASSWORD' ? '*********' : change.to,
}))}
/>
);
})
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field inserted',
value: formatGenericText(data.field.type),
},
]}
/>
))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field uninserted',
value: formatGenericText(data.field),
},
]}
/>
))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Type',
value: DOCUMENT_AUDIT_LOG_EMAIL_FORMAT[data.emailType].description,
},
{
key: 'Sent to',
value: data.recipientEmail,
},
]}
/>
))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field prefilled',
value: formatGenericText(data.field.type),
},
]}
/>
))
.exhaustive()}
{isUserDetailsVisible && (
<>
<div className="mb-1 mt-2 flex flex-row space-x-2">
<Badge variant="neutral" className="text-muted-foreground">
IP: {auditLog.ipAddress ?? 'Unknown'}
</Badge>
<Badge variant="neutral" className="text-muted-foreground">
Browser: {extractBrowser(auditLog.userAgent)}
</Badge>
</div>
</>
)}
</li>
))}
{hasNextPage && (
<div className="flex items-center justify-center py-4">
<Button
variant="outline"
loading={isFetchingNextPage}
onClick={async () => fetchNextPage()}
>
Show more
</Button>
</div>
)}
</ul>
)}
</SheetContent>
</Sheet>
);
};

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
@ -10,6 +11,7 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
import type { TTemplate } from '@documenso/lib/types/template';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -125,6 +127,10 @@ export const TemplateEditForm = ({
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
const { signatureTypes } = data.meta;
const parsedGlobalAccessAuth = z
.array(ZDocumentAccessAuthTypesSchema)
.safeParse(data.globalAccessAuth);
try {
await updateTemplateSettings({
templateId: template.id,
@ -132,7 +138,7 @@ export const TemplateEditForm = ({
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? [],
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
globalActionAuth: data.globalActionAuth ?? [],
},
meta: {

View File

@ -1,7 +1,7 @@
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern';
@ -13,11 +13,9 @@ import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { DocumentHistorySheet } from '~/components/general/document/document-history-sheet';
import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
import { DocumentPageViewDropdown } from '~/components/general/document/document-page-view-dropdown';
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
@ -101,9 +99,6 @@ export default function DocumentPage() {
const { recipients, documentData, documentMeta } = document;
// This was a feature flag. Leave to false since it's not ready.
const isDocumentHistoryEnabled = false;
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && (
@ -154,17 +149,6 @@ export default function DocumentPage() {
)}
</div>
</div>
{isDocumentHistoryEnabled && (
<div className="self-end">
<DocumentHistorySheet documentId={document.id} userId={user.id}>
<Button variant="outline">
<Clock9 className="mr-1.5 h-4 w-4" />
<Trans>Document history</Trans>
</Button>
</DocumentHistorySheet>
</div>
)}
</div>
<div className="mt-6 grid w-full grid-cols-12 gap-8">

View File

@ -41,6 +41,7 @@
"colord": "^2.9.3",
"framer-motion": "^10.12.8",
"hono": "4.7.0",
"hono-rate-limiter": "^0.4.2",
"hono-react-router-adapter": "^0.6.2",
"input-otp": "^1.2.4",
"isbot": "^5.1.17",
@ -100,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.0-rc.4"
"version": "1.12.0"
}

View File

@ -1,10 +1,16 @@
import { Hono } from 'hono';
import { rateLimiter } from 'hono-rate-limiter';
import { contextStorage } from 'hono/context-storage';
import { requestId } from 'hono/request-id';
import type { RequestIdVariables } from 'hono/request-id';
import type { Logger } from 'pino';
import { tsRestHonoApp } from '@documenso/api/hono';
import { auth } from '@documenso/auth/server';
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
import { jobsClient } from '@documenso/lib/jobs/client';
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
import { logger } from '@documenso/lib/utils/logger';
import { openApiDocument } from '@documenso/trpc/server/open-api';
import { filesRoute } from './api/files';
@ -14,13 +20,33 @@ import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
export interface HonoEnv {
Variables: {
Variables: RequestIdVariables & {
context: AppContext;
logger: Logger;
};
}
const app = new Hono<HonoEnv>();
/**
* Rate limiting for v1 and v2 API routes only.
* - 100 requests per minute per IP address
*/
const rateLimitMiddleware = rateLimiter({
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per window
keyGenerator: (c) => {
try {
return getIpAddress(c.req.raw);
} catch (error) {
return 'unknown';
}
},
message: {
error: 'Too many requests, please try again later.',
},
});
/**
* Attach session and context to requests.
*/
@ -31,6 +57,24 @@ app.use(appContext);
* RR7 app middleware.
*/
app.use('*', appMiddleware);
app.use('*', requestId());
app.use(async (c, next) => {
const metadata = c.get('context').requestMetadata;
const honoLogger = logger.child({
requestId: c.var.requestId,
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
});
c.set('logger', honoLogger);
await next();
});
// Apply rate limit to /api/v1/*
app.use('/api/v1/*', rateLimitMiddleware);
app.use('/api/v2/*', rateLimitMiddleware);
// Auth server.
app.route('/api/auth', auth);

345
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.0-rc.4",
"version": "1.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.0-rc.4",
"version": "1.12.0",
"workspaces": [
"apps/*",
"packages/*"
@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.0-rc.4",
"version": "1.12.0",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
@ -118,6 +118,7 @@
"colord": "^2.9.3",
"framer-motion": "^10.12.8",
"hono": "4.7.0",
"hono-rate-limiter": "^0.4.2",
"hono-react-router-adapter": "^0.6.2",
"input-otp": "^1.2.4",
"isbot": "^5.1.17",
@ -3072,36 +3073,6 @@
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
"license": "MIT"
},
"node_modules/@honeybadger-io/core": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@honeybadger-io/core/-/core-6.7.0.tgz",
"integrity": "sha512-bEXRe2UVbfr9q3434/2eO3AHguUT0froYEqrHfTPphR4Aw6+AlFac0YE8elqDZqUSgRQ6m1OXqxmq/HOF+W6LQ==",
"license": "MIT",
"dependencies": {
"json-nd": "^1.0.0",
"stacktrace-parser": "^0.1.10"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@honeybadger-io/js": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@honeybadger-io/js/-/js-6.11.0.tgz",
"integrity": "sha512-nSibKUr9ccrs6Jb3Ql7uO/4MdEEv3ONGP1CrD0w3zSMHUvQKHe43NPYfESA7btxjrf9PVeV+m6ETP/193BSILg==",
"license": "MIT",
"dependencies": {
"@honeybadger-io/core": "^6.7.0",
"@types/aws-lambda": "^8.10.89",
"@types/express": "^4.17.13"
},
"bin": {
"honeybadger-checkins-sync": "scripts/check-ins-sync-bin.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@hono/node-server": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz",
@ -11714,16 +11685,6 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/bunyan": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz",
@ -11844,30 +11805,6 @@
"@types/estree": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/formidable": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz",
@ -11897,12 +11834,6 @@
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@ -11976,12 +11907,6 @@
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"license": "MIT"
},
"node_modules/@types/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@ -12069,12 +11994,6 @@
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"license": "MIT"
},
"node_modules/@types/ramda": {
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.2.tgz",
@ -12084,12 +12003,6 @@
"types-ramda": "^0.30.1"
}
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
@ -12123,27 +12036,6 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@types/shimmer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
@ -13311,6 +13203,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
@ -14936,7 +14837,6 @@
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/colors": {
@ -16118,6 +16018,15 @@
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@ -18240,6 +18149,12 @@
"node": ">=4"
}
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -18308,6 +18223,21 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"license": "MIT"
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -20241,6 +20171,15 @@
"node": ">=16.9.0"
}
},
"node_modules/hono-rate-limiter": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/hono-rate-limiter/-/hono-rate-limiter-0.4.2.tgz",
"integrity": "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw==",
"license": "MIT",
"peerDependencies": {
"hono": "^4.1.1"
}
},
"node_modules/hono-react-router-adapter": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/hono-react-router-adapter/-/hono-react-router-adapter-0.6.5.tgz",
@ -21643,7 +21582,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -21852,12 +21790,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-nd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-nd/-/json-nd-1.0.0.tgz",
"integrity": "sha512-8TIp0HZAY0VVrwRQJJPb4+nOTSPoOWZeEKBTLizUfQO4oym5Fc/MKqN8vEbLCxcyxDf2vwNxOQ1q84O49GWPyQ==",
"license": "BSD-3-Clause"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@ -26074,6 +26006,15 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"license": "MIT"
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -26893,6 +26834,82 @@
"node": ">=0.10.0"
}
},
"node_modules/pino": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz",
"integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-abstract-transport/node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"license": "MIT",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-pretty/node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@ -27770,6 +27787,22 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -28007,6 +28040,12 @@
],
"license": "MIT"
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/quick-lru": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
@ -29398,6 +29437,15 @@
"integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==",
"license": "MIT"
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/recharts": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz",
@ -30449,6 +30497,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -30514,6 +30571,12 @@
"node": ">=4"
}
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
@ -31075,6 +31138,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/sort-keys": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz",
@ -31304,27 +31376,6 @@
"integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
"license": "MIT"
},
"node_modules/stacktrace-parser": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
"integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==",
"license": "MIT",
"dependencies": {
"type-fest": "^0.7.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/stacktrace-parser/node_modules/type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=8"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -32340,6 +32391,15 @@
"node": ">=0.8"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@ -35984,7 +36044,6 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@honeybadger-io/js": "^6.10.1",
"@lingui/core": "^5.2.0",
"@lingui/macro": "^5.2.0",
"@lingui/react": "^5.2.0",
@ -36005,6 +36064,8 @@
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",
"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",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.0-rc.4",
"version": "1.12.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -8,10 +8,12 @@ import { testCredentialsHandler } from '@documenso/lib/server-only/public-api/te
import { listDocumentsHandler } from '@documenso/lib/server-only/webhooks/zapier/list-documents';
import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/subscribe';
import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe';
// This is a bit nasty. Todo: Extract
import type { HonoEnv } from '@documenso/remix/server/router';
// This is bad, ts-router will be created on each request.
// But don't really have a choice here.
export const tsRestHonoApp = new Hono();
export const tsRestHonoApp = new Hono<HonoEnv>();
tsRestHonoApp
.get('/openapi', (c) => c.redirect('https://openapi-v1.documenso.com'))

View File

@ -77,9 +77,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
getDocument: authenticatedMiddleware(async (args, user, team) => {
getDocument: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: documentId } = args.params;
logger.info({
input: {
id: documentId,
},
});
try {
const document = await getDocumentById({
documentId: Number(documentId),
@ -139,10 +145,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
downloadSignedDocument: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: documentId } = args.params;
const { downloadOriginalDocument } = args.query;
logger.info({
input: {
id: documentId,
},
});
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
return {
@ -204,9 +216,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
logger.info({
input: {
id: documentId,
},
});
try {
const document = await getDocumentById({
documentId: Number(documentId),
@ -382,9 +400,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
deleteTemplate: authenticatedMiddleware(async (args, user, team) => {
deleteTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: templateId } = args.params;
logger.info({
input: {
id: templateId,
},
});
try {
const deletedTemplate = await deleteTemplate({
id: Number(templateId),
@ -406,9 +430,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
getTemplate: authenticatedMiddleware(async (args, user, team) => {
getTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: templateId } = args.params;
logger.info({
input: {
id: templateId,
},
});
try {
const template = await getTemplateById({
id: Number(templateId),
@ -463,202 +493,224 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
createDocumentFromTemplate: authenticatedMiddleware(
async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
logger.info({
input: {
templateId: params.templateId,
},
};
}
const templateId = Number(params.templateId);
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const document = await createDocumentFromTemplateLegacy({
templateId,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
});
let documentDataId = document.documentDataId;
if (body.formValues) {
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
documentDataId = newDocumentData.id;
}
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
title: fileName,
externalId: body.externalId || null,
formValues: body.formValues,
documentData: {
connect: {
id: documentDataId,
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
},
},
});
};
}
if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: metadata,
});
}
const templateId = Number(params.templateId);
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let document: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
document = await createDocumentFromTemplate({
const document = await createDocumentFromTemplateLegacy({
templateId,
externalId: body.externalId || null,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
prefillFields: body.prefillFields,
override: {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (body.formValues) {
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
let documentDataId = document.documentDataId;
if (body.formValues) {
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
documentDataId = newDocumentData.id;
}
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
title: fileName,
externalId: body.externalId || null,
formValues: body.formValues,
documentData: {
connect: {
id: newDocumentData.id,
id: documentDataId,
},
},
},
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: metadata,
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
},
),
generateDocumentFromTemplate: authenticatedMiddleware(
async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
logger.info({
input: {
templateId: params.templateId,
},
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let document: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
document = await createDocumentFromTemplate({
templateId,
externalId: body.externalId || null,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
prefillFields: body.prefillFields,
override: {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (body.formValues) {
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
formValues: body.formValues,
documentData: {
connect: {
id: newDocumentData.id,
},
},
},
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
},
),
sendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
const { sendEmail, sendCompletionEmails } = args.body;
logger.info({
input: {
id: documentId,
},
});
try {
const document = await getDocumentById({
documentId: Number(documentId),
@ -730,10 +782,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
resendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
const { recipients } = args.body;
logger.info({
input: {
id: documentId,
},
});
try {
await resendDocument({
userId: user.id,
@ -759,10 +817,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
logger.info({
input: {
id: documentId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -850,10 +914,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
updateRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
updateRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, recipientId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
logger.info({
input: {
id: documentId,
recipientId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -916,9 +987,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
deleteRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, recipientId } = args.params;
logger.info({
input: {
id: documentId,
recipientId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -970,8 +1048,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
createField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
logger.info({
input: {
id: documentId,
},
});
const fields = Array.isArray(args.body) ? args.body : [args.body];
const document = await prisma.document.findFirst({
@ -1131,11 +1216,18 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
updateField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
updateField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, fieldId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
args.body;
logger.info({
input: {
id: documentId,
fieldId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -1222,9 +1314,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
deleteField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, fieldId } = args.params;
logger.info({
input: {
id: documentId,
fieldId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,

View File

@ -1,10 +1,14 @@
import type { Team, User } from '@prisma/client';
import type { TsRestRequest } from '@ts-rest/serverless';
import type { Logger } from 'pino';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { BaseApiLog, RootApiLog } from '@documenso/lib/types/api-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid } from '@documenso/lib/universal/id';
import { logger } from '@documenso/lib/utils/logger';
type B = {
// appRoute: any;
@ -27,10 +31,24 @@ export const authenticatedMiddleware = <
args: T & { req: TsRestRequest },
user: Pick<User, 'id' | 'email' | 'name' | 'disabled'>,
team: Team,
options: { metadata: ApiRequestMetadata },
options: { metadata: ApiRequestMetadata; logger: Logger },
) => Promise<R>,
) => {
return async (args: T, { request }: B) => {
const requestMetadata = extractRequestMetadata(request);
const apiLogger = logger.child({
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
requestId: nanoid(),
} satisfies RootApiLog);
const infoToLog: BaseApiLog = {
auth: 'api',
source: 'apiV1',
path: request.url,
};
try {
const { authorization } = args.headers;
@ -51,8 +69,14 @@ export const authenticatedMiddleware = <
});
}
apiLogger.info({
...infoToLog,
userId: apiToken.user.id,
apiTokenId: apiToken.id,
} satisfies BaseApiLog);
const metadata: ApiRequestMetadata = {
requestMetadata: extractRequestMetadata(request),
requestMetadata,
source: 'apiV1',
auth: 'api',
auditUser: {
@ -69,10 +93,12 @@ export const authenticatedMiddleware = <
},
apiToken.user,
apiToken.team,
{ metadata },
{ metadata, logger: apiLogger },
);
} catch (err) {
console.log({ err: err });
console.log({ err });
apiLogger.info(infoToLog);
let message = 'Unauthorized';

View File

@ -23,11 +23,9 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@honeybadger-io/js": "^6.10.1",
"@lingui/core": "^5.2.0",
"@lingui/macro": "^5.2.0",
"@lingui/react": "^5.2.0",
"jose": "^6.0.0",
"@noble/ciphers": "0.4.0",
"@noble/hashes": "1.3.2",
"@node-rs/bcrypt": "^1.10.0",
@ -37,6 +35,7 @@
"@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",
@ -44,6 +43,8 @@
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",
"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",
@ -59,4 +60,4 @@
"@types/luxon": "^3.3.1",
"@types/pg": "^8.11.4"
}
}
}

View File

@ -1,4 +1,6 @@
import { DocumentSource, type Prisma, WebhookTriggerEvents } from '@prisma/client';
import type { Prisma, Recipient } from '@prisma/client';
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
import { omit } from 'remeda';
import { prisma } from '@documenso/prisma';
@ -7,7 +9,7 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { prefixedId } from '../../universal/id';
import { nanoid, prefixedId } from '../../universal/id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getDocumentWhereInput } from './get-document-by-id';
@ -40,14 +42,16 @@ export const duplicateDocument = async ({
type: true,
},
},
documentMeta: {
authOptions: true,
visibility: true,
documentMeta: true,
recipients: {
select: {
message: true,
subject: true,
dateFormat: true,
password: true,
timezone: true,
redirectUrl: true,
email: true,
name: true,
role: true,
signingOrder: true,
fields: true,
},
},
},
@ -59,56 +63,83 @@ export const duplicateDocument = async ({
});
}
const createDocumentArguments: Prisma.DocumentCreateArgs = {
const documentData = await prisma.documentData.create({
data: {
title: document.title,
qrToken: prefixedId('qr'),
user: {
connect: {
id: document.userId,
},
},
team: {
connect: {
id: teamId,
},
},
documentData: {
create: {
...document.documentData,
data: document.documentData.initialData,
},
},
documentMeta: {
create: {
...document.documentMeta,
},
},
source: DocumentSource.DOCUMENT,
type: document.documentData.type,
data: document.documentData.initialData,
initialData: document.documentData.initialData,
},
};
});
if (teamId !== undefined) {
createDocumentArguments.data.team = {
connect: {
id: teamId,
let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined;
if (document.documentMeta) {
documentMeta = {
create: {
...omit(document.documentMeta, ['id', 'documentId']),
emailSettings: document.documentMeta.emailSettings || undefined,
},
};
}
const createdDocument = await prisma.document.create({
...createDocumentArguments,
data: {
userId: document.userId,
teamId: teamId,
title: document.title,
documentDataId: documentData.id,
authOptions: document.authOptions || undefined,
visibility: document.visibility,
qrToken: prefixedId('qr'),
documentMeta,
source: DocumentSource.DOCUMENT,
},
include: {
recipients: true,
documentMeta: true,
},
});
const recipientsToCreate = document.recipients.map((recipient) => ({
documentId: createdDocument.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
documentId: createdDocument.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
}));
const recipients: Recipient[] = [];
for (const recipientData of recipientsToCreate) {
const newRecipient = await prisma.recipient.create({
data: recipientData,
});
recipients.push(newRecipient);
}
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: ZWebhookDocumentSchema.parse({
...mapDocumentToWebhookDocumentPayload(createdDocument),
recipients: createdDocument.recipients,
recipients,
documentMeta: createdDocument.documentMeta,
}),
userId: userId,

View File

@ -27,7 +27,6 @@ export const viewedDocument = async ({
const recipient = await prisma.recipient.findFirst({
where: {
token,
readStatus: ReadStatus.NOT_OPENED,
},
});
@ -37,6 +36,30 @@ export const viewedDocument = async ({
const { documentId } = recipient;
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED,
documentId,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata,
data: {
recipientEmail: recipient.email,
recipientId: recipient.id,
recipientName: recipient.name,
recipientRole: recipient.role,
accessAuth: recipientAccessAuth ?? [],
},
}),
});
// Early return if already opened.
if (recipient.readStatus === ReadStatus.OPENED) {
return;
}
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
where: {

View File

@ -23,8 +23,15 @@ export const duplicateTemplate = async ({
team: buildTeamWhereQuery({ teamId, userId }),
},
include: {
recipients: true,
fields: true,
recipients: {
select: {
email: true,
name: true,
role: true,
signingOrder: true,
fields: true,
},
},
templateDocumentData: true,
templateMeta: true,
},
@ -59,13 +66,8 @@ export const duplicateTemplate = async ({
teamId,
title: template.title + ' (copy)',
templateDocumentDataId: documentData.id,
recipients: {
create: template.recipients.map((recipient) => ({
email: recipient.email,
name: recipient.name,
token: nanoid(),
})),
},
authOptions: template.authOptions || undefined,
visibility: template.visibility,
templateMeta,
},
include: {
@ -73,32 +75,36 @@ export const duplicateTemplate = async ({
},
});
await prisma.field.createMany({
data: template.fields.map((field) => {
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
const recipientsToCreate = template.recipients.map((recipient) => ({
templateId: duplicatedTemplate.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
templateId: duplicatedTemplate.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
}));
const duplicatedTemplateRecipient = duplicatedTemplate.recipients.find(
(doc) => doc.email === recipient?.email,
);
if (!duplicatedTemplateRecipient) {
throw new Error('Recipient not found.');
}
return {
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: field.customText,
inserted: field.inserted,
templateId: duplicatedTemplate.id,
recipientId: duplicatedTemplateRecipient.id,
};
}),
});
for (const recipientData of recipientsToCreate) {
await prisma.recipient.create({
data: recipientData,
});
}
return duplicatedTemplate;
};

View File

@ -313,6 +313,10 @@ msgstr "{prefix} hat den Titel des Dokuments aktualisiert"
msgid "{prefix} updated the document visibility"
msgstr "{prefix} hat die Sichtbarkeit des Dokuments aktualisiert"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr ""
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} Dokument"
@ -1767,7 +1771,6 @@ msgstr "Klicken Sie hier, um zu beginnen"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Klicken Sie hier, um es erneut zu versuchen"
@ -2749,11 +2752,6 @@ msgstr "Externe ID des Dokuments aktualisiert"
msgid "Document found in your account"
msgstr "Dokument in Ihrem Konto gefunden"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Dokumentverlauf"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2872,6 +2870,10 @@ msgstr "Dokumenten-Upload deaktiviert aufgrund unbezahlter Rechnungen"
msgid "Document uploaded"
msgstr "Dokument hochgeladen"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Dokument angesehen"
@ -3712,10 +3714,6 @@ msgstr "Hallo, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Ausblenden"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Zusätzliche Informationen ausblenden"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -4139,10 +4137,10 @@ msgstr "Abonnement verwalten"
msgid "Manage the {0} organisation"
msgstr "Verwalten Sie die {0} Organisation"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Verwalten Sie das Abonnement der {0} Organisation"
msgid "Manage the {1} organisation subscription"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -5814,10 +5812,6 @@ msgstr "Teilen Sie Ihre Unterzeichnungserfahrung!"
msgid "Show"
msgstr "Anzeigen"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Zusätzliche Informationen anzeigen"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -7247,7 +7241,6 @@ msgid "Unable to join this organisation at this time."
msgstr "Zurzeit kann dieser Organisation nicht beigetreten werden."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "Kann den Dokumentverlauf nicht laden"
@ -8016,11 +8009,8 @@ msgstr "Wir konnten Ihre E-Mail derzeit nicht verifizieren."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "Wir konnten Ihre E-Mail nicht bestätigen. Wenn Ihre E-Mail noch nicht bestätigt wurde, versuchen Sie es bitte erneut."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "Wir generieren Signierlinks mit Ihnen, die Sie den Empfängern über Ihre bevorzugte Methode senden können."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "Wir werden Unterzeichnungslinks für Sie erstellen, die Sie an die Empfänger über Ihre bevorzugte Methode senden können."

View File

@ -308,6 +308,10 @@ msgstr "{prefix} updated the document title"
msgid "{prefix} updated the document visibility"
msgstr "{prefix} updated the document visibility"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr "{prefix} viewed the document"
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} document"
@ -1762,7 +1766,6 @@ msgstr "Click here to get started"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Click here to retry"
@ -2744,11 +2747,6 @@ msgstr "Document external ID updated"
msgid "Document found in your account"
msgstr "Document found in your account"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Document history"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2867,6 +2865,10 @@ msgstr "Document upload disabled due to unpaid invoices"
msgid "Document uploaded"
msgstr "Document uploaded"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr "Document viewed"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Document Viewed"
@ -3707,10 +3709,6 @@ msgstr "Hi, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Hide"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Hide additional information"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -4134,10 +4132,10 @@ msgstr "Manage subscription"
msgid "Manage the {0} organisation"
msgstr "Manage the {0} organisation"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Manage the {0} organisation subscription"
msgid "Manage the {1} organisation subscription"
msgstr "Manage the {1} organisation subscription"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -5809,10 +5807,6 @@ msgstr "Share your signing experience!"
msgid "Show"
msgstr "Show"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Show additional information"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -7254,7 +7248,6 @@ msgid "Unable to join this organisation at this time."
msgstr "Unable to join this organisation at this time."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "Unable to load document history"
@ -8023,11 +8016,8 @@ msgstr "We were unable to verify your email at this time."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "We were unable to verify your email. If your email is not verified already, please try again."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "We will generate signing links for with you, which you can send to the recipients through your method of choice."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "We will generate signing links for you, which you can send to the recipients through your method of choice."

View File

@ -313,6 +313,10 @@ msgstr "{prefix} actualizó el título del documento"
msgid "{prefix} updated the document visibility"
msgstr "{prefix} actualizó la visibilidad del documento"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr ""
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} documento"
@ -1767,7 +1771,6 @@ msgstr "Haga clic aquí para comenzar"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Haga clic aquí para reintentar"
@ -2749,11 +2752,6 @@ msgstr "ID externo del documento actualizado"
msgid "Document found in your account"
msgstr "Documento encontrado en tu cuenta"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Historial de documentos"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2872,6 +2870,10 @@ msgstr "La carga de documentos está deshabilitada debido a facturas impagadas"
msgid "Document uploaded"
msgstr "Documento subido"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Documento visto"
@ -3712,10 +3714,6 @@ msgstr "Hola, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Ocultar"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Ocultar información adicional"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -4139,10 +4137,10 @@ msgstr "Gestionar suscripción"
msgid "Manage the {0} organisation"
msgstr "Gestionar la organización {0}"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Gestionar la suscripción de la organización {0}"
msgid "Manage the {1} organisation subscription"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -5814,10 +5812,6 @@ msgstr "¡Comparte tu experiencia de firma!"
msgid "Show"
msgstr "Mostrar"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Mostrar información adicional"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -7249,7 +7243,6 @@ msgid "Unable to join this organisation at this time."
msgstr "No se puede unirse a esta organización en este momento."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "No se pudo cargar el historial del documento"
@ -8018,11 +8011,8 @@ msgstr "No pudimos verificar tu correo electrónico en este momento."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "No pudimos verificar tu correo electrónico. Si tu correo electrónico no está verificado ya, por favor inténtalo de nuevo."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "Generaremos enlaces de firma para ti, que podrás enviar a los destinatarios a través de tu método preferido."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "Generaremos enlaces de firma para ti, que podrás enviar a los destinatarios a través de tu método preferido."

View File

@ -313,6 +313,10 @@ msgstr "{prefix} a mis à jour le titre du document"
msgid "{prefix} updated the document visibility"
msgstr "{prefix} a mis à jour la visibilité du document"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr ""
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} document"
@ -1767,7 +1771,6 @@ msgstr "Cliquez ici pour commencer"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Cliquez ici pour réessayer"
@ -2749,11 +2752,6 @@ msgstr "ID externe du document mis à jour"
msgid "Document found in your account"
msgstr "Document trouvé dans votre compte"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Historique du document"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2872,6 +2870,10 @@ msgstr "Importation de documents désactivé en raison de factures impayées"
msgid "Document uploaded"
msgstr "Document importé"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Document consulté"
@ -3712,10 +3714,6 @@ msgstr "Bonjour, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Cacher"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Cacher des informations supplémentaires"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -4139,10 +4137,10 @@ msgstr "Gérer l'abonnement"
msgid "Manage the {0} organisation"
msgstr "Gérer l'organisation {0}"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Gérer l'abonnement de l'organisation {0}"
msgid "Manage the {1} organisation subscription"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -5814,10 +5812,6 @@ msgstr "Partagez votre expérience de signature !"
msgid "Show"
msgstr "Afficher"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Afficher des informations supplémentaires"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -7247,7 +7241,6 @@ msgid "Unable to join this organisation at this time."
msgstr "Impossible de rejoindre cette organisation pour le moment."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "Impossible de charger l'historique des documents"
@ -8016,11 +8009,8 @@ msgstr "Nous n'avons pas pu vérifier votre email pour le moment."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "Nous n'avons pas pu vérifier votre e-mail. Si votre e-mail n'est pas déjà vérifié, veuillez réessayer."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "Nous générerons des liens de signature pour vous, que vous pourrez envoyer aux destinataires par votre méthode de choix."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "Nous allons générer des liens de signature pour vous, que vous pouvez envoyer aux destinataires par votre méthode de choix."

View File

@ -313,6 +313,10 @@ msgstr "{prefix} ha aggiornato il titolo del documento"
msgid "{prefix} updated the document visibility"
msgstr "{prefix} ha aggiornato la visibilità del documento"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr ""
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} documento"
@ -1767,7 +1771,6 @@ msgstr "Clicca qui per iniziare"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Clicca qui per riprovare"
@ -2749,11 +2752,6 @@ msgstr "ID esterno del documento aggiornato"
msgid "Document found in your account"
msgstr "Documento trovato nel tuo account"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Cronologia del documento"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2872,6 +2870,10 @@ msgstr "Caricamento del documento disabilitato a causa di fatture non pagate"
msgid "Document uploaded"
msgstr "Documento caricato"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Documento visualizzato"
@ -3712,10 +3714,6 @@ msgstr "Ciao, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Nascondi"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Nascondi informazioni aggiuntive"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -4139,10 +4137,10 @@ msgstr "Gestisci abbonamento"
msgid "Manage the {0} organisation"
msgstr "Gestisci l'organizzazione {0}"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Gestisci l'abbonamento dell'organizzazione {0}"
msgid "Manage the {1} organisation subscription"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -5814,10 +5812,6 @@ msgstr "Condividi la tua esperienza di firma!"
msgid "Show"
msgstr "Mostra"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Mostra informazioni aggiuntive"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -7259,7 +7253,6 @@ msgid "Unable to join this organisation at this time."
msgstr "Impossibile unirsi a questa organizzazione in questo momento."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "Impossibile caricare la cronologia del documento"
@ -8028,11 +8021,8 @@ msgstr "Non siamo stati in grado di verificare la tua email in questo momento."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "Non siamo riusciti a verificare la tua email. Se la tua email non è già verificata, riprova."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "Genereremo link di firma con te, che potrai inviare ai destinatari tramite il tuo metodo preferito."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "Genereremo link di firma per te, che potrai inviare ai destinatari tramite il metodo di tua scelta."

View File

@ -313,6 +313,10 @@ msgstr "Użytkownik {prefix} zaktualizował tytuł dokumentu"
msgid "{prefix} updated the document visibility"
msgstr "Użytkownik {prefix} zaktualizował widoczność dokumentu"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr ""
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} dokument"
@ -1767,7 +1771,6 @@ msgstr "Kliknij, aby rozpocząć"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Kliknij tutaj, aby spróbować ponownie"
@ -2749,11 +2752,6 @@ msgstr "Zaktualizowane ID zewnętrzne dokumentu"
msgid "Document found in your account"
msgstr "Dokument znaleziony na Twoim koncie"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Historia dokumentu"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2872,6 +2870,10 @@ msgstr "Przesyłanie dokumentu wyłączone z powodu nieopłaconych faktur"
msgid "Document uploaded"
msgstr "Przesłano dokument"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Dokument został wyświetlony"
@ -3712,10 +3714,6 @@ msgstr "Cześć, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Ukryj"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Ukryj dodatkowe informacje"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -4139,10 +4137,10 @@ msgstr "Zarządzaj subskrypcją"
msgid "Manage the {0} organisation"
msgstr "Zarządzaj organizacją {0}"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Zarządzaj subskrypcjami organizacji {0}"
msgid "Manage the {1} organisation subscription"
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -5814,10 +5812,6 @@ msgstr "Podziel się swoim doświadczeniem podpisywania!"
msgid "Show"
msgstr "Pokaż"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Pokaż dodatkowe informacje"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -7247,7 +7241,6 @@ msgid "Unable to join this organisation at this time."
msgstr "Nie można w tej chwili dołączyć do tej organizacji."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "Nie można załadować historii dokumentu"
@ -8016,11 +8009,8 @@ msgstr "Nie udało się zweryfikować Twojego e-maila w tym momencie."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "Nie udało się zweryfikować twojego e-maila. Jeśli twój e-mail nie jest jeszcze zweryfikowany, spróbuj ponownie."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "Wygenerujemy linki do podpisu dla Ciebie, które możesz wysłać do odbiorców w wybrany przez siebie sposób."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "Wygenerujemy dla Ciebie linki do podpisania, które możesz wysłać do odbiorców za pomocą wybranej metody."

View File

@ -0,0 +1,29 @@
import type { ApiRequestMetadata } from '../universal/extract-request-metadata';
/**
* The minimum required fields that the parent API logger must contain.
*/
export type RootApiLog = {
ipAddress?: string;
userAgent?: string;
requestId: string;
};
/**
* The minimum API log that must be logged at the start of every API request.
*/
export type BaseApiLog = Partial<RootApiLog> & {
path: string;
auth: ApiRequestMetadata['auth'];
source: ApiRequestMetadata['source'];
userId?: number | null;
apiTokenId?: number | null;
};
/**
* The TRPC API log.
*/
export type TrpcApiLog = BaseApiLog & {
trpcMiddleware: string;
unverifiedTeamId?: number | null;
};

View File

@ -33,6 +33,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
'DOCUMENT_OPENED', // When the document is opened by a recipient.
'DOCUMENT_VIEWED', // When the document is viewed by a recipient.
'DOCUMENT_RECIPIENT_REJECTED', // When a recipient rejects the document.
'DOCUMENT_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document.
'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING.
@ -438,6 +439,22 @@ export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
}),
});
/**
* Event: Document viewed.
*/
export const ZDocumentAuditLogEventDocumentViewedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED),
data: ZBaseRecipientDataSchema.extend({
accessAuth: z.preprocess((unknownValue) => {
if (!unknownValue) {
return [];
}
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
}, z.array(ZRecipientAccessAuthTypesSchema)),
}),
});
/**
* Event: Document recipient completed the document (the recipient has fully actioned and completed their required steps for the document).
*/
@ -601,6 +618,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
ZDocumentAuditLogEventDocumentOpenedSchema,
ZDocumentAuditLogEventDocumentViewedSchema,
ZDocumentAuditLogEventDocumentRecipientCompleteSchema,
ZDocumentAuditLogEventDocumentRecipientRejectedSchema,
ZDocumentAuditLogEventDocumentSentSchema,

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import { getIpAddress } from './get-ip-address';
const ZIpSchema = z.string().ip();
export const ZRequestMetadataSchema = z.object({
@ -40,11 +42,13 @@ export type ApiRequestMetadata = {
};
export const extractRequestMetadata = (req: Request): RequestMetadata => {
const forwardedFor = req.headers.get('x-forwarded-for');
const ip = forwardedFor
?.split(',')
.map((ip) => ip.trim())
.at(0);
let ip: string | undefined = undefined;
try {
ip = getIpAddress(req);
} catch {
// Do nothing.
}
const parsedIp = ZIpSchema.safeParse(ip);

View File

@ -0,0 +1,39 @@
export const getIpAddress = (req: Request) => {
// Check for forwarded headers first (common in proxy setups)
const forwarded = req.headers.get('x-forwarded-for');
if (forwarded) {
// x-forwarded-for can contain multiple IPs, take the first one
return forwarded.split(',')[0].trim();
}
// Check for real IP header (used by some proxies)
const realIp = req.headers.get('x-real-ip');
if (realIp) {
return realIp;
}
// Check for client IP header
const clientIp = req.headers.get('x-client-ip');
if (clientIp) {
return clientIp;
}
// Check for CF-Connecting-IP (Cloudflare)
const cfConnectingIp = req.headers.get('cf-connecting-ip');
if (cfConnectingIp) {
return cfConnectingIp;
}
// Check for True-Client-IP (Akamai and Cloudflare)
const trueClientIp = req.headers.get('true-client-ip');
if (trueClientIp) {
return trueClientIp;
}
throw new Error('No IP address found');
};

View File

@ -338,6 +338,10 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Document opened`,
identified: msg`${prefix} opened the document`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED }, () => ({
anonymous: msg`Document viewed`,
identified: msg`${prefix} viewed the document`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
anonymous: msg`Document title updated`,
identified: msg`${prefix} updated the document title`,

View File

@ -1,112 +1,35 @@
import Honeybadger from '@honeybadger-io/js';
import { type TransportTargetOptions, pino } from 'pino';
import { env } from './env';
export const buildLogger = () => {
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
return new HoneybadgerLogger();
}
const transports: TransportTargetOptions[] = [];
return new DefaultLogger();
};
interface LoggerDescriptionOptions {
method?: string;
path?: string;
context?: Record<string, unknown>;
/**
* The type of log to be captured.
*
* Defaults to `info`.
*/
level?: 'info' | 'error' | 'critical';
if (env('NODE_ENV') !== 'production' && !env('INTERNAL_FORCE_JSON_LOGGER')) {
transports.push({
target: 'pino-pretty',
level: 'info',
});
}
/**
* Basic logger implementation intended to be used in the server side for capturing
* explicit errors and other logs.
*
* Not intended to capture the request and responses.
*/
interface Logger {
log(message: string, options?: LoggerDescriptionOptions): void;
const loggingFilePath = env('NEXT_PRIVATE_LOGGER_FILE_PATH');
error(error: Error, options?: LoggerDescriptionOptions): void;
if (loggingFilePath) {
transports.push({
target: 'pino/file',
level: 'info',
options: {
destination: loggingFilePath,
mkdir: true,
},
});
}
class DefaultLogger implements Logger {
log(_message: string, _options?: LoggerDescriptionOptions) {
// Do nothing.
}
error(_error: Error, _options?: LoggerDescriptionOptions): void {
// Do nothing.
}
}
class HoneybadgerLogger implements Logger {
constructor() {
if (!env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
}
Honeybadger.configure({
apiKey: env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY'),
});
}
/**
* Honeybadger doesn't really have a non-error logging system.
*/
log(message: string, options?: LoggerDescriptionOptions) {
const { context = {}, level = 'info' } = options || {};
try {
Honeybadger.event({
message,
context: {
level,
...context,
},
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
error(error: Error, options?: LoggerDescriptionOptions): void {
const { context = {}, level = 'error', method, path } = options || {};
// const tags = [`level:${level}`];
const tags = [];
let errorMessage = error.message;
if (method) {
tags.push(`method:${method}`);
errorMessage = `[${method}]: ${error.message}`;
}
if (path) {
tags.push(`path:${path}`);
}
try {
Honeybadger.notify(errorMessage, {
context: {
level,
...context,
},
tags,
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
}
export const logger = pino({
level: 'info',
transport:
transports.length > 0
? {
targets: transports,
}
: undefined,
});

View File

@ -12,9 +12,15 @@ import {
export const createAdminOrganisationRoute = adminProcedure
.input(ZCreateAdminOrganisationRequestSchema)
.output(ZCreateAdminOrganisationResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { ownerUserId, data } = input;
ctx.logger.info({
input: {
ownerUserId,
},
});
const organisation = await createOrganisation({
userId: ownerUserId,
name: data.name,

View File

@ -11,9 +11,15 @@ import {
export const createStripeCustomerRoute = adminProcedure
.input(ZCreateStripeCustomerRequestSchema)
.output(ZCreateStripeCustomerResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { organisationId } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,

View File

@ -9,9 +9,13 @@ import {
export const createSubscriptionClaimRoute = adminProcedure
.input(ZCreateSubscriptionClaimRequestSchema)
.output(ZCreateSubscriptionClaimResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { name, teamCount, memberCount, flags } = input;
ctx.logger.info({
input,
});
await prisma.subscriptionClaim.create({
data: {
name,

View File

@ -10,9 +10,15 @@ import {
export const deleteSubscriptionClaimRoute = adminProcedure
.input(ZDeleteSubscriptionClaimRequestSchema)
.output(ZDeleteSubscriptionClaimResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const existingClaim = await prisma.subscriptionClaim.findFirst({
where: {
id,

View File

@ -10,9 +10,15 @@ import {
export const getAdminOrganisationRoute = adminProcedure
.input(ZGetAdminOrganisationRequestSchema)
.output(ZGetAdminOrganisationResponseSchema)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
const { organisationId } = input;
ctx.logger.info({
input: {
organisationId,
},
});
return await getAdminOrganisation({
organisationId,
});

View File

@ -61,17 +61,30 @@ export const adminRouter = router({
updateUser: adminProcedure
.input(ZAdminUpdateProfileMutationSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { id, name, email, roles } = input;
ctx.logger.info({
input: {
id,
roles,
},
});
return await updateUser({ id, name, email, roles });
}),
updateRecipient: adminProcedure
.input(ZAdminUpdateRecipientMutationSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { id, name, email } = input;
ctx.logger.info({
input: {
id,
},
});
return await updateRecipient({ id, name, email });
}),
@ -80,6 +93,12 @@ export const adminRouter = router({
.mutation(async ({ ctx, input }) => {
const { id, enabled, data } = input;
ctx.logger.info({
input: {
id,
},
});
return await upsertSiteSetting({
id,
enabled,
@ -90,9 +109,15 @@ export const adminRouter = router({
resealDocument: adminProcedure
.input(ZAdminResealDocumentMutationSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const document = await getEntireDocument({ id });
const isResealing = isDocumentCompleted(document.status);
@ -100,44 +125,75 @@ export const adminRouter = router({
return await sealDocument({ documentId: id, isResealing });
}),
enableUser: adminProcedure.input(ZAdminEnableUserMutationSchema).mutation(async ({ input }) => {
const { id } = input;
enableUser: adminProcedure
.input(ZAdminEnableUserMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
ctx.logger.info({
input: {
id,
},
});
}
return await enableUser({ id });
}),
const user = await getUserById({ id }).catch(() => null);
disableUser: adminProcedure.input(ZAdminDisableUserMutationSchema).mutation(async ({ input }) => {
const { id } = input;
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
const user = await getUserById({ id }).catch(() => null);
return await enableUser({ id });
}),
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
disableUser: adminProcedure
.input(ZAdminDisableUserMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
}
return await disableUser({ id });
}),
const user = await getUserById({ id }).catch(() => null);
deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
const { id } = input;
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
return await deleteUser({ id });
}),
return await disableUser({ id });
}),
deleteUser: adminProcedure
.input(ZAdminDeleteUserMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
return await deleteUser({ id });
}),
deleteDocument: adminProcedure
.input(ZAdminDeleteDocumentMutationSchema)
.mutation(async ({ ctx, input }) => {
const { id, reason } = input;
ctx.logger.info({
input: {
id,
},
});
await sendDeleteEmail({ documentId: id, reason });
return await superDeleteDocument({

View File

@ -10,9 +10,15 @@ import {
export const updateAdminOrganisationRoute = adminProcedure
.input(ZUpdateAdminOrganisationRequestSchema)
.output(ZUpdateAdminOrganisationResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { organisationId, data } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,

View File

@ -12,9 +12,13 @@ import {
export const updateSubscriptionClaimRoute = adminProcedure
.input(ZUpdateSubscriptionClaimRequestSchema)
.output(ZUpdateSubscriptionClaimResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { id, data } = input;
ctx.logger.info({
input,
});
const existingClaim = await prisma.subscriptionClaim.findUnique({
where: { id },
});

View File

@ -15,6 +15,12 @@ export const apiTokenRouter = router({
.mutation(async ({ input, ctx }) => {
const { tokenName, teamId, expirationDate } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await createApiToken({
userId: ctx.user.id,
teamId,
@ -28,6 +34,13 @@ export const apiTokenRouter = router({
.mutation(async ({ input, ctx }) => {
const { id, teamId } = input;
ctx.logger.info({
input: {
id,
teamId,
},
});
return await deleteTokenById({
id,
teamId,

View File

@ -66,6 +66,12 @@ export const authRouter = router({
.mutation(async ({ ctx, input }) => {
const { passkeyId } = input;
ctx.logger.info({
input: {
passkeyId,
},
});
await deletePasskey({
userId: ctx.user.id,
passkeyId,
@ -91,6 +97,12 @@ export const authRouter = router({
.mutation(async ({ ctx, input }) => {
const { passkeyId, name } = input;
ctx.logger.info({
input: {
passkeyId,
},
});
await updatePasskey({
userId: ctx.user.id,
passkeyId,

View File

@ -14,6 +14,13 @@ export const createSubscriptionRoute = authenticatedProcedure
.mutation(async ({ ctx, input }) => {
const { organisationId, priceId } = input;
ctx.logger.info({
input: {
organisationId,
priceId,
},
});
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {

View File

@ -13,6 +13,12 @@ export const getInvoicesRoute = authenticatedProcedure
.query(async ({ ctx, input }) => {
const { organisationId } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {

View File

@ -11,6 +11,12 @@ export const getSubscriptionRoute = authenticatedProcedure
.query(async ({ ctx, input }) => {
const { organisationId } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {

View File

@ -14,6 +14,12 @@ export const manageSubscriptionRoute = authenticatedProcedure
.mutation(async ({ ctx, input }) => {
const { organisationId } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {

View File

@ -1,14 +1,19 @@
import type { Session } from '@prisma/client';
import type { Context } from 'hono';
import type { Logger } from 'pino';
import { z } from 'zod';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import type { RootApiLog } from '@documenso/lib/types/api-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { alphaid } from '@documenso/lib/universal/id';
import { logger } from '@documenso/lib/utils/logger';
// This is a bit nasty. Todo: Extract
import type { HonoEnv } from '@documenso/remix/server/router';
type CreateTrpcContextOptions = {
c: Context;
c: Context<HonoEnv>;
requestSource: 'app' | 'apiV1' | 'apiV2';
};
@ -20,14 +25,22 @@ export const createTrpcContext = async ({
const req = c.req.raw;
const requestMetadata = c.get('context').requestMetadata;
const metadata: ApiRequestMetadata = {
requestMetadata: extractRequestMetadata(req),
requestMetadata,
source: requestSource,
auth: null,
};
const rawTeamId = req.headers.get('x-team-id') || undefined;
const trpcLogger = logger.child({
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
requestId: alphaid(),
} satisfies RootApiLog);
const teamId = z.coerce
.number()
.optional()
@ -36,6 +49,7 @@ export const createTrpcContext = async ({
if (!session || !user) {
return {
logger: trpcLogger,
session: null,
user: null,
teamId,
@ -45,6 +59,7 @@ export const createTrpcContext = async ({
}
return {
logger: trpcLogger,
session,
user,
teamId,
@ -66,4 +81,5 @@ export type TrpcContext = (
teamId: number | undefined;
req: Request;
metadata: ApiRequestMetadata;
logger: Logger;
};

View File

@ -62,6 +62,7 @@ export const documentRouter = router({
find: findInboxRoute,
getCount: getInboxCountRoute,
},
updateDocument: updateDocumentRoute,
/**
* @private
@ -72,6 +73,12 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await getDocumentById({
userId: ctx.user.id,
teamId,
@ -231,6 +238,13 @@ export const documentRouter = router({
const { teamId, user } = ctx;
const { documentId, folderId } = input;
ctx.logger.info({
input: {
documentId,
folderId,
},
});
return await getDocumentWithDetailsById({
userId: user.id,
teamId,
@ -332,6 +346,12 @@ export const documentRouter = router({
const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId } = input;
ctx.logger.info({
input: {
folderId,
},
});
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
@ -353,8 +373,6 @@ export const documentRouter = router({
});
}),
updateDocument: updateDocumentRoute,
/**
* @public
*/
@ -373,6 +391,12 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const userId = ctx.user.id;
await deleteDocument({
@ -396,6 +420,13 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId, signingOrder } = input;
ctx.logger.info({
input: {
documentId,
signingOrder,
},
});
return await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
@ -427,6 +458,12 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId, meta = {} } = input;
ctx.logger.info({
input: {
documentId,
},
});
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
@ -474,6 +511,13 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId, recipients } = input;
ctx.logger.info({
input: {
documentId,
recipients,
},
});
await resendDocument({
userId: ctx.user.id,
teamId,
@ -503,6 +547,12 @@ export const documentRouter = router({
const { teamId, user } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await duplicateDocument({
userId: user.id,
teamId,
@ -544,6 +594,12 @@ export const documentRouter = router({
orderByDirection,
} = input;
ctx.logger.info({
input: {
documentId,
},
});
return await findDocumentAuditLogs({
userId: ctx.user.id,
teamId,
@ -565,6 +621,12 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const document = await getDocumentById({
documentId,
userId: ctx.user.id,
@ -597,6 +659,12 @@ export const documentRouter = router({
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const document = await getDocumentById({
documentId,
userId: ctx.user.id,

View File

@ -19,6 +19,12 @@ export const updateDocumentRoute = authenticatedProcedure
const { teamId } = ctx;
const { documentId, data, meta = {} } = input;
ctx.logger.info({
input: {
documentId,
},
});
const userId = ctx.user.id;
if (Object.values(meta).length > 0) {

View File

@ -16,6 +16,12 @@ export const updateEmbeddingDocumentRoute = procedure
.input(ZUpdateEmbeddingDocumentRequestSchema)
.output(ZUpdateEmbeddingDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
ctx.logger.info({
input: {
documentId: input.documentId,
},
});
try {
const authorizationHeader = ctx.req.headers.get('authorization');

View File

@ -15,6 +15,12 @@ export const updateEmbeddingTemplateRoute = procedure
.input(ZUpdateEmbeddingTemplateRequestSchema)
.output(ZUpdateEmbeddingTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
ctx.logger.info({
input: {
templateId: input.templateId,
},
});
try {
const authorizationHeader = ctx.req.headers.get('authorization');

View File

@ -62,6 +62,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
return await getFieldById({
userId: ctx.user.id,
teamId,
@ -88,6 +94,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { documentId, field } = input;
ctx.logger.info({
input: {
documentId,
},
});
const createdFields = await createDocumentFields({
userId: ctx.user.id,
teamId,
@ -118,6 +130,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { documentId, fields } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await createDocumentFields({
userId: ctx.user.id,
teamId,
@ -146,6 +164,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { documentId, field } = input;
ctx.logger.info({
input: {
documentId,
},
});
const updatedFields = await updateDocumentFields({
userId: ctx.user.id,
teamId,
@ -176,6 +200,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { documentId, fields } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await updateDocumentFields({
userId: ctx.user.id,
teamId,
@ -203,6 +233,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
await deleteDocumentField({
userId: ctx.user.id,
teamId,
@ -225,6 +261,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { documentId, fields } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
@ -263,6 +305,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { templateId, field } = input;
ctx.logger.info({
input: {
templateId,
},
});
const createdFields = await createTemplateFields({
userId: ctx.user.id,
teamId,
@ -293,6 +341,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
return await getFieldById({
userId: ctx.user.id,
teamId,
@ -319,6 +373,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { templateId, fields } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await createTemplateFields({
userId: ctx.user.id,
teamId,
@ -346,6 +406,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { templateId, field } = input;
ctx.logger.info({
input: {
templateId,
},
});
const updatedFields = await updateTemplateFields({
userId: ctx.user.id,
teamId,
@ -375,6 +441,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { templateId, fields } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await updateTemplateFields({
userId: ctx.user.id,
teamId,
@ -401,6 +473,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
await deleteTemplateField({
userId: ctx.user.id,
teamId,
@ -422,6 +500,12 @@ export const fieldRouter = router({
const { teamId } = ctx;
const { templateId, fields } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await setFieldsForTemplate({
templateId,
userId: ctx.user.id,
@ -448,6 +532,12 @@ export const fieldRouter = router({
.mutation(async ({ input, ctx }) => {
const { token, fieldId, value, isBase64, authOptions } = input;
ctx.logger.info({
input: {
fieldId,
},
});
return await signFieldWithToken({
token,
fieldId,
@ -467,6 +557,12 @@ export const fieldRouter = router({
.mutation(async ({ input, ctx }) => {
const { token, fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
return await removeSignedFieldWithToken({
token,
fieldId,

View File

@ -42,6 +42,13 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { parentId, type } = input;
ctx.logger.info({
input: {
parentId,
type,
},
});
const folders = await findFolders({
userId: user.id,
teamId,
@ -75,6 +82,13 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { parentId, type } = input;
ctx.logger.info({
input: {
parentId,
type,
},
});
const folders = await findFolders({
userId: user.id,
teamId,
@ -107,6 +121,13 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { name, parentId, type } = input;
ctx.logger.info({
input: {
parentId,
type,
},
});
if (parentId) {
try {
await getFolderById({
@ -146,6 +167,12 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { id, name, visibility } = input;
ctx.logger.info({
input: {
id,
},
});
const currentFolder = await getFolderById({
userId: user.id,
teamId,
@ -177,6 +204,12 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
await deleteFolder({
userId: user.id,
teamId,
@ -193,6 +226,13 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { id, parentId } = input;
ctx.logger.info({
input: {
id,
parentId,
},
});
const currentFolder = await getFolderById({
userId: user.id,
teamId,
@ -238,6 +278,13 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { documentId, folderId } = input;
ctx.logger.info({
input: {
documentId,
folderId,
},
});
if (folderId !== null) {
try {
await getFolderById({
@ -277,6 +324,13 @@ export const folderRouter = router({
const { teamId, user } = ctx;
const { templateId, folderId } = input;
ctx.logger.info({
input: {
templateId,
folderId,
},
});
if (folderId !== null) {
try {
await getFolderById({
@ -310,16 +364,24 @@ export const folderRouter = router({
* @private
*/
pinFolder: authenticatedProcedure.input(ZPinFolderSchema).mutation(async ({ ctx, input }) => {
const { folderId } = input;
ctx.logger.info({
input: {
folderId,
},
});
const currentFolder = await getFolderById({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
folderId,
});
const result = await pinFolder({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
folderId,
type: currentFolder.type,
});
@ -333,16 +395,24 @@ export const folderRouter = router({
* @private
*/
unpinFolder: authenticatedProcedure.input(ZUnpinFolderSchema).mutation(async ({ ctx, input }) => {
const { folderId } = input;
ctx.logger.info({
input: {
folderId,
},
});
const currentFolder = await getFolderById({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
folderId,
});
const result = await unpinFolder({
userId: ctx.user.id,
teamId: ctx.teamId,
folderId: input.folderId,
folderId,
type: currentFolder.type,
});

View File

@ -24,6 +24,12 @@ export const createOrganisationGroupRoute = authenticatedProcedure
const { organisationId, organisationRole, name, memberIds } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,

View File

@ -14,6 +14,12 @@ export const createOrganisationMemberInvitesRoute = authenticatedProcedure
const userId = ctx.user.id;
const userName = ctx.user.name || '';
ctx.logger.info({
input: {
organisationId,
},
});
await createOrganisationMemberInvites({
userId,
userName,

View File

@ -23,6 +23,12 @@ export const createOrganisationRoute = authenticatedProcedure
const { name, priceId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
priceId,
},
});
// Check if user can create a free organiastion.
if (IS_BILLING_ENABLED() && !priceId) {
const userOrganisations = await prisma.organisation.findMany({

View File

@ -19,6 +19,13 @@ export const deleteOrganisationGroupRoute = authenticatedProcedure
const { groupId, organisationId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
groupId,
organisationId,
},
});
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,

View File

@ -19,6 +19,13 @@ export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
const { organisationId, invitationIds } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
organisationId,
invitationIds,
},
});
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,

View File

@ -13,6 +13,13 @@ export const deleteOrganisationMemberRoute = authenticatedProcedure
const { organisationId, organisationMemberId } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
organisationId,
organisationMemberId,
},
});
await deleteOrganisationMembers({
userId,
organisationId,

View File

@ -21,6 +21,13 @@ export const deleteOrganisationMembersRoute = authenticatedProcedure
const { organisationId, organisationMemberIds } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
organisationId,
organisationMemberIds,
},
});
await deleteOrganisationMembers({
userId,
organisationId,

View File

@ -17,6 +17,12 @@ export const deleteOrganisationRoute = authenticatedProcedure
const { organisationId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,

View File

@ -21,6 +21,12 @@ export const findOrganisationGroupsRoute = authenticatedProcedure
input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
return await findOrganisationGroups({
userId: user.id,
organisationId,

View File

@ -21,6 +21,12 @@ export const findOrganisationMemberInvitesRoute = authenticatedProcedure
const { organisationId, query, page, perPage, status } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
return await findOrganisationMemberInvites({
userId: user.id,
organisationId,

View File

@ -14,6 +14,12 @@ export const getOrganisationRoute = authenticatedProcedure
.query(async ({ input, ctx }) => {
const { organisationReference } = input;
ctx.logger.info({
input: {
organisationReference,
},
});
return await getOrganisation({
userId: ctx.user.id,
organisationReference,

View File

@ -17,9 +17,14 @@ export const leaveOrganisationRoute = authenticatedProcedure
.output(ZLeaveOrganisationResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({ organisationId, userId }),
include: {

View File

@ -20,6 +20,13 @@ export const resendOrganisationMemberInviteRoute = authenticatedProcedure
const userId = ctx.user.id;
const userName = ctx.user.name || '';
ctx.logger.info({
input: {
organisationId,
invitationId,
},
});
await resendOrganisationMemberInvitation({
userId,
userName,

View File

@ -25,6 +25,12 @@ export const updateOrganisationGroupRoute = authenticatedProcedure
const { id, ...data } = input;
const { user } = ctx;
ctx.logger.info({
input: {
id,
},
});
const organisationGroup = await prisma.organisationGroup.findFirst({
where: {
id,

View File

@ -24,6 +24,13 @@ export const updateOrganisationMemberRoute = authenticatedProcedure
const { organisationId, organisationMemberId, data } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
organisationId,
organisationMemberId,
},
});
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,

View File

@ -16,6 +16,12 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
const { user } = ctx;
const { organisationId, data } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const {
// Document related settings.
documentVisibility,

View File

@ -15,9 +15,14 @@ export const updateOrganisationRoute = authenticatedProcedure
.output(ZUpdateOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, data } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
organisationId,
},
});
// Check if organisation exists and user has access to it
const existingOrganisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({

View File

@ -23,9 +23,15 @@ export const profileRouter = router({
});
}),
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => {
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
return await getUserById({ id });
}),
@ -53,6 +59,13 @@ export const profileRouter = router({
.mutation(async ({ input, ctx }) => {
const { bytes, teamId, organisationId } = input;
ctx.logger.info({
input: {
teamId,
organisationId,
},
});
let target: SetAvatarImageOptions['target'] = {
type: 'user',
};

View File

@ -62,6 +62,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { recipientId } = input;
ctx.logger.info({
input: {
recipientId,
},
});
return await getRecipientById({
userId: ctx.user.id,
teamId,
@ -88,6 +94,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { documentId, recipient } = input;
ctx.logger.info({
input: {
documentId,
},
});
const createdRecipients = await createDocumentRecipients({
userId: ctx.user.id,
teamId,
@ -118,6 +130,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { documentId, recipients } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await createDocumentRecipients({
userId: ctx.user.id,
teamId,
@ -146,6 +164,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { documentId, recipient } = input;
ctx.logger.info({
input: {
documentId,
},
});
const updatedRecipients = await updateDocumentRecipients({
userId: ctx.user.id,
teamId,
@ -176,6 +200,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { documentId, recipients } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await updateDocumentRecipients({
userId: ctx.user.id,
teamId,
@ -203,6 +233,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { recipientId } = input;
ctx.logger.info({
input: {
recipientId,
},
});
await deleteDocumentRecipient({
userId: ctx.user.id,
teamId,
@ -223,6 +259,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { documentId, recipients } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await setDocumentRecipients({
userId: ctx.user.id,
teamId,
@ -259,6 +301,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { recipientId } = input;
ctx.logger.info({
input: {
recipientId,
},
});
return await getRecipientById({
userId: ctx.user.id,
teamId,
@ -285,6 +333,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { templateId, recipient } = input;
ctx.logger.info({
input: {
templateId,
},
});
const createdRecipients = await createTemplateRecipients({
userId: ctx.user.id,
teamId,
@ -314,6 +368,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { templateId, recipients } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await createTemplateRecipients({
userId: ctx.user.id,
teamId,
@ -341,6 +401,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { templateId, recipient } = input;
ctx.logger.info({
input: {
templateId,
},
});
const updatedRecipients = await updateTemplateRecipients({
userId: ctx.user.id,
teamId,
@ -370,6 +436,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { templateId, recipients } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await updateTemplateRecipients({
userId: ctx.user.id,
teamId,
@ -396,6 +468,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { recipientId } = input;
ctx.logger.info({
input: {
recipientId,
},
});
await deleteTemplateRecipient({
recipientId,
userId: ctx.user.id,
@ -415,6 +493,12 @@ export const recipientRouter = router({
const { teamId } = ctx;
const { templateId, recipients } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await setTemplateRecipients({
userId: ctx.user.id,
teamId,
@ -438,6 +522,12 @@ export const recipientRouter = router({
.mutation(async ({ input, ctx }) => {
const { token, documentId, authOptions, nextSigner } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await completeDocumentWithToken({
token,
documentId,
@ -456,6 +546,12 @@ export const recipientRouter = router({
.mutation(async ({ input, ctx }) => {
const { token, documentId, reason } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await rejectDocumentWithToken({
token,
documentId,

View File

@ -14,6 +14,12 @@ export const getDocumentInternalUrlForQRCodeRoute = procedure
.query(async ({ input, ctx }) => {
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
if (!ctx.user) {
return null;
}

View File

@ -10,6 +10,12 @@ export const shareLinkRouter = router({
.mutation(async ({ ctx, input }) => {
const { documentId, token } = input;
ctx.logger.info({
input: {
documentId,
},
});
if (token) {
return await createOrGetShareLink({ documentId, token });
}

View File

@ -27,6 +27,13 @@ export const createTeamGroupsRoute = authenticatedProcedure
const { teamId, groups } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
groups,
},
});
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({
teamId,

View File

@ -21,6 +21,13 @@ export const createTeamMembersRoute = authenticatedProcedure
const { teamId, organisationMembers } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
organisationMembers,
},
});
return await createTeamMembers({
userId: user.id,
teamId,

View File

@ -11,6 +11,12 @@ export const createTeamRoute = authenticatedProcedure
const { teamName, teamUrl, organisationId, inheritMembers } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
return await createTeam({
userId: user.id,
teamName,

View File

@ -19,6 +19,13 @@ export const deleteTeamGroupRoute = authenticatedProcedure
const { teamGroupId, teamId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamGroupId,
teamId,
},
});
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({
teamId,

View File

@ -20,6 +20,13 @@ export const deleteTeamMemberRoute = authenticatedProcedure
const { teamId, memberId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
memberId,
},
});
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({
teamId,

View File

@ -11,6 +11,12 @@ export const deleteTeamRoute = authenticatedProcedure
const { teamId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
},
});
await deleteTeam({
userId: user.id,
teamId,

View File

@ -20,6 +20,13 @@ export const findTeamGroupsRoute = authenticatedProcedure
const { teamId, types, query, page, perPage, teamGroupId, organisationRoles } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
teamGroupId,
},
});
return await findTeamGroups({
userId: user.id,
teamId,

View File

@ -13,6 +13,12 @@ export const findTeamMembersRoute = authenticatedProcedure
const { teamId, query, page, perPage } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
},
});
return await findTeamMembers({
userId: user.id,
teamId,

View File

@ -11,5 +11,11 @@ export const findTeamsRoute = authenticatedProcedure
const { organisationId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
},
});
return findTeams({ userId: user.id, organisationId });
});

View File

@ -14,6 +14,12 @@ export const getTeamMembersRoute = authenticatedProcedure
const { teamId } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
},
});
return await getTeamMembers({
userId: user.id,
teamId,

View File

@ -8,8 +8,16 @@ export const getTeamRoute = authenticatedProcedure
.input(ZGetTeamRequestSchema)
.output(ZGetTeamResponseSchema)
.query(async ({ input, ctx }) => {
const { teamReference } = input;
ctx.logger.info({
input: {
teamReference,
},
});
return await getTeam({
teamReference: input.teamReference,
teamReference,
userId: ctx.user.id,
});
});

View File

@ -61,6 +61,12 @@ export const teamRouter = router({
update: authenticatedProcedure
.input(ZUpdateTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
ctx.logger.info({
input: {
teamId: input.teamId,
},
});
return await updateTeamEmail({
userId: ctx.user.id,
...input,
@ -69,39 +75,71 @@ export const teamRouter = router({
delete: authenticatedProcedure
.input(ZDeleteTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await deleteTeamEmail({
userId: ctx.user.id,
userEmail: ctx.user.email,
...input,
teamId,
});
}),
verification: {
send: authenticatedProcedure
.input(ZCreateTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, email, name } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await createTeamEmailVerification({
teamId: input.teamId,
teamId,
userId: ctx.user.id,
data: {
email: input.email,
name: input.name,
email,
name,
},
});
}),
resend: authenticatedProcedure
.input(ZResendTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = input;
ctx.logger.info({
input: {
teamId,
},
});
await resendTeamEmailVerification({
userId: ctx.user.id,
...input,
teamId,
});
}),
delete: authenticatedProcedure
.input(ZDeleteTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await deleteTeamEmailVerification({
userId: ctx.user.id,
...input,
teamId,
});
}),
},

View File

@ -19,6 +19,15 @@ export const updateTeamGroupRoute = authenticatedProcedure
const { id, data } = input;
const { user } = ctx;
ctx.logger.info({
input: {
id,
data: {
teamRole: data.teamRole,
},
},
});
const teamGroup = await prisma.teamGroup.findFirst({
where: {
id,

View File

@ -22,6 +22,13 @@ export const updateTeamMemberRoute = authenticatedProcedure
const { teamId, memberId, data } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
teamId,
memberId,
},
});
const team = await prisma.team.findFirst({
where: {
AND: [

View File

@ -16,6 +16,12 @@ export const updateTeamSettingsRoute = authenticatedProcedure
const { user } = ctx;
const { teamId, data } = input;
ctx.logger.info({
input: {
teamId,
},
});
const {
// Document related settings.
documentVisibility,

View File

@ -13,6 +13,12 @@ export const updateTeamRoute = authenticatedProcedure
const { name, url, profileBio, profileEnabled } = data;
ctx.logger.info({
input: {
teamId,
},
});
if (name || url) {
await updateTeam({
userId: ctx.user.id,

View File

@ -67,6 +67,12 @@ export const templateRouter = router({
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
ctx.logger.info({
input: {
folderId: input.folderId,
},
});
return await findTemplates({
userId: ctx.user.id,
teamId,
@ -92,6 +98,12 @@ export const templateRouter = router({
const { teamId } = ctx;
const { templateId } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await getTemplateById({
id: templateId,
userId: ctx.user.id,
@ -120,6 +132,12 @@ export const templateRouter = router({
const { teamId } = ctx;
const { title, templateDocumentDataId, folderId } = input;
ctx.logger.info({
input: {
folderId,
},
});
return await createTemplate({
userId: ctx.user.id,
teamId,
@ -146,9 +164,14 @@ export const templateRouter = router({
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, data, meta } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
return await updateTemplate({
userId,
teamId,
@ -176,6 +199,12 @@ export const templateRouter = router({
const { teamId } = ctx;
const { templateId } = input;
ctx.logger.info({
input: {
templateId,
},
});
return await duplicateTemplate({
userId: ctx.user.id,
teamId,
@ -200,9 +229,14 @@ export const templateRouter = router({
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
await deleteTemplate({ userId, id: templateId, teamId });
return ZGenericSuccessResponse;
@ -228,6 +262,12 @@ export const templateRouter = router({
const { templateId, recipients, distributeDocument, customDocumentDataId, prefillFields } =
input;
ctx.logger.info({
input: {
templateId,
},
});
const limits = await getServerLimits({ userId: ctx.user.id, teamId });
if (limits.remaining.documents === 0) {
@ -291,6 +331,12 @@ export const templateRouter = router({
templateUpdatedAt,
} = input;
ctx.logger.info({
input: {
directTemplateToken,
},
});
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
@ -330,6 +376,13 @@ export const templateRouter = router({
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
directRecipientId,
},
});
const template = await getTemplateById({ id: templateId, teamId, userId: ctx.user.id });
const limits = await getServerLimits({ userId: ctx.user.id, teamId: template.teamId });
@ -364,6 +417,12 @@ export const templateRouter = router({
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
await deleteTemplateDirectLink({ userId, teamId, templateId });
return ZGenericSuccessResponse;
@ -390,6 +449,12 @@ export const templateRouter = router({
const userId = ctx.user.id;
ctx.logger.info({
input: {
templateId,
},
});
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
}),
@ -402,6 +467,13 @@ export const templateRouter = router({
const { templateId, teamId, csv, sendImmediately } = input;
const { user } = ctx;
ctx.logger.info({
input: {
templateId,
teamId,
},
});
if (csv.length > 4 * 1024 * 1024) {
throw new TRPCError({
code: 'BAD_REQUEST',

View File

@ -4,7 +4,9 @@ import type { AnyZodObject } from 'zod';
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { TrpcApiLog } from '@documenso/lib/types/api-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { alphaid } from '@documenso/lib/universal/id';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import type { TrpcContext } from './context';
@ -65,7 +67,15 @@ const t = initTRPC
/**
* Middlewares
*/
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
const infoToLog: TrpcApiLog = {
path,
auth: ctx.metadata.auth,
source: ctx.metadata.source,
trpcMiddleware: 'authenticated',
unverifiedTeamId: ctx.teamId,
};
const authorizationHeader = ctx.req.headers.get('authorization');
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
@ -79,6 +89,12 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
const apiToken = await getApiTokenByToken({ token });
ctx.logger.info({
...infoToLog,
userId: apiToken.user.id,
apiTokenId: apiToken.id,
} satisfies TrpcApiLog);
return await next({
ctx: {
...ctx,
@ -111,9 +127,21 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
});
}
// Recreate the logger with a sub request ID to differentiate between batched requests.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
trpcSessionLogger.info({
...infoToLog,
userId: ctx.user.id,
apiTokenId: null,
} satisfies TrpcApiLog);
return await next({
ctx: {
...ctx,
logger: trpcSessionLogger,
user: ctx.user,
session: ctx.session,
metadata: {
@ -129,10 +157,26 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
});
});
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
// Recreate the logger with a sub request ID to differentiate between batched requests.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
ctx.logger.info({
path,
auth: ctx.metadata.auth,
source: ctx.metadata.source,
userId: ctx.user?.id,
apiTokenId: null,
trpcMiddleware: 'maybeAuthenticated',
unverifiedTeamId: ctx.teamId,
} satisfies TrpcApiLog);
return await next({
ctx: {
...ctx,
logger: trpcSessionLogger,
user: ctx.user,
session: ctx.session,
metadata: {
@ -150,7 +194,7 @@ export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next }) =
});
});
export const adminMiddleware = t.middleware(async ({ ctx, next }) => {
export const adminMiddleware = t.middleware(async ({ ctx, next, path }) => {
if (!ctx.session || !ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
@ -167,9 +211,24 @@ export const adminMiddleware = t.middleware(async ({ ctx, next }) => {
});
}
// Recreate the logger with a sub request ID to differentiate between batched requests.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
trpcSessionLogger.info({
path,
auth: ctx.metadata.auth,
source: ctx.metadata.source,
userId: ctx.user.id,
apiTokenId: null,
trpcMiddleware: 'admin',
} satisfies TrpcApiLog);
return await next({
ctx: {
...ctx,
logger: trpcSessionLogger,
user: ctx.user,
session: ctx.session,
metadata: {
@ -185,11 +244,34 @@ export const adminMiddleware = t.middleware(async ({ ctx, next }) => {
});
});
export const procedureMiddleware = t.middleware(async ({ ctx, next, path }) => {
// Recreate the logger with a sub request ID to differentiate between batched requests.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
trpcSessionLogger.info({
path,
auth: ctx.metadata.auth,
source: ctx.metadata.source,
userId: ctx.user?.id,
apiTokenId: null,
trpcMiddleware: 'procedure',
} satisfies TrpcApiLog);
return await next({
ctx: {
...ctx,
logger: trpcSessionLogger,
},
});
});
/**
* Routers and Procedures
*/
export const router = t.router;
export const procedure = t.procedure;
export const procedure = t.procedure.use(procedureMiddleware);
export const authenticatedProcedure = t.procedure.use(authenticatedMiddleware);
// While this is functionally the same as `procedure`, it's useful for indicating purpose
export const maybeAuthenticatedProcedure = t.procedure.use(maybeAuthenticatedMiddleware);

View File

@ -19,6 +19,12 @@ export const webhookRouter = router({
.query(async ({ ctx, input }) => {
const { teamId } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await getWebhooksByTeamId(teamId, ctx.user.id);
}),
@ -27,6 +33,13 @@ export const webhookRouter = router({
.query(async ({ input, ctx }) => {
const { id, teamId } = input;
ctx.logger.info({
input: {
id,
teamId,
},
});
return await getWebhookById({
id,
userId: ctx.user.id,
@ -39,6 +52,12 @@ export const webhookRouter = router({
.mutation(async ({ input, ctx }) => {
const { enabled, eventTriggers, secret, webhookUrl, teamId } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await createWebhook({
enabled,
secret,
@ -54,6 +73,13 @@ export const webhookRouter = router({
.mutation(async ({ input, ctx }) => {
const { id, teamId } = input;
ctx.logger.info({
input: {
id,
teamId,
},
});
return await deleteWebhookById({
id,
teamId,
@ -66,6 +92,13 @@ export const webhookRouter = router({
.mutation(async ({ input, ctx }) => {
const { id, teamId, ...data } = input;
ctx.logger.info({
input: {
id,
teamId,
},
});
return await editWebhook({
id,
data,

View File

View File

@ -1,14 +1,14 @@
import type { ErrorHandlerOptions } from '@trpc/server/unstable-core-do-not-import';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildLogger } from '@documenso/lib/utils/logger';
import { logger } from '@documenso/lib/utils/logger';
const logger = buildLogger();
import type { TrpcContext } from '../server/context';
// Parameters<NonNullable<Parameters<typeof trpcServer>[0]['onError']>>[0], // :-)
export const handleTrpcRouterError = (
{ error, path }: Pick<ErrorHandlerOptions<undefined>, 'error' | 'path'>,
source: 'trpc' | 'apiV1' | 'apiV2',
{ error, ctx }: Pick<ErrorHandlerOptions<TrpcContext>, 'error' | 'path' | 'ctx'>,
_source: 'trpc' | 'apiV1' | 'apiV2',
) => {
const appError = AppError.parseError(error.cause || error);
@ -23,16 +23,16 @@ export const handleTrpcRouterError = (
// not an AppError.
const isLoggableTrpcError = !isAppError && errorCodesToAlertOn.includes(error.code);
if (isLoggableAppError || isLoggableTrpcError) {
console.error(error);
const errorLogger = (ctx?.logger || logger).child({
status: 'error',
appError: AppError.toJSON(appError),
});
logger.error(error, {
method: path,
context: {
source,
appError: AppError.toJSON(appError),
},
});
// Only fully log the error on certain conditions since some errors are expected.
if (isLoggableAppError || isLoggableTrpcError) {
errorLogger.error(error);
} else {
errorLogger.info('TRPC_ERROR_HANDLER');
}
};

View File

@ -15,6 +15,8 @@ declare namespace NodeJS {
NEXT_PRIVATE_ENCRYPTION_KEY: string;
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: string;
NEXT_PRIVATE_LOGGER_FILE_PATH?: string;
NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
@ -77,8 +79,6 @@ declare namespace NodeJS {
NEXT_PRIVATE_INNGEST_APP_ID?: string;
NEXT_PRIVATE_INNGEST_EVENT_KEY?: string;
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY?: string;
POSTGRES_URL?: string;
DATABASE_URL?: string;
POSTGRES_PRISMA_URL?: string;

View File

@ -23,7 +23,11 @@ export const ZAddSettingsFormSchema = z.object({
.min(1, { message: msg`Title cannot be empty`.id }),
externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
globalAccessAuth: z
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
.optional()
.default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
meta: z.object({
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),

View File

@ -206,7 +206,7 @@ export const AddSubjectFormPartial = ({
<p className="mt-2">
<Trans>
We will generate signing links for with you, which you can send to the
We will generate signing links for you, which you can send to the
recipients through your method of choice.
</Trans>
</p>

View File

@ -29,6 +29,7 @@ import {
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -324,7 +325,7 @@ export const AddTemplateFieldsFormPartial = ({
pageX -= fieldPageWidth / 2;
pageY -= fieldPageHeight / 2;
append({
const field = {
formId: nanoid(12),
type: selectedField,
pageNumber,
@ -336,7 +337,13 @@ export const AddTemplateFieldsFormPartial = ({
signerId: selectedSigner.id,
signerToken: selectedSigner.token ?? '',
fieldMeta: undefined,
});
};
append(field);
if (ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING.includes(selectedField)) {
setCurrentField(field);
setShowAdvancedSettings(true);
}
setIsFieldWithinBounds(false);
setSelectedField(null);

View File

@ -22,7 +22,11 @@ export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),
externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalAccessAuth: z
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
.optional()
.default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
meta: z.object({
subject: z.string(),

View File

@ -50,6 +50,7 @@
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
"NEXT_PRIVATE_DATABASE_URL",
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
"NEXT_PRIVATE_LOGGER_FILE_PATH",
"NEXT_PRIVATE_SIGNING_TRANSPORT",
"NEXT_PRIVATE_SIGNING_PASSPHRASE",
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH",
@ -104,7 +105,6 @@
"NEXT_PRIVATE_BROWSERLESS_URL",
"NEXT_PRIVATE_JOBS_PROVIDER",
"NEXT_PRIVATE_INNGEST_APP_ID",
"NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY",
"INNGEST_EVENT_KEY",
"NEXT_PRIVATE_INNGEST_EVENT_KEY",
"CI",