Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/build-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Build and Push Crove Sign Docker Image

on:
push:
branches:
- dev
- main
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
build-and-push:
name: Build & Push GHCR Container
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
type=sha,format=short

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
37 changes: 37 additions & 0 deletions apps/remix/server/api/webhooks/dos-webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { handleDosWebhookEvent } from '@documenso/lib/server-only/dos-id/handle-dos-webhook';
import { verifyDosWebhookSignature } from '@documenso/lib/server-only/dos-id/verify-dos-signature';
import { env } from '@documenso/lib/utils/env';
import { Hono } from 'hono';

export const dosWebhookRoute = new Hono()
.post('/dos-org-sync', async (c) => {
const signatureHeader = c.req.header('x-dos-signature') ?? null;
const webhookSecret = env('CROVE_DOS_WEBHOOK_SECRET') || env('NEXT_PRIVATE_DOS_WEBHOOK_SECRET') || '';

const rawBody = await c.req.text();

if (webhookSecret) {
const isValid = verifyDosWebhookSignature({
rawBody,
signatureHeader,
secret: webhookSecret,
});

if (!isValid) {
return c.json({ success: false, message: 'Invalid webhook signature' }, 401);
}
}

try {
const payload = JSON.parse(rawBody);
const result = await handleDosWebhookEvent(payload);

return c.json(result, result.success ? 200 : 400);
} catch (error) {
console.error('[DOS Webhook] Error processing payload:', error);
return c.json(
{ success: false, message: error instanceof Error ? error.message : 'Internal Server Error' },
500,
);
}
});
4 changes: 4 additions & 0 deletions apps/remix/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { Logger } from 'pino';
import { aiRoute } from './api/ai/route';
import { downloadRoute } from './api/download/download';
import { filesRoute } from './api/files/files';
import { dosWebhookRoute } from './api/webhooks/dos-webhook';
import { type AppContext, appContext } from './context';
import { appMiddleware } from './middleware';
import { securityHeadersMiddleware } from './security-headers';
Expand Down Expand Up @@ -116,6 +117,9 @@ app.route('/api/ai', aiRoute);
// CSC OAuth routes (mounted from @documenso/ee).
app.route('/api/csc', csc);

// DOS.Me Webhook endpoints.
app.route('/api/webhooks', dosWebhookRoute);

// API servers.
app.route('/api/v1', tsRestHonoApp);
app.use('/api/jobs/*', jobsClient.getApiHandler());
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ WORKDIR /app
ENV HUSKY 0
ENV DOCKER_OUTPUT 1
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_OPTIONS="--max-old-space-size=4096"

# Encryption keys
ARG NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
Expand Down
28 changes: 28 additions & 0 deletions docker/compose.crove-server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: crove-sign

services:
crove-sign:
image: documenso/documenso:v2.17.0
container_name: crove-sign
restart: always
env_file:
- .env
volumes:
- ./migrations:/app/packages/prisma/migrations:ro
ports:
- "127.0.0.1:4008:3000"
networks:
- default
- postiz-network
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))\""]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s

networks:
default:
postiz-network:
name: postiz-docker-compose_postiz-network
external: true
241 changes: 241 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Kiến Trúc Hệ Thống Crove Sign (Crove OS)

Tài liệu mô tả kiến trúc kỹ thuật, luồng dữ liệu, hạ tầng mạng, cơ sở dữ liệu và cơ chế tích hợp định danh (Identity & SSO) của **Crove Sign** trong hệ sinh thái **Crove OS / DOS.Me**.

---

## 1. Tổng Quan Hệ Thống

**Crove Sign** là dịch vụ ký tài liệu số điện tử (e-Signature Engine) của hệ sinh thái Crove, được xây dựng dựa trên core Documenso v2.17.0 (React Router v7 / Remix + Hono + Prisma + PDF Signing Engine).

```
┌────────────────────────────────────────┐
│ End Users / Browser │
└───────────────────┬────────────────────┘
│ HTTPS (sign.crove.com)
┌────────────────────────────────────────┐
│ Cloudflare Edge (Zero Trust) │
│ Tunnel: Crove-GCP │
└───────────────────┬────────────────────┘
│ QUIC / HTTP2
┌──────────────────────────────────────────────────────────────────────────────────┐
│ GCP Compute Engine: crove-server (Project: crove-os | Zone: asia-southeast1-b) │
│ │
│ ┌───────────────────────┐ Docker Network: crove_postiz-network │
│ │ crove-cloudflared ├────────────────────────────┐ │
│ │ (Cloudflare Connector)│ │ │
│ └───────────────────────┘ ▼ │
│ ┌─────────────────────┐ │
│ │ crove-sign │ │
│ │ (Documenso v2.17.0) │ │
│ │ Port: 3000 (4008) │ │
│ └──────────┬──────────┘ │
└────────────────────────────────────────────────────────┼─────────────────────────┘
┌─────────────────────────────────────┴─────────────────────────┐
│ │
▼ (OIDC Discovery / Token / UserInfo) ▼ (Postgres Session Pooler)
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ DOS ID / Supabase Auth │ │ Supabase Managed Postgres │
│ (Auth & Single Sign-On) │ │ (Schema: sign - 163 tables) │
│ https://id.dos.me / auth/v1 │ │ aws-1-ap-southeast-1.pooler...:5432 │
└────────────────────────────────────────┘ └────────────────────────────────────────┘
```

