Add embedded signing support with webhook configuration

- Add helper function to generate embedded signing URLs from Documenso links
- Store embedded signature links for all signers (Client, Developer, CC)
- Add webhook secret configuration for embedded signing
- Add documentation for embedded signing and website implementation
- Update Interest type with embedded signature link fields
This commit is contained in:
Matt 2025-06-11 17:57:58 +02:00
parent 85773fc09e
commit f891060e7c
5 changed files with 568 additions and 1 deletions

View File

@ -15,5 +15,8 @@ NUXT_EMAIL_SMTP_PORT=587
NUXT_EMAIL_LOGO_URL=https://portnimara.com/logo.png
# Documenso Configuration
NUXT_DOCUMENSO_API_KEY=your-documenso-api-key
NUXT_DOCUMENSO_API_KEY=your_documenso_api_key_here
NUXT_DOCUMENSO_BASE_URL=https://signatures.portnimara.dev
# Webhook Configuration for Embedded Signing
WEBHOOK_SECRET_SIGNING=96BQQRiKkTIN2w0rHbqo7yHggV/sT8702HtHih3uNSY=

View File

@ -0,0 +1,207 @@
# Embedded Signing Implementation Guide
## Overview
This document outlines the implementation of embedded Documenso signing for Port Nimara's Expression of Interest (EOI) documents. The system transforms the signing experience from external Documenso links to embedded signing within the Port Nimara website.
## Current Implementation Status
### ✅ Client Portal Changes (COMPLETED)
#### 1. Type Definitions Updated
- **File**: `utils/types.ts`
- **Added fields**:
- `EmbeddedSignatureLinkClient?: string`
- `EmbeddedSignatureLinkCC?: string`
- `EmbeddedSignatureLinkDeveloper?: string`
#### 2. EOI Generation Enhanced
- **File**: `server/api/email/generate-eoi-document.ts`
- **Added URL transformation function**:
```typescript
const createEmbeddedSigningUrl = (documensoUrl: string, signerType: 'client' | 'cc' | 'developer'): string => {
if (!documensoUrl) return '';
const token = documensoUrl.split('/').pop();
return `https://portnimara.com/sign/${signerType}/${token}`;
};
```
#### 3. Database Schema
- **Added NocoDB columns**:
- `EmbeddedSignatureLinkClient`
- `EmbeddedSignatureLinkCC`
- `EmbeddedSignatureLinkDeveloper`
#### 4. Configuration
- **File**: `.env.example`
- **Added webhook secret**: `WEBHOOK_SECRET_SIGNING=96BQQRiKkTIN2w0rHbqo7yHggV/sT8702HtHih3uNSY=`
## URL Structure
### Original Documenso URLs:
```
Client: https://signatures.portnimara.dev/sign/ABC123
CC: https://signatures.portnimara.dev/sign/DEF456
Developer: https://signatures.portnimara.dev/sign/GHI789
```
### New Embedded URLs:
```
Client: https://portnimara.com/sign/client/ABC123
CC: https://portnimara.com/sign/cc/DEF456
Developer: https://portnimara.com/sign/developer/GHI789
```
## Next Steps Required
### 🔄 Website Implementation (PENDING)
The Port Nimara website needs the following implementation:
#### 1. Install Documenso Embedding Package
```bash
npm install @documenso/embed-vue
```
#### 2. Create Dynamic Route
**File**: `pages/sign/[type]/[token].vue`
#### 3. Implement Signing Pages
- Main signing page with embedded interface
- Success page (`pages/sign/success.vue`)
- Error page (`pages/sign/error.vue`)
#### 4. Technical Specifications
- **Framework**: Nuxt 3 with Vue 3
- **Styling**: Tailwind CSS
- **Documenso Host**: `https://signatures.portnimara.dev`
- **Brand Colors**: `#0056b3` (primary), `#ffffff`, `#f8f9fa`
- **Logos**: `/images/logo-full.png`, `/images/logo-no-text.png`
### 🔄 CORS Configuration (MANUAL)
Add to nginx config for `signatures.portnimara.dev`:
```nginx
location / {
# CORS headers for embedding
add_header 'Access-Control-Allow-Origin' 'https://portnimara.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://portnimara.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
}
```
## Testing Strategy
### Phase 1: Basic Functionality
1. Generate a test EOI in client portal
2. Verify both URL sets are populated in database
3. Test embedded URLs manually
4. Verify CORS headers are working
### Phase 2: End-to-End Testing
1. Create interest
2. Generate EOI
3. Complete signing via embedded interface
4. Verify status updates in client portal
### Phase 3: Email Integration
1. Update email templates to use embedded URLs
2. Test email delivery with embedded links
3. Monitor completion rates
## Implementation Details for Website Team
### Required Environment Variables
```env
WEBHOOK_SECRET_SIGNING=96BQQRiKkTIN2w0rHbqo7yHggV/sT8702HtHih3uNSY=
```
### Signer Type Messaging
```javascript
const signerMessages = {
client: {
title: 'Sign Your Expression of Interest',
subtitle: 'Please review and sign your Port Nimara berth EOI'
},
cc: {
title: 'Approve Expression of Interest',
subtitle: 'Please review and approve this Port Nimara berth EOI'
},
developer: {
title: 'Review and Sign Expression of Interest',
subtitle: 'Please review and sign this Port Nimara berth EOI'
}
};
```
### Token Validation
- Validate signer type: `['client', 'cc', 'developer'].includes(signerType)`
- Validate token format: `/^[a-zA-Z0-9_-]+$/`
- Redirect to error page for invalid tokens
### Webhook Integration (Optional - Phase 2)
```javascript
// Endpoint: https://client-portal.portnimara.com/api/webhook/document-signed
// Method: POST
// Headers: { 'x-webhook-secret': '96BQQRiKkTIN2w0rHbqo7yHggV/sT8702HtHih3uNSY=' }
// Body: { token, signerType, signedAt }
```
## Current Data Flow
### Document Generation:
1. Client Portal generates EOI via Documenso API
2. Receives original signing URLs from Documenso
3. Transforms URLs to embedded format
4. Stores both original and embedded URLs in NocoDB
### Database Example:
```json
{
"Signature Link Client": "https://signatures.portnimara.dev/sign/ABC123",
"EmbeddedSignatureLinkClient": "https://portnimara.com/sign/client/ABC123",
"Signature Link CC": "https://signatures.portnimara.dev/sign/DEF456",
"EmbeddedSignatureLinkCC": "https://portnimara.com/sign/cc/DEF456",
"Signature Link Developer": "https://signatures.portnimara.dev/sign/GHI789",
"EmbeddedSignatureLinkDeveloper": "https://portnimara.com/sign/developer/GHI789"
}
```
## Rollback Strategy
If issues arise:
1. **Immediate**: Change email templates back to original URLs
2. **Database**: Original URLs remain intact for fallback
3. **Website**: Can redirect embedded routes to Documenso temporarily
## Success Metrics
- ✅ Embedded signing works for all signer types
- ✅ Status tracking continues working identical to current system
- ✅ Mobile responsive interface
- ✅ No increase in signing error rates
- ✅ Improved user experience (staying on portnimara.com domain)
## Contact Information
- **Webhook Secret**: `96BQQRiKkTIN2w0rHbqo7yHggV/sT8702HtHih3uNSY=`
- **Documenso Instance**: `https://signatures.portnimara.dev`
- **Development Server**: `http://localhost:3000` (client portal)
---
*Implementation completed: Client Portal phase*
*Next: Website implementation and CORS configuration*