---

## 2. Hạ Tầng & Mạng (Infrastructure & Network)

### 2.1. Máy Chủ Ứng Dụng (Cloud VM)
- **GCP Project**: `crove-os` (Project Number: `352034351652`, Organization: Tingee).
- **Instance**: `crove-server` (`asia-southeast1-b`).
- **Cấu hình**: `e2-standard-2` (2 vCPU, 8GB RAM), 50GB Boot Disk.
- **Docker Compose Stack**: Đặt tại `/opt/crove/sign/docker-compose.yml`, expose local port `127.0.0.1:4008:3000`.

### 2.2. Định Tuyến & Tên Miền (Cloudflare Zero Trust)
- **Tên miền công khai**: `https://sign.crove.com`.
- **Cloudflare Tunnel**: `Crove-GCP` (Tunnel ID: `41d183ca-1507-4092-a2e5-a5bd988282ee`).
- **Ingress Rule**:
```yaml
- hostname: sign.crove.com
service: http://crove-sign:3000
originRequest:
httpHostHeader: sign.crove.com
```
- **Docker Network**: Container `crove-sign` gắn vào `crove_postiz-network` để `crove-cloudflared` phân giải DNS nội bộ trực tiếp qua service name `crove-sign`.

---

## 3. Kiến Trúc Cơ Sở Dữ Liệu (Database Architecture)

Crove Sign sử dụng chung cụm PostgreSQL quản trị bởi Supabase (`gulptwduchsjcsbndmua`) nhưng được **cô lập hoàn toàn ở cấp độ Schema (Schema-level Multi-Tenancy)**:

| Thông số | Giá trị |
| :--- | :--- |
| **Schema Name** | `sign` (Độc lập với `public`, `cal`, `post`, `crm`, `dosai`, `dosafe`) |
| **Prisma Migrations** | 163 migrations áp dụng thành công trong schema `sign` |
| **Connection Endpoint** | `aws-1-ap-southeast-1.pooler.supabase.com:5432` |
| **Connection String** | `postgresql://postgres.gulptwduchsjcsbndmua:<DB_PASSWORD>@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres?schema=sign&sslmode=no-verify` |
| **Cơ chế mở rộng ID** | Tích hợp hàm `sign.nanoid()` và `sign.nanoid_optimized()` sử dụng extension `extensions.pgcrypto` |

### 3.1. Các bảng dữ liệu cốt lõi
- `sign.User`: Thông tin người dùng e-sign (map theo `email` hoặc `sub` từ DOS ID).
- `sign.Account`: Liên kết tài khoản OAuth/OIDC với `provider = 'oidc'`, `providerAccountId = sub`.
- `sign.Organisation` & `sign.OrganisationMember`: Tổ chức, quyền hạn thành viên, branding, seat limits.
- `sign.Team` & `sign.TeamMember`: Nhóm làm việc trong tổ chức.
- `sign.Envelope` & `sign.EnvelopeItem`: Tài liệu ký (PDF), trạng thái hoàn thành, audit trail.
- `sign.Recipient` & `sign.Field` & `sign.Signature`: Người nhận, tọa độ các ô ký/text/date và chữ ký số.

---

## 4. Định Danh & Xác Thực (Authentication & SSO)

Crove Sign triển khai mô hình **SSO-First** tập trung về **DOS.Me ID** qua giao thức chuẩn **OpenID Connect (OIDC)**:

### 4.1. Thông số Cấu Hình OIDC
- **Provider**: DOS.Me ID (Supabase Auth OpenID Connect Provider).
- **Well-Known Discovery**: `https://gulptwduchsjcsbndmua.supabase.co/auth/v1/.well-known/openid-configuration`.
- **Client ID**: `18790ccb-4d71-48cd-ad24-aee5f3ced3da` (OAuth Client `Crove`).
- **Token Endpoint Auth Method**: `client_secret_basic` (Authorization Basic header).
- **Scopes**: `openid profile email offline_access`.
- **Redirect / Callback URI**: `https://sign.crove.com/api/auth/callback/oidc`.
- **Prompt**: `consent`.

### 4.2. Luồng Đăng Nhập (OIDC Authorization Code Flow with PKCE)

```
User -> Browser Crove Sign (App) DOS.Me ID (Supabase Auth)
│ │ │
├──── Bấm "DOS.Me ID" ─────────>│ │
│ ├─ Tạo state & PKCE code_verifier ───>│
│ ├─ Redirect sang /oauth/authorize ────┤
│<── Redirect 302 ──────────────┤ │
│ │
├──── Đăng nhập / Cấp quyền trên id.dos.me ──────────────────────────>│
│ │
│<── Callback 302 về /api/auth/callback/oidc?code=...&state=... ──────┤
│ │
├──── Gửi code & state ────────>│ │
│ ├─ Gửi Token Request (Basic Auth) ───>│
│ │<─ Trả về Access Token + ID Token ───┤
│ │ │
│ ├─ Decode ID Token (sub, email, name) │
│ ├─ Upsert User & Account in DB (sign) │
│ ├─ Tạo Session Cookie │
│<── Redirect 302 về Dashboard ─┤ │
```

### 4.3. Chính sách SSO-First (Cài đặt môi trường)
```ini
# Vô hiệu hóa form đăng ký/đăng nhập local bằng mật khẩu:
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true

# Giữ trang signin hiển thị nút DOS.Me ID & Passkey:
NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true
NEXT_PRIVATE_OIDC_SKIP_VERIFY=true
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="DOS.Me ID"
```

---

## 5. Kiến Trúc Đồng Bộ Tổ Chức (Organization Synchronization)

Hệ sinh thái Crove OS áp dụng mô hình **Hybrid Organization Sync** để quản lý đa tổ chức nhất quán:

```
┌───────────────────────────────┐
│ DOS.Me Core Workspace │
│ (public.organizations / roles)│
└───────────────┬───────────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
▼ (JIT / Claims) ▼ (OIDC Claims) ▼ (JIT / Webhook)
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Crove Sign │ │ Crove Post │ │ Crove CRM │
│ (schema: sign) │ │ (schema: post) │ │ (schema: core) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
```

1. **Just-In-Time (JIT) Provisioning**:
- Khi người dùng đăng nhập lần đầu qua DOS.Me ID, Crove Sign tự động tạo bản ghi `User` và `Personal Organisation` + `Personal Team`.
- Nếu email đã tồn tại trước đó, hệ thống thực hiện **Account Linking** tự động liên kết `providerAccountId` (`sub`) vào user đó.
2. **Organization Claims Sync**:
- ID Token / UserInfo mang thông tin `organization_id` và `role` từ DOS.Me.
- Callback Auth tiến hành cập nhật/tạo tổ chức doanh nghiệp tương ứng và gán vai trò (`ADMIN` hoặc `MEMBER`) trong schema `sign`.
3. **Webhook Lifecycle (Phase 2)**:
- Lắng nghe sự kiện `organization.created`, `organization.member_added`, `organization.member_removed` từ DOS.Me để đồng bộ trạng thái thành viên theo thời gian thực.

---

## 6. Danh Mục Biến Môi Trường Sản Xuất (`/opt/crove/sign/.env`)

```ini
PORT=3000
NEXTAUTH_SECRET="<SECRET_32_BYTES>"
NEXT_PRIVATE_ENCRYPTION_KEY="<HEX_16_BYTES>"
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<HEX_16_BYTES>"

# App URLs
NEXT_PUBLIC_WEBAPP_URL="https://sign.crove.com"
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"

# Database Connection (Supabase Session Pooler - schema: sign)
NEXT_PRIVATE_DATABASE_URL="postgresql://postgres.gulptwduchsjcsbndmua:<DB_PASSWORD>@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres?schema=sign&sslmode=no-verify"
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgresql://postgres.gulptwduchsjcsbndmua:<DB_PASSWORD>@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres?schema=sign&sslmode=no-verify"

# Storage & Mail
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
NEXT_PRIVATE_SMTP_HOST="127.0.0.1"
NEXT_PRIVATE_SMTP_PORT=25
NEXT_PRIVATE_SMTP_FROM_NAME="Crove Sign"
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@crove.com"

# DOS.Me OIDC SSO
NEXT_PRIVATE_OIDC_WELL_KNOWN="https://gulptwduchsjcsbndmua.supabase.co/auth/v1/.well-known/openid-configuration"
NEXT_PRIVATE_OIDC_CLIENT_ID="18790ccb-4d71-48cd-ad24-aee5f3ced3da"
NEXT_PRIVATE_OIDC_CLIENT_SECRET="<OAUTH_CLIENT_SECRET>"
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="DOS.Me ID"
NEXT_PRIVATE_OIDC_SKIP_VERIFY=true
NEXT_PRIVATE_OIDC_PROMPT="consent"

# SSO-First Access Controls
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true
NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true
```

---

## 7. Quy Trình Vận Hành & Khôi Phục (Operations & Runbook)

### 7.1. Khởi động / Khởi động lại dịch vụ
```bash
cd /opt/crove/sign
sudo docker compose up -d
sudo docker compose restart
```

### 7.2. Kiểm tra trạng thái & Healthcheck
```bash
# Kiểm tra container status
docker ps --filter name=crove-sign

# Kiểm tra log ứng dụng
docker logs --tail 50 crove-sign

# Kiểm tra endpoint sức khỏe
curl -s http://127.0.0.1:4008/api/health
```

### 7.3. Cập nhật Ingress Tunnel khi cần
- File cấu hình Cloudflare Tunnel: `/opt/crove/tunnel/config.yml`.
- Sau khi chỉnh sửa, khởi động lại connector:
```bash
cd /opt/crove
sudo docker compose -f docker-compose.prod.yaml restart cloudflared
```
Loading
Loading