View File

@ -0,0 +1,343 @@
# Website Implementation Guide for Embedded Documenso Signing
## Task Summary
Implement embedded Documenso signing on the Port Nimara website to provide a seamless, branded signing experience for Expression of Interest (EOI) documents.
## Context
The Port Nimara client portal (separate repository) generates EOI documents via a self-hosted Documenso instance and now creates embedded signing URLs that should be handled by the main website at `portnimara.com`.
## Technical Specifications
### Website Stack
- **Framework**: Nuxt 3 (v3.15.4) with Vue 3
- **Styling**: Tailwind CSS
- **TypeScript**: Enabled
- **Package to Install**: `@documenso/embed-vue`
### Documenso Configuration
- **Host**: `https://signatures.portnimara.dev`
- **Self-hosted community edition** (full embedding features available)
- **Authentication**: Handled via tokens (no API keys needed on website)
### Brand Assets
- **Logos**: `/images/logo-full.png`, `/images/logo-no-text.png`, `/images/logo-no-text-white.png`
- **Primary Color**: `#0056b3`
- **Background**: `#ffffff`, `#f8f9fa`
- **Fonts**: Custom fonts in `/public/fonts/` (Bill Corporate, Garamond, Runalto)
## URL Structure to Implement
### Route Pattern
```
/sign/[type]/[token]
```
### URL Examples
```
Client signing: https://portnimara.com/sign/client/ABC123
CC approval: https://portnimara.com/sign/cc/DEF456
Developer signing: https://portnimara.com/sign/developer/GHI789
```
### Route Parameters
- **type**: `'client' | 'cc' | 'developer'`
- **token**: Alphanumeric string from Documenso (e.g., `ABC123`, `mK9N2pL5vX8`)
## Required Implementation
### 1. Install Dependencies
```bash
npm install @documenso/embed-vue
```
### 2. Create Main Signing Page
**File**: `pages/sign/[type]/[token].vue`
**Key Requirements**:
- Validate route parameters (type and token)
- Extract token and signer type from route
- Display appropriate messaging per signer type
- Embed Documenso signing interface
- Handle successful signing with redirect
- Apply Port Nimara branding throughout
**Signer Type Messages**:
```javascript
const signerMessages = {
client: {
title: 'Sign Your Expression of Interest',
subtitle: 'Please review and sign your Port Nimara berth EOI'
},
cc: {
title: 'Approve Expression of Interest',
subtitle: 'Please review and approve this Port Nimara berth EOI'
},
developer: {
title: 'Review and Sign Expression of Interest',
subtitle: 'Please review and sign this Port Nimara berth EOI'
}
};
```
**Embed Configuration**:
```javascript
// Use these exact values
const documensoHost = 'https://signatures.portnimara.dev';
// CSS variables for brand consistency
const cssVars = {
primary: '#0056b3',
primaryForeground: '#ffffff',
background: '#ffffff',
foreground: '#1a1a1a',
secondary: '#f8f9fa',
secondaryForeground: '#1a1a1a',
accent: '#e9ecef',
accentForeground: '#1a1a1a',
destructive: '#dc3545',
destructiveForeground: '#ffffff',
border: '#dee2e6',
input: '#ffffff',
ring: '#0056b3',
radius: '0.5rem'
};
```
### 3. Create Success Page
**File**: `pages/sign/success.vue`
**Requirements**:
- Thank you message
- Confirmation that document was signed
- Link back to main Port Nimara website
- Professional, clean design
### 4. Create Error Page
**File**: `pages/sign/error.vue`
**Requirements**:
- Handle invalid/expired tokens
- Provide contact information
- Link to support or main website
### 5. Add Environment Configuration
Add to your `.env` file:
```env
WEBHOOK_SECRET_SIGNING=96BQQRiKkTIN2w0rHbqo7yHggV/sT8702HtHih3uNSY=
```
## Implementation Template
### Main Signing Page Structure
```vue
<template>
<div class="min-h-screen bg-gray-50">
<!-- Header with Port Nimara branding -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex items-center justify-between">
<img src="/images/logo-full.png" alt="Port Nimara" class="h-12" />
<div class="text-right">
<h1 class="text-2xl font-bold text-gray-900">{{ pageTitle }}</h1>
<p class="text-sm text-gray-600">{{ pageSubtitle }}</p>
</div>
</div>
</div>
</header>
<!-- Main signing interface -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-white rounded-lg shadow-lg overflow-hidden">
<!-- Loading state -->
<div v-if="loading" class="flex items-center justify-center h-96">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p class="mt-4 text-gray-600">Loading document...</p>
</div>
</div>
<!-- Embedded Documenso interface -->
<EmbedSignDocument
v-else
:token="token"
:host="documensoHost"
:on-document-signed="handleDocumentSigned"
:on-document-viewed="handleDocumentViewed"
:dark-mode-disabled="false"
class="w-full h-[800px]"
:css-vars="cssVars"
/>
</div>
</main>
</div>
</template>
<script setup>
import { EmbedSignDocument } from '@documenso/embed-vue';
// ... implementation details in full guide
</script>
```
## Validation Requirements
### Route Parameter Validation
```javascript
// Validate signer type
const validSignerTypes = ['client', 'cc', 'developer'];
if (!validSignerTypes.includes(signerType.value)) {
router.push('/sign/error');
}
// Validate token format (basic)
const tokenPattern = /^[a-zA-Z0-9_-]+$/;
if (!tokenPattern.test(token.value)) {
router.push('/sign/error');
}
```
## Event Handling
### Document Signed Handler
```javascript
const handleDocumentSigned = async () => {
console.log('Document signed successfully');
// Optional: Webhook notification (Phase 2)
// [Webhook code - initially commented out]
// Always redirect to success
router.push('/sign/success');
};
```
### Document Viewed Handler
```javascript
const handleDocumentViewed = () => {
console.log('Document viewed:', { token: token.value, type: signerType.value });
// Optional: Analytics tracking
};
```
## Mobile Responsiveness
Ensure the implementation works well on mobile devices:
```css
/* Example responsive adjustments */
@media (max-width: 768px) {
.signing-header h1 {
font-size: 1.25rem;
}
.documenso-embed {
height: 600px; /* Reduced height for mobile */
}
}
```
## Testing Approach
### 1. Manual Testing
Once implemented, you can test with these example URLs:
- `http://localhost:3000/sign/client/test123`
- `http://localhost:3000/sign/cc/test456`
- `http://localhost:3000/sign/developer/test789`
### 2. Real Token Testing
After the client portal generates a real EOI, you'll receive actual tokens to test with.
## CORS Requirements
The Documenso instance needs CORS headers to allow embedding. This is handled manually via nginx configuration on the server.
**Expected CORS headers from `signatures.portnimara.dev`**:
```
Access-Control-Allow-Origin: https://portnimara.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Credentials: true
```
## Error Handling
### Common Scenarios
1. **Invalid token**: Redirect to error page
2. **Invalid signer type**: Redirect to error page
3. **Expired token**: Documenso will handle this within the embed
4. **Network issues**: Show loading state, let Documenso handle retries
## Success Criteria
**Functionality**:
- Signing interface loads correctly
- Different messaging for each signer type
- Successful signing redirects to success page
- Invalid URLs redirect to error page
**User Experience**:
- Port Nimara branding throughout
- Mobile responsive design
- Fast loading times
- Clear error messages
**Technical**:
- Clean URL structure
- Proper error handling
- Console logging for debugging
- Ready for webhook integration
## Webhook Integration (Phase 2)
*Initially implement with webhook code commented out*
```javascript
// Future webhook endpoint:
// https://client-portal.portnimara.com/api/webhook/document-signed
const notifyPortal = async () => {
try {
await $fetch('https://client-portal.portnimara.com/api/webhook/document-signed', {
method: 'POST',
headers: {
'x-webhook-secret': process.env.WEBHOOK_SECRET_SIGNING
},
body: {
token: token.value,
signerType: signerType.value,
signedAt: new Date().toISOString()
}
});
} catch (error) {
console.error('Webhook failed:', error);
// Don't block user flow
}
};
```
## Questions & Support
If you encounter any issues during implementation:
1. **CORS errors**: The server-side CORS configuration may need adjustment
2. **Token format issues**: Check that tokens are being extracted correctly
3. **Embedding not loading**: Verify the Documenso host URL and package installation
4. **Styling issues**: Refer to the CSS variables provided for brand consistency
## Next Steps After Implementation
1. **Test with real tokens** from generated EOIs
2. **Verify mobile experience** on actual devices
3. **Coordinate with client portal** for email template updates
4. **Monitor for any CORS issues** in production
5. **Prepare for webhook integration** in Phase 2
---
*This guide provides everything needed to implement embedded signing on the Port Nimara website. The client portal changes are already complete and ready to generate the embedded URLs this implementation will handle.*

View File

@ -1,5 +1,12 @@
import { getInterestById, updateInterest } from '~/server/utils/nocodb';
// Helper function to create embedded signing URLs
const createEmbeddedSigningUrl = (documensoUrl: string, signerType: 'client' | 'cc' | 'developer'): string => {
if (!documensoUrl) return '';
const token = documensoUrl.split('/').pop();
return `https://portnimara.com/sign/${signerType}/${token}`;
};
interface DocumensoRecipient {
id: number;
name: string;
@ -323,12 +330,15 @@ export default defineEventHandler(async (event) => {
// Add signing links to update data with new column names
if (signingLinks['Client']) {
updateData['Signature Link Client'] = signingLinks['Client'];
updateData['EmbeddedSignatureLinkClient'] = createEmbeddedSigningUrl(signingLinks['Client'], 'client');
}
if (signingLinks['David Mizrahi']) {
updateData['Signature Link Developer'] = signingLinks['David Mizrahi'];
updateData['EmbeddedSignatureLinkDeveloper'] = createEmbeddedSigningUrl(signingLinks['David Mizrahi'], 'developer');
}
if (signingLinks['Oscar Faragher']) {
updateData['Signature Link CC'] = signingLinks['Oscar Faragher'];
updateData['EmbeddedSignatureLinkCC'] = createEmbeddedSigningUrl(signingLinks['Oscar Faragher'], 'cc');
}
await updateInterest(interestId, updateData);

View File

@ -127,6 +127,10 @@ export interface Interest {
"Signature Link Client"?: string;
"Signature Link CC"?: string;
"Signature Link Developer"?: string;
// Embedded signature link fields
"EmbeddedSignatureLinkClient"?: string;
"EmbeddedSignatureLinkCC"?: string;
"EmbeddedSignatureLinkDeveloper"?: string;
// Documenso document ID for generated documents
"documensoID"?: string;
}