mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
64 Commits
v1.5.0-rc.
...
v1.5.2-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 82efc5f589 | |||
| 8a9588ca65 | |||
| f723a34464 | |||
| dd1d657057 | |||
| 10ef5b6e51 | |||
| 9525b9bd63 | |||
| d382e03085 | |||
| b8fa45380b | |||
| 2cce6dc2e5 | |||
| 927cb1a934 | |||
| 579a2f96e5 | |||
| 8a82952b34 | |||
| 70494fa5bb | |||
| 41ccefe212 | |||
| e83a4becee | |||
| a03ce728f3 | |||
| d29caaf823 | |||
| 7e9b96f059 | |||
| 691255da3f | |||
| f6f9c301da | |||
| a17d4a2a39 | |||
| 73aae6f1e3 | |||
| 328d16483c | |||
| 6dd2abfe51 | |||
| 452545dab1 | |||
| 437410c73a | |||
| 36a95f6153 | |||
| 0f03ad4a6b | |||
| 8674ad4c88 | |||
| 98e00739c8 | |||
| 00c36782ff | |||
| 665ccd7628 | |||
| e5fe3d897d | |||
| bfb1c65f98 | |||
| 3b8b87a90b | |||
| 819e58dd61 | |||
| 8c435d48b7 | |||
| 0c8a89a2ea | |||
| 6065140715 | |||
| 34a59d2db3 | |||
| ba37633ecd | |||
| 46e83d65bb | |||
| 142c93aa63 | |||
| 3eb1a17d3c | |||
| 6d1ad179d4 | |||
| 8eed13e275 | |||
| 346078dbbe | |||
| c8337d7dcc | |||
| 75630ef19d | |||
| 634807328e | |||
| 2bbbe1098a | |||
| d24b9de254 | |||
| c86f79dd7b | |||
| 0c12e34c38 | |||
| 26b604dbd0 | |||
| 308f55f3d4 | |||
| e5b7bf81fa | |||
| b35f050409 | |||
| ce6f523230 | |||
| 9e57de512a | |||
| 9b5d64cc1a | |||
| fc372d0aa9 | |||
| e470020b16 | |||
| 0a9006430f |
24
.github/actions/cache-build/action.yml
vendored
Normal file
24
.github/actions/cache-build/action.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Cache production build binaries
|
||||
description: 'Cache or restore if necessary'
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v18.x
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Cache production build
|
||||
uses: actions/cache@v3
|
||||
id: production-build-cache
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/web/.next
|
||||
${{ github.workspace }}/apps/marketing/.next
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
|
||||
key: prod-build-${{ github.run_id }}
|
||||
restore-keys: prod-build-
|
||||
|
||||
- run: npm run build
|
||||
shell: bash
|
||||
39
.github/actions/node-install/action.yml
vendored
Normal file
39
.github/actions/node-install/action.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: 'Setup node and cache node_modules'
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v18.x
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Set up Node ${{ inputs.node_version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: npm-
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
id: cache-node-modules
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
apps/*/node_modules
|
||||
key: modules-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
npm ci --no-audit
|
||||
npm run prisma:generate
|
||||
env:
|
||||
HUSKY: '0'
|
||||
19
.github/actions/playwright-install/action.yml
vendored
Normal file
19
.github/actions/playwright-install/action.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: Install playwright binaries
|
||||
description: 'Install playwright, cache and restore if necessary'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Cache playwright
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: playwright-
|
||||
|
||||
- name: Install playwright
|
||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps
|
||||
shell: bash
|
||||
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: 'Continuous Integration'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
@ -10,9 +11,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
build_app:
|
||||
name: Build App
|
||||
@ -23,20 +21,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Copy env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
build_docker:
|
||||
name: Build Docker Image
|
||||
@ -47,5 +37,31 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Build Docker Image
|
||||
run: ./docker/build.sh
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: false
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
tags: documenso-${{ github.sha }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
- # Temp fix
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
29
.github/workflows/clean-cache.yml
vendored
Normal file
29
.github/workflows/clean-cache.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: cleanup caches by a branch
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
13
.github/workflows/codeql-analysis.yml
vendored
13
.github/workflows/codeql-analysis.yml
vendored
@ -25,19 +25,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Copy env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Build Documenso
|
||||
run: npm run build
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
|
||||
20
.github/workflows/e2e-tests.yml
vendored
20
.github/workflows/e2e-tests.yml
vendored
@ -6,29 +6,21 @@ on:
|
||||
branches: ['main']
|
||||
jobs:
|
||||
e2e_tests:
|
||||
name: "E2E Tests"
|
||||
name: 'E2E Tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Copy env
|
||||
run: cp .env.example .env
|
||||
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Start Services
|
||||
run: npm run dx:up
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Generate Prisma Client
|
||||
run: npm run prisma:generate -w @documenso/prisma
|
||||
- uses: ./.github/actions/playwright-install
|
||||
|
||||
- name: Create the database
|
||||
run: npm run prisma:migrate-dev
|
||||
@ -36,6 +28,8 @@ jobs:
|
||||
- name: Seed the database
|
||||
run: npm run prisma:seed
|
||||
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run ci
|
||||
|
||||
@ -43,7 +37,7 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: "packages/app-tests/**/test-results/*"
|
||||
path: 'packages/app-tests/**/test-results/*'
|
||||
retention-days: 30
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
36
.github/workflows/publish.yml
vendored
Normal file
36
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Publish Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['release']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and push the docker image
|
||||
run: ./docker/buildx-and-push.sh
|
||||
@ -1,5 +1,3 @@
|
||||
> 🚨 It is Launch Week #2 - Day 1: We launches teams 🎉 https://documen.so/day1
|
||||
|
||||
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
|
||||
|
||||
<p align="center" style="margin-top: 20px">
|
||||
@ -107,7 +105,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
|
||||
|
||||
To run Documenso locally, you will need
|
||||
|
||||
- Node.js
|
||||
- Node.js (v18 or above)
|
||||
- Postgres SQL Database
|
||||
- Docker (optional)
|
||||
|
||||
|
||||
61
apps/marketing/content/blog/launch-week-2-day-5.mdx
Normal file
61
apps/marketing/content/blog/launch-week-2-day-5.mdx
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Launch Week II - Day 5 - Documenso Profiles
|
||||
description: Documenso profiles allow you to send signing links to people so they can sign anytime and see who you are. Documenso Profile Usernames can be claimed starting today. Long ones free, short ones paid. Profiles will launch as soon as they are shiny.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-03-01
|
||||
tags:
|
||||
- Launch Week
|
||||
- Profiles
|
||||
---
|
||||
|
||||
<video
|
||||
id="vid"
|
||||
width="100%"
|
||||
src="https://github.com/documenso/design/assets/1309312/89643ae2-2aa9-484c-a522-a0e35097c469"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
></video>
|
||||
|
||||
> TLDR; Documenso profiles allow you to send signing links to people so they can sign anytime and see who you are. Documenso Profile Usernames can be claimed starting today. Long ones free, short ones paid. Profiles launch as soon as they are shiny.
|
||||
|
||||
## Introducing Documenso Profile Links
|
||||
|
||||
Day 5 - The Finale 🔥
|
||||
|
||||
Signing documents has always been between humans, and signing something together should be as frictionless as possible. It should also be async, so you don't force your counterpart to jog to their device to send something when you are ready. Today we are announcing the new Documenso Profiles:
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/profile.png"
|
||||
width="1260"
|
||||
height="813"
|
||||
alt="Timur's Documenso Profile"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Async > Sync: Add public templates to your Documenso Link and let people sign whenever they are ready.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Documenso profiles work with your existing templates. You can just add them to your public profile to let everyone with your link sign them. With profiles, we want to bring back the human aspect of signing.
|
||||
|
||||
By making profiles public, you can always access what your counterparty offers and make them more visible in the process. Long-term, we plan to add more to profiles to help you ensure the person you are dealing with is who they claim to be. Documenso wants to be the trust layer of the internet, and we want to start at the very fundamental level: The individual transaction.
|
||||
|
||||
Profiles are our first step towards bringing more trust into everything, simply by making the use of signing more frictionless. As there is more and more content of questionable origin out there, we want to support you in making it clear what you send out and what not.
|
||||
|
||||
## Pricing and Claiming
|
||||
|
||||
Documenso profile username can be claimed starting today. Documenso profiles will launch as soon as we are happy with the details ✨
|
||||
|
||||
- Long usernames (6 characters or more) come free with every account, e.g. **documenso.com/u/timurercan**
|
||||
- Short usernames (5 characters or fewer) or less require any paid account ([Early Adopter](https://documen.so/claim-early-adopters-plan), [Teams](https://documen.so/teams) or Enterprise): **e.g., documenso.com/u/timur**
|
||||
|
||||
You can claim your username here: [https://documen.so/claim](https://documen.so/claim)
|
||||
|
||||
> 🚨 We need you help to help us to make this the biggest launch week yet: <a href="https://twitter.com/intent/tweet?text=It's @Documenso Launch Week Day 5! You can now claim your username for the upcoming profile links 😮 https://documen.so/day5"> Support us on Twitter </a> or anywhere to spread awareness for open signing! The best posts will receive merch codes 👀
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
BIN
apps/marketing/public/blog/profile.png
Normal file
BIN
apps/marketing/public/blog/profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
@ -56,7 +56,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-background text-center text-sm">
|
||||
<div className="text-background text-center text-sm text-white">
|
||||
Claim your documenso public profile username now!{' '}
|
||||
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
|
||||
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes, useEffect, useState } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts';
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export type MonthlyNewUsersChartProps = {
|
||||
export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartProps) => {
|
||||
const formattedData = [...data].reverse().map(({ month, count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'),
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
count: Number(count),
|
||||
};
|
||||
});
|
||||
@ -32,6 +32,9 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr
|
||||
<YAxis />
|
||||
|
||||
<Tooltip
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), 'New Users']}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
|
||||
@ -14,7 +14,7 @@ export type MonthlyTotalUsersChartProps = {
|
||||
export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersChartProps) => {
|
||||
const formattedData = [...data].reverse().map(({ month, cume_count: count }) => {
|
||||
return {
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'),
|
||||
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'),
|
||||
count: Number(count),
|
||||
};
|
||||
});
|
||||
@ -32,6 +32,9 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha
|
||||
<YAxis />
|
||||
|
||||
<Tooltip
|
||||
labelStyle={{
|
||||
color: 'hsl(var(--primary-foreground))',
|
||||
}}
|
||||
formatter={(value) => [Number(value).toLocaleString('en-US'), 'Total Users']}
|
||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||
/>
|
||||
|
||||
@ -15,6 +15,7 @@ import { MonthlyNewUsersChart } from './monthly-new-users-chart';
|
||||
import { MonthlyTotalUsersChart } from './monthly-total-users-chart';
|
||||
import { TeamMembers } from './team-members';
|
||||
import { OpenPageTooltip } from './tooltip';
|
||||
import { Typefully } from './typefully';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Open Startup',
|
||||
@ -237,6 +238,8 @@ export default async function OpenPage() {
|
||||
<MonthlyTotalUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
|
||||
<MonthlyNewUsersChart data={MONTHLY_USERS} className="col-span-12 lg:col-span-6" />
|
||||
|
||||
<Typefully className="col-span-12 lg:col-span-6" />
|
||||
|
||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||
<h2 className="text-2xl font-bold">Where's the rest?</h2>
|
||||
|
||||
|
||||
39
apps/marketing/src/app/(marketing)/open/typefully.tsx
Normal file
39
apps/marketing/src/app/(marketing)/open/typefully.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export type TypefullyProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const Typefully = ({ className, ...props }: TypefullyProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)} {...props}>
|
||||
<h3 className="px-4 text-lg font-semibold">Twitter Stats</h3>
|
||||
|
||||
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border py-8 shadow-sm hover:shadow">
|
||||
<div className="flex flex-col items-center gap-y-4 text-center">
|
||||
<FaXTwitter className="h-12 w-12" />
|
||||
<Link href="https://typefully.com/documenso/stats" target="_blank">
|
||||
<h1>Documenso on X</h1>
|
||||
</Link>
|
||||
<Button className="rounded-full" size="sm" asChild>
|
||||
<Link href="https://typefully.com/documenso/stats" target="_blank">
|
||||
View all stats
|
||||
</Link>
|
||||
</Button>
|
||||
<Button className="rounded-full bg-white" size="sm" asChild>
|
||||
<Link href="https://twitter.com/documenso" target="_blank">
|
||||
Follow us on X
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -203,7 +203,7 @@ export const SinglePlayerClient = () => {
|
||||
target="_blank"
|
||||
className="hover:text-foreground/80 font-semibold transition-colors"
|
||||
>
|
||||
community plan
|
||||
early adopter plan
|
||||
</Link>{' '}
|
||||
for exclusive features, including the ability to collaborate with multiple signers.
|
||||
</p>
|
||||
|
||||
@ -40,7 +40,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
Claim Early Adopter Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
$30/mo
|
||||
</span>
|
||||
|
||||
@ -114,7 +114,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
Claim Early Adopter Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
$30/mo
|
||||
</span>
|
||||
@ -225,7 +225,8 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
<span className="bg-primary text-black">
|
||||
(in a non-legally binding, but heartfelt way)
|
||||
</span>{' '}
|
||||
and lock in the community plan for forever, including everything we build this year.
|
||||
and lock in the early adopter plan for forever, including everything we build this
|
||||
year.
|
||||
</p>
|
||||
|
||||
<div className="flex h-24 items-center">
|
||||
|
||||
@ -102,7 +102,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-plan="community"
|
||||
data-plan="early-adopter"
|
||||
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
|
||||
>
|
||||
<p className="text-foreground text-4xl font-medium">Early Adopters</p>
|
||||
@ -119,7 +119,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
|
||||
<Button className="mt-6 rounded-full text-base" asChild>
|
||||
<Link
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-community`}
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup?utm_source=pricing-early-adopter`}
|
||||
target="_blank"
|
||||
>
|
||||
Signup Now
|
||||
|
||||
@ -199,7 +199,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<h3 className="text-xl font-semibold">Sign up to Community Plan</h3>
|
||||
<h3 className="text-xl font-semibold">Sign up to Early Adopter Plan</h3>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
with Timur Ercan & Lucas Smith from Documenso
|
||||
</p>
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
"@documenso/ee": "*",
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
@ -46,18 +45,19 @@
|
||||
"remeda": "^1.27.1",
|
||||
"sharp": "0.33.1",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"typescript": "5.2.2",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uqr": "^0.1.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/ua-parser-js": "^0.7.39"
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"next-auth": {
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { type Document, DocumentStatus } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AdminActionsProps = {
|
||||
className?: string;
|
||||
document: Document;
|
||||
};
|
||||
|
||||
export const AdminActions = ({ className, document }: AdminActionsProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutate: resealDocument, isLoading: isResealDocumentLoading } =
|
||||
trpc.admin.resealDocument.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Document resealed',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to reseal document',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-x-4', className)}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
loading={isResealDocumentLoading}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
onClick={() => resealDocument({ id: document.id })}
|
||||
>
|
||||
Reseal document
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="max-w-[40ch]">
|
||||
Attempts sealing the document again, useful for after a code change has occurred to
|
||||
resolve an erroneous document.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/admin/users/${document.userId}`}>Go to owner</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
86
apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx
Normal file
86
apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@documenso/ui/primitives/accordion';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
import { AdminActions } from './admin-actions';
|
||||
import { RecipientItem } from './recipient-item';
|
||||
|
||||
type AdminDocumentDetailsPageProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
|
||||
const document = await getEntireDocument({ id: Number(params.id) });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<h1 className="text-2xl font-semibold">{document.title}</h1>
|
||||
<DocumentStatus status={document.status} />
|
||||
</div>
|
||||
|
||||
{document.deletedAt && (
|
||||
<Badge size="large" variant="destructive">
|
||||
Deleted
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground mt-4 text-sm">
|
||||
<div>
|
||||
Created on: <LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} />
|
||||
</div>
|
||||
<div>
|
||||
Last updated at: <LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<h2 className="text-lg font-semibold">Admin Actions</h2>
|
||||
|
||||
<AdminActions className="mt-2" document={document} />
|
||||
|
||||
<hr className="my-4" />
|
||||
<h2 className="text-lg font-semibold">Recipients</h2>
|
||||
|
||||
<div className="mt-4">
|
||||
<Accordion type="multiple" className="space-y-4">
|
||||
{document.Recipient.map((recipient) => (
|
||||
<AccordionItem
|
||||
key={recipient.id}
|
||||
value={recipient.id.toString()}
|
||||
className="rounded-lg border"
|
||||
>
|
||||
<AccordionTrigger className="px-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<h4 className="font-semibold">{recipient.name}</h4>
|
||||
<Badge size="small" variant="neutral">
|
||||
{recipient.email}
|
||||
</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="border-t px-4 pt-4">
|
||||
<RecipientItem recipient={recipient} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type Field,
|
||||
type Recipient,
|
||||
type Signature,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ZAdminUpdateRecipientFormSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type TAdminUpdateRecipientFormSchema = z.infer<typeof ZAdminUpdateRecipientFormSchema>;
|
||||
|
||||
export type RecipientItemProps = {
|
||||
recipient: Recipient & {
|
||||
Field: Array<
|
||||
Field & {
|
||||
Signature: Signature | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
export const RecipientItem = ({ recipient }: RecipientItemProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<TAdminUpdateRecipientFormSchema>({
|
||||
defaultValues: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation();
|
||||
|
||||
const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => {
|
||||
try {
|
||||
await updateRecipient({
|
||||
id: recipient.id,
|
||||
name,
|
||||
email,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Recipient updated',
|
||||
description: 'The recipient has been updated successfully',
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Failed to update recipient',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onUpdateRecipientFormSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full max-w-xl flex-col gap-y-4"
|
||||
disabled={
|
||||
form.formState.isSubmitting || recipient.signingStatus === SigningStatus.SIGNED
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>Name</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>Email</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Update Recipient
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<h2 className="mb-4 text-lg font-semibold">Fields</h2>
|
||||
|
||||
<DataTable
|
||||
data={recipient.Field}
|
||||
columns={[
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
cell: ({ row }) => <div>{row.original.id}</div>,
|
||||
},
|
||||
{
|
||||
header: 'Type',
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => <div>{row.original.type}</div>,
|
||||
},
|
||||
{
|
||||
header: 'Inserted',
|
||||
accessorKey: 'inserted',
|
||||
cell: ({ row }) => <div>{row.original.inserted ? 'True' : 'False'}</div>,
|
||||
},
|
||||
{
|
||||
header: 'Value',
|
||||
accessorKey: 'customText',
|
||||
cell: ({ row }) => <div>{row.original.customText}</div>,
|
||||
},
|
||||
{
|
||||
header: 'Signature',
|
||||
accessorKey: 'signature',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
{row.original.Signature?.typedSignature && (
|
||||
<span>{row.original.Signature.typedSignature}</span>
|
||||
)}
|
||||
|
||||
{row.original.Signature?.signatureImageAsBase64 && (
|
||||
<img
|
||||
src={row.original.Signature.signatureImageAsBase64}
|
||||
alt="Signature"
|
||||
className="h-12 w-full dark:invert"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,125 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Document, User } from '@documenso/prisma/client';
|
||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export type DocumentsDataTableProps = {
|
||||
results: FindResultSet<
|
||||
Document & {
|
||||
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const onPaginationChange = (page: number, perPage: number) => {
|
||||
startTransition(() => {
|
||||
updateSearchParams({
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
header: 'Created',
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
},
|
||||
{
|
||||
header: 'Title',
|
||||
accessorKey: 'title',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="block max-w-[5rem] truncate font-medium md:max-w-[10rem]">
|
||||
{row.original.title}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Owner',
|
||||
accessorKey: 'owner',
|
||||
cell: ({ row }) => {
|
||||
const avatarFallbackText = row.original.User.name
|
||||
? extractInitials(row.original.User.name)
|
||||
: row.original.User.email.slice(0, 1).toUpperCase();
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger>
|
||||
<Link href={`/admin/users/${row.original.User.id}`}>
|
||||
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarFallback className="text-xs text-gray-400">
|
||||
{avatarFallbackText}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex max-w-xs items-center gap-2">
|
||||
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarFallback className="text-xs text-gray-400">
|
||||
{avatarFallbackText}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="text-muted-foreground flex flex-col text-sm">
|
||||
<span>{row.original.User.name}</span>
|
||||
<span>{row.original.User.email}</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Last updated',
|
||||
accessorKey: 'updatedAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
|
||||
},
|
||||
]}
|
||||
data={results.data}
|
||||
perPage={results.perPage}
|
||||
currentPage={results.currentPage}
|
||||
totalPages={results.totalPages}
|
||||
onPaginationChange={onPaginationChange}
|
||||
>
|
||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||
</DataTable>
|
||||
|
||||
{isPending && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
// export type AdminDocumentResultsProps = {};
|
||||
|
||||
export const AdminDocumentResults = () => {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const [term, setTerm] = useState(() => searchParams?.get?.('term') ?? '');
|
||||
const debouncedTerm = useDebouncedValue(term, 500);
|
||||
|
||||
const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
|
||||
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
|
||||
|
||||
const { data: findDocumentsData, isLoading: isFindDocumentsLoading } =
|
||||
trpc.admin.findDocuments.useQuery(
|
||||
{
|
||||
term: debouncedTerm,
|
||||
page: page || 1,
|
||||
perPage: perPage || 20,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
const onPaginationChange = (newPage: number, newPerPage: number) => {
|
||||
updateSearchParams({
|
||||
page: newPage,
|
||||
perPage: newPerPage,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search by document title"
|
||||
value={term}
|
||||
onChange={(e) => setTerm(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="relative mt-4">
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
header: 'Created',
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
|
||||
},
|
||||
{
|
||||
header: 'Title',
|
||||
accessorKey: 'title',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/admin/documents/${row.original.id}`}
|
||||
className="block max-w-[5rem] truncate font-medium hover:underline md:max-w-[10rem]"
|
||||
>
|
||||
{row.original.title}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
header: 'Owner',
|
||||
accessorKey: 'owner',
|
||||
cell: ({ row }) => {
|
||||
const avatarFallbackText = row.original.User.name
|
||||
? extractInitials(row.original.User.name)
|
||||
: row.original.User.email.slice(0, 1).toUpperCase();
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger>
|
||||
<Link href={`/admin/users/${row.original.User.id}`}>
|
||||
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarFallback className="text-xs text-gray-400">
|
||||
{avatarFallbackText}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="flex max-w-xs items-center gap-2">
|
||||
<Avatar className="dark:border-border h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarFallback className="text-xs text-gray-400">
|
||||
{avatarFallbackText}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="text-muted-foreground flex flex-col text-sm">
|
||||
<span>{row.original.User.name}</span>
|
||||
<span>{row.original.User.email}</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Last updated',
|
||||
accessorKey: 'updatedAt',
|
||||
cell: ({ row }) => <LocaleDate date={row.original.updatedAt} />,
|
||||
},
|
||||
]}
|
||||
data={findDocumentsData?.data ?? []}
|
||||
perPage={findDocumentsData?.perPage ?? 20}
|
||||
currentPage={findDocumentsData?.currentPage ?? 1}
|
||||
totalPages={findDocumentsData?.totalPages ?? 1}
|
||||
onPaginationChange={onPaginationChange}
|
||||
>
|
||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||
</DataTable>
|
||||
|
||||
{isFindDocumentsLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,28 +1,12 @@
|
||||
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
|
||||
|
||||
import { DocumentsDataTable } from './data-table';
|
||||
|
||||
export type DocumentsPageProps = {
|
||||
searchParams?: {
|
||||
page?: string;
|
||||
perPage?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Documents({ searchParams = {} }: DocumentsPageProps) {
|
||||
const page = Number(searchParams.page) || 1;
|
||||
const perPage = Number(searchParams.perPage) || 20;
|
||||
|
||||
const results = await findDocuments({
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
import { AdminDocumentResults } from './document-results';
|
||||
|
||||
export default function AdminDocumentsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-4xl font-semibold">Manage documents</h2>
|
||||
|
||||
<div className="mt-8">
|
||||
<DocumentsDataTable results={results} />
|
||||
<AdminDocumentResults />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteUserDialogProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: deleteUser, isLoading: isDeletingUser } =
|
||||
trpc.admin.deleteUser.useMutation();
|
||||
|
||||
const onDeleteAccount = async () => {
|
||||
try {
|
||||
await deleteUser({
|
||||
id: user.id,
|
||||
email,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Account deleted',
|
||||
description: 'The account has been deleted successfully.',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.push('/admin/users');
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: 'An error occurred',
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
variant: 'destructive',
|
||||
description:
|
||||
err.message ??
|
||||
'We encountered an unknown error while attempting to delete your account. Please try again later.',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>Delete Account</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
Delete the users account and all its contents. This action is irreversible and will
|
||||
cancel their subscription, so proceed with caution.
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Delete Account</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="space-y-4">
|
||||
<DialogTitle>Delete Account</DialogTitle>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="selection:bg-red-100">
|
||||
This action is not reversible. Please be certain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<DialogDescription>
|
||||
To confirm, please enter the accounts email address <br />({user.email}).
|
||||
</DialogDescription>
|
||||
|
||||
<Input
|
||||
className="mt-2"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={onDeleteAccount}
|
||||
loading={isDeletingUser}
|
||||
variant="destructive"
|
||||
disabled={email !== user.email}
|
||||
>
|
||||
{isDeletingUser ? 'Deleting account...' : 'Delete Account'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -7,7 +7,7 @@ import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
|
||||
import { ZAdminUpdateProfileMutationSchema } from '@documenso/trpc/server/admin-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
@ -20,9 +20,10 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { DeleteUserDialog } from './delete-user-dialog';
|
||||
import { MultiSelectRoleCombobox } from './multiselect-role-combobox';
|
||||
|
||||
const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
|
||||
const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
|
||||
|
||||
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
|
||||
|
||||
@ -137,6 +138,10 @@ export default function UserPage({ params }: { params: { id: number } }) {
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
{user && <DeleteUserDialog user={user} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ export default async function AdminManageUsers({ searchParams = {} }: AdminManag
|
||||
|
||||
const [{ users, totalPages }, individualPrices] = await Promise.all([
|
||||
search(searchString, page, perPage),
|
||||
getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY),
|
||||
getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY).catch(() => []),
|
||||
]);
|
||||
|
||||
const individualPriceIds = individualPrices.map((price) => price.id);
|
||||
|
||||
@ -27,15 +27,16 @@ export const ClaimProfileAlertDialog = ({ className, user }: ClaimProfileAlertDi
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>Claim your profile</AlertTitle>
|
||||
<AlertTitle>{user.url ? 'Update your profile' : 'Claim your profile'}</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
Profiles are coming soon! Claim your profile username now to reserve your corner of the
|
||||
signing revolution.
|
||||
{user.url
|
||||
? 'Profiles are coming soon! Update your profile username to reserve your corner of the signing revolution.'
|
||||
: 'Profiles are coming soon! Claim your profile username now to reserve your corner of the signing revolution.'}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Button onClick={() => setOpen(true)}>Claim Now</Button>
|
||||
<Button onClick={() => setOpen(true)}>{user.url ? 'Update Now' : 'Claim Now'}</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
|
||||
<CardContent className="flex flex-row items-center justify-between p-4">
|
||||
<div className="flex flex-col text-sm">
|
||||
<p className="text-foreground font-semibold">
|
||||
Current plan: {teamSubscription ? 'Team' : 'Community Team'}
|
||||
Current plan: {teamSubscription ? 'Team' : 'Early Adopter Team'}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-0.5">
|
||||
|
||||
@ -57,7 +57,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
key={href}
|
||||
href={`${rootHref}${href}`}
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||
{
|
||||
'text-foreground dark:text-muted-foreground': pathname?.startsWith(
|
||||
`${rootHref}${href}`,
|
||||
|
||||
@ -7,7 +7,6 @@ import { useParams } from 'next/navigation';
|
||||
|
||||
import { MenuIcon, SearchIcon } from 'lucide-react';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { getRootHref } from '@documenso/lib/utils/params';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
@ -19,7 +18,6 @@ import { CommandMenu } from '../common/command-menu';
|
||||
import { DesktopNav } from './desktop-nav';
|
||||
import { MenuSwitcher } from './menu-switcher';
|
||||
import { MobileNavigation } from './mobile-navigation';
|
||||
import { ProfileDropdown } from './profile-dropdown';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
user: User;
|
||||
@ -29,10 +27,6 @@ export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
const params = useParams();
|
||||
|
||||
const { getFlag } = useFeatureFlags();
|
||||
|
||||
const isTeamsEnabled = getFlag('app_teams');
|
||||
|
||||
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
@ -47,34 +41,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
if (!isTeamsEnabled) {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
|
||||
scrollY > 5 && 'border-b-border',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="focus-visible:ring-ring ring-offset-background rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Logo className="h-6 w-auto" />
|
||||
</Link>
|
||||
|
||||
<DesktopNav />
|
||||
|
||||
<div className="flex gap-x-4 md:ml-8">
|
||||
<ProfileDropdown user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
@ -86,7 +52,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
||||
<Link
|
||||
href={getRootHref(params)}
|
||||
href={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
||||
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
||||
>
|
||||
<Logo className="h-6 w-auto" />
|
||||
|
||||
@ -1,177 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
Braces,
|
||||
CreditCard,
|
||||
FileSpreadsheet,
|
||||
Lock,
|
||||
LogOut,
|
||||
User as LucideUser,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
Sun,
|
||||
UserCog,
|
||||
} from 'lucide-react';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
|
||||
export type ProfileDropdownProps = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
||||
const { getFlag } = useFeatureFlags();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const isUserAdmin = isAdmin(user);
|
||||
|
||||
const isBillingEnabled = getFlag('app_billing');
|
||||
|
||||
const avatarFallback = user.name
|
||||
? extractInitials(user.name)
|
||||
: user.email.slice(0, 1).toUpperCase();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
title="Profile Dropdown"
|
||||
className="relative h-10 w-10 rounded-full"
|
||||
>
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarFallback>{avatarFallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="z-[60] w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel>Account</DropdownMenuLabel>
|
||||
|
||||
{isUserAdmin && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin" className="cursor-pointer">
|
||||
<UserCog className="mr-2 h-4 w-4" />
|
||||
Admin
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/profile" className="cursor-pointer">
|
||||
<LucideUser className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/security" className="cursor-pointer">
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
Security
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/tokens" className="cursor-pointer">
|
||||
<Braces className="mr-2 h-4 w-4" />
|
||||
API Tokens
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{isBillingEnabled && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/billing" className="cursor-pointer">
|
||||
<CreditCard className="mr-2 h-4 w-4" />
|
||||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/templates" className="cursor-pointer">
|
||||
<FileSpreadsheet className="mr-2 h-4 w-4" />
|
||||
Templates
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Palette className="mr-2 h-4 w-4" />
|
||||
Themes
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="z-[60]">
|
||||
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
||||
<DropdownMenuRadioItem value="light">
|
||||
<Sun className="mr-2 h-4 w-4" /> Light
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="dark">
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
Dark
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="system">
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
System
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
className="cursor-pointer"
|
||||
target="_blank"
|
||||
>
|
||||
<LuGithub className="mr-2 h-4 w-4" />
|
||||
Star on Github
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
void signOut({
|
||||
callbackUrl: '/',
|
||||
})
|
||||
}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -19,7 +19,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
const { getFlag } = useFeatureFlags();
|
||||
|
||||
const isBillingEnabled = getFlag('app_billing');
|
||||
const isTeamsEnabled = getFlag('app_teams');
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-y-2', className)} {...props}>
|
||||
@ -36,20 +35,18 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isTeamsEnabled && (
|
||||
<Link href="/settings/teams">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/teams') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
Teams
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/settings/teams">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/teams') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
Teams
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/security">
|
||||
<Button
|
||||
|
||||
@ -19,7 +19,6 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
const { getFlag } = useFeatureFlags();
|
||||
|
||||
const isBillingEnabled = getFlag('app_billing');
|
||||
const isTeamsEnabled = getFlag('app_teams');
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -39,20 +38,18 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isTeamsEnabled && (
|
||||
<Link href="/settings/teams">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/teams') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
Teams
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/settings/teams">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/teams') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
Teams
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/security">
|
||||
<Button
|
||||
|
||||
@ -119,8 +119,6 @@ export const SignUpFormV2 = ({
|
||||
form.formState.dirtyFields.signature &&
|
||||
form.formState.errors.signature === undefined;
|
||||
|
||||
console.log({ formSTate: form.formState });
|
||||
|
||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
|
||||
@ -421,8 +419,7 @@ export const SignUpFormV2 = ({
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
disabled={step === 'BASIC_DETAILS'}
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={step === 'BASIC_DETAILS' || form.formState.isSubmitting}
|
||||
onClick={() => setStep('BASIC_DETAILS')}
|
||||
>
|
||||
Back
|
||||
|
||||
@ -32,6 +32,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk add --no-cache jq
|
||||
# Required for node_modules/aws-crt
|
||||
RUN apk add --no-cache make cmake g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Disable husky from installing hooks
|
||||
@ -80,6 +81,7 @@ WORKDIR /app
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
USER nextjs
|
||||
|
||||
COPY --from=installer /app/apps/web/next.config.js .
|
||||
@ -91,4 +93,18 @@ COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
|
||||
|
||||
CMD node apps/web/server.js
|
||||
# Copy the prisma binary, schema and migrations
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/prisma/schema.prisma ./packages/prisma/schema.prisma
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/prisma/migrations ./packages/prisma/migrations
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/node_modules/prisma/ ./node_modules/prisma/
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/node_modules/@prisma/ ./node_modules/@prisma/
|
||||
|
||||
# Symlink the prisma binary
|
||||
RUN mkdir node_modules/.bin
|
||||
RUN ln -s /app/node_modules/prisma/build/index.js ./node_modules/.bin/prisma
|
||||
|
||||
|
||||
# Get the start script from docker/start.sh
|
||||
COPY --chown=nextjs:nodejs ./docker/start.sh ./start.sh
|
||||
|
||||
CMD ["sh", "start.sh"]
|
||||
|
||||
128
docker/README.md
Normal file
128
docker/README.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Docker Setup for Documenso
|
||||
|
||||
The following guide will walk you through setting up Documenso using Docker. You can choose between a production setup using Docker Compose or a standalone container.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure that you have the following installed:
|
||||
|
||||
- Docker
|
||||
- Docker Compose (if using the Docker Compose setup)
|
||||
|
||||
## Option 1: Production Docker Compose Setup
|
||||
|
||||
This setup includes a PostgreSQL database and the Documenso application. You will need to provide your own SMTP details via environment variables.
|
||||
|
||||
1. Download the Docker Compose file from the Documenso repository: [compose.yml](https://raw.githubusercontent.com/documenso/documenso/release/docker/production/compose.yml)
|
||||
2. Navigate to the directory containing the `compose.yml` file.
|
||||
3. Create a `.env` file in the same directory and add your SMTP details as well as a few extra environment variables, following the example below:
|
||||
|
||||
```
|
||||
NEXTAUTH_SECRET="<your-secret>"
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY="<your-key>"
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-secondary-key>"
|
||||
NEXT_PUBLIC_WEBAPP_URL="<your-url>"
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
|
||||
NEXT_PRIVATE_SMTP_HOST="<your-host>"
|
||||
NEXT_PRIVATE_SMTP_PORT=<your-port>
|
||||
NEXT_PRIVATE_SMTP_USERNAME="<your-username>"
|
||||
NEXT_PRIVATE_SMTP_PASSWORD="<your-password>"
|
||||
```
|
||||
|
||||
4. Run the following command to start the containers:
|
||||
|
||||
```
|
||||
docker-compose --env-file ./.env -d up
|
||||
```
|
||||
|
||||
This will start the PostgreSQL database and the Documenso application containers.
|
||||
|
||||
5. Access the Documenso application by visiting `http://localhost:3000` in your web browser.
|
||||
|
||||
## Option 2: Standalone Docker Container
|
||||
|
||||
If you prefer to host the Documenso application on your container provider of choice, you can use the pre-built Docker image from DockerHub or GitHub's Package Registry. Note that you will need to provide your own database and SMTP host.
|
||||
|
||||
1. Pull the Documenso Docker image:
|
||||
|
||||
```
|
||||
docker pull documenso/documenso
|
||||
```
|
||||
|
||||
Or, if using GitHub's Package Registry:
|
||||
|
||||
```
|
||||
docker pull ghcr.io/documenso/documenso
|
||||
```
|
||||
|
||||
2. Run the Docker container, providing the necessary environment variables for your database and SMTP host:
|
||||
|
||||
```
|
||||
docker run -d \
|
||||
-p 3000:3000 \
|
||||
-e POSTGRES_USER="<your-postgres-user>"
|
||||
-e POSTGRES_PASSWORD="<your-postgres-password>"
|
||||
-e POSTGRES_DB="<your-postgres-db>"
|
||||
-e NEXTAUTH_URL="<your-nextauth-url>"
|
||||
-e NEXTAUTH_SECRET="<your-nextauth-secret>"
|
||||
-e NEXT_PRIVATE_ENCRYPTION_KEY="<your-next-private-encryption-key>"
|
||||
-e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-next-private-encryption-secondary-key>"
|
||||
-e NEXT_PUBLIC_WEBAPP_URL="<your-next-public-webapp-url>"
|
||||
-e NEXT_PRIVATE_DATABASE_URL="<your-next-private-database-url>"
|
||||
-e NEXT_PRIVATE_SMTP_TRANSPORT="<your-next-private-smtp-transport>"
|
||||
-e NEXT_PRIVATE_SMTP_FROM_NAME="<your-next-private-smtp-from-name>"
|
||||
-e NEXT_PRIVATE_SMTP_FROM_ADDRESS="<your-next-private-smtp-from-address>"
|
||||
documenso/documenso
|
||||
```
|
||||
|
||||
Replace the placeholders with your actual database and SMTP details.
|
||||
|
||||
1. Access the Documenso application by visiting the URL you provided in the `NEXT_PUBLIC_WEBAPP_URL` environment variable in your web browser.
|
||||
|
||||
## Success
|
||||
|
||||
You have now successfully set up Documenso using Docker. You can start organizing and managing your documents efficiently. If you encounter any issues or have further questions, please refer to the official Documenso documentation or seek assistance from the community.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
The environment variables listed above are a subset of those that are available for configuring Documenso. For a complete list of environment variables and their descriptions, refer to the table below:
|
||||
|
||||
Here's a markdown table documenting all the provided environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | The port to run the Documenso application on, defaults to `3000`. |
|
||||
| `NEXTAUTH_URL` | The URL for the NextAuth.js authentication service. |
|
||||
| `NEXTAUTH_SECRET` | The secret key used by NextAuth.js for encryption and signing. |
|
||||
| `NEXT_PRIVATE_ENCRYPTION_KEY` | The primary encryption key for symmetric encryption and decryption (at least 32 characters). |
|
||||
| `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | The secondary encryption key for symmetric encryption and decryption (at least 32 characters). |
|
||||
| `NEXT_PRIVATE_GOOGLE_CLIENT_ID` | The Google client ID for Google authentication (optional). |
|
||||
| `NEXT_PRIVATE_GOOGLE_CLIENT_SECRET` | The Google client secret for Google authentication (optional). |
|
||||
| `NEXT_PUBLIC_WEBAPP_URL` | The URL for the web application. |
|
||||
| `NEXT_PRIVATE_DATABASE_URL` | The URL for the primary database connection (with connection pooling). |
|
||||
| `NEXT_PRIVATE_DIRECT_DATABASE_URL` | The URL for the direct database connection (without connection pooling). |
|
||||
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | The transport to use for file uploads (database or s3). |
|
||||
| `NEXT_PRIVATE_UPLOAD_ENDPOINT` | The endpoint for the S3 storage transport (for third-party S3-compatible providers). |
|
||||
| `NEXT_PRIVATE_UPLOAD_REGION` | The region for the S3 storage transport (defaults to us-east-1). |
|
||||
| `NEXT_PRIVATE_UPLOAD_BUCKET` | The bucket to use for the S3 storage transport. |
|
||||
| `NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID` | The access key ID for the S3 storage transport. |
|
||||
| `NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY` | The secret access key for the S3 storage transport. |
|
||||
| `NEXT_PRIVATE_SMTP_TRANSPORT` | The transport to use for sending emails (smtp-auth, smtp-api, resend, or mailchannels). |
|
||||
| `NEXT_PRIVATE_SMTP_HOST` | The host for the SMTP server for SMTP transports. |
|
||||
| `NEXT_PRIVATE_SMTP_PORT` | The port for the SMTP server for SMTP transports. |
|
||||
| `NEXT_PRIVATE_SMTP_USERNAME` | The username for the SMTP server for the `smtp-auth` transport. |
|
||||
| `NEXT_PRIVATE_SMTP_PASSWORD` | The password for the SMTP server for the `smtp-auth` transport. |
|
||||
| `NEXT_PRIVATE_SMTP_APIKEY_USER` | The API key user for the SMTP server for the `smtp-api` transport. |
|
||||
| `NEXT_PRIVATE_SMTP_APIKEY` | The API key for the SMTP server for the `smtp-api` transport. |
|
||||
| `NEXT_PRIVATE_SMTP_SECURE` | Whether to force the use of TLS for the SMTP server for SMTP transports. |
|
||||
| `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | The email address for the "from" address. |
|
||||
| `NEXT_PRIVATE_SMTP_FROM_NAME` | The sender name for the "from" address. |
|
||||
| `NEXT_PRIVATE_RESEND_API_KEY` | The API key for Resend.com for the `resend` transport. |
|
||||
| `NEXT_PRIVATE_MAILCHANNELS_API_KEY` | The optional API key for MailChannels (if using a proxy) for the `mailchannels` transport. |
|
||||
| `NEXT_PRIVATE_MAILCHANNELS_ENDPOINT` | The optional endpoint for the MailChannels API (if using a proxy) for the `mailchannels` transport. |
|
||||
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN` | The domain for DKIM signing with MailChannels for the `mailchannels` transport. |
|
||||
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR` | The selector for DKIM signing with MailChannels for the `mailchannels` transport. |
|
||||
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY` | The private key for DKIM signing with MailChannels for the `mailchannels` transport. |
|
||||
| `NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT` | The maximum document upload limit displayed to the user (in MB). |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | The optional PostHog key for analytics and feature flags. |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Whether to disable user signups through the /signup page. |
|
||||
@ -5,15 +5,10 @@ command -v docker >/dev/null 2>&1 || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "jq is not installed. Please install jq and try again."
|
||||
exit 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
APP_VERSION="$(jq -r '.version' "$MONOREPO_ROOT/apps/web/package.json")"
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
@ -22,7 +17,10 @@ echo "Git SHA: $GIT_SHA"
|
||||
|
||||
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||
--progress=plain \
|
||||
-t "documenso:latest" \
|
||||
-t "documenso:$GIT_SHA" \
|
||||
-t "documenso:$APP_VERSION" \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
29
docker/buildx-and-push.sh
Executable file
29
docker/buildx-and-push.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "Docker is not running. Please start Docker and try again."
|
||||
exit 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
echo "App version: $APP_VERSION"
|
||||
echo "Git SHA: $GIT_SHA"
|
||||
|
||||
docker buildx build \
|
||||
-f "$SCRIPT_DIR/Dockerfile" \
|
||||
--platformlinux/amd64,linux/arm64 \
|
||||
--progress=plain \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
--push \
|
||||
"$MONOREPO_ROOT"
|
||||
28
docker/buildx.sh
Executable file
28
docker/buildx.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "Docker is not running. Please start Docker and try again."
|
||||
exit 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
echo "App version: $APP_VERSION"
|
||||
echo "Git SHA: $GIT_SHA"
|
||||
|
||||
docker buildx build \
|
||||
-f "$SCRIPT_DIR/Dockerfile" \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--progress=plain \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
cd "$MONOREPO_ROOT"
|
||||
|
||||
npm ci
|
||||
|
||||
npm run prisma:migrate-dev
|
||||
|
||||
npm run dev
|
||||
@ -1,32 +0,0 @@
|
||||
name: documenso_test
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=documenso
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=documenso
|
||||
ports:
|
||||
- 54322:5432
|
||||
|
||||
inbucket:
|
||||
image: inbucket/inbucket
|
||||
# ports:
|
||||
# - 9000:9000
|
||||
# - 2500:2500
|
||||
# - 1100:1100
|
||||
|
||||
documenso:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: docker/Dockerfile
|
||||
depends_on:
|
||||
- database
|
||||
- inbucket
|
||||
env_file:
|
||||
- ../.env.example
|
||||
environment:
|
||||
- NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
ports:
|
||||
- 3000:3000
|
||||
@ -1,19 +0,0 @@
|
||||
name: documenso
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
container_name: database
|
||||
environment:
|
||||
- POSTGRES_USER=documenso
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=documenso
|
||||
ports:
|
||||
- 54320:5432
|
||||
|
||||
inbucket:
|
||||
image: inbucket/inbucket
|
||||
container_name: mailserver
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 2500:2500
|
||||
- 1100:1100
|
||||
@ -1,40 +0,0 @@
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=documenso
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=documenso
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
inbucket:
|
||||
image: inbucket/inbucket
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 2500:2500
|
||||
- 1100:1100
|
||||
|
||||
documenso:
|
||||
image: node:18
|
||||
working_dir: /app
|
||||
command: ./docker/compose-entrypoint.sh
|
||||
depends_on:
|
||||
- database
|
||||
- inbucket
|
||||
environment:
|
||||
- NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
- NEXTAUTH_SECRET=my-super-secure-secret
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- SENDGRID_API_KEY=
|
||||
- SMTP_MAIL_HOST=inbucket
|
||||
- SMTP_MAIL_PORT=2500
|
||||
- SMTP_MAIL_USER=username
|
||||
- SMTP_MAIL_PASSWORD=password
|
||||
- MAIL_FROM=admin@example.com
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ../:/app
|
||||
@ -1,4 +1,5 @@
|
||||
name: documenso-services
|
||||
name: documenso-development
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
64
docker/production/compose.yml
Normal file
64
docker/production/compose.yml
Normal file
@ -0,0 +1,64 @@
|
||||
name: documenso-production
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:?err}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
|
||||
- POSTGRES_DB=${POSTGRES_DB:?err}
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
|
||||
documenso:
|
||||
image: documenso/documenso:latest
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- PORT=${PORT:-3000}
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL:-${NEXT_PUBLIC_WEBAPP_URL}}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
||||
- NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY:?err}
|
||||
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY:?err}
|
||||
- NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID}
|
||||
- NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET}
|
||||
- NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:?err}
|
||||
- NEXT_PUBLIC_MARKETING_URL=${NEXT_PUBLIC_MARKETING_URL:-https://documenso.com}
|
||||
- NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:?err}
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL:-${NEXT_PRIVATE_DATABASE_URL}}
|
||||
- NEXT_PUBLIC_UPLOAD_TRANSPORT=${NEXT_PUBLIC_UPLOAD_TRANSPORT:-database}
|
||||
- NEXT_PRIVATE_UPLOAD_ENDPOINT=${NEXT_PRIVATE_UPLOAD_ENDPOINT}
|
||||
- NEXT_PRIVATE_UPLOAD_REGION=${NEXT_PRIVATE_UPLOAD_REGION}
|
||||
- NEXT_PRIVATE_UPLOAD_BUCKET=${NEXT_PRIVATE_UPLOAD_BUCKET}
|
||||
- NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=${NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID}
|
||||
- NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=${NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY}
|
||||
- NEXT_PRIVATE_SMTP_TRANSPORT=${NEXT_PRIVATE_SMTP_TRANSPORT:?err}
|
||||
- NEXT_PRIVATE_SMTP_HOST=${NEXT_PRIVATE_SMTP_HOST}
|
||||
- NEXT_PRIVATE_SMTP_PORT=${NEXT_PRIVATE_SMTP_PORT}
|
||||
- NEXT_PRIVATE_SMTP_USERNAME=${NEXT_PRIVATE_SMTP_USERNAME}
|
||||
- NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD}
|
||||
- NEXT_PRIVATE_SMTP_APIKEY_USER=${NEXT_PRIVATE_SMTP_APIKEY_USER}
|
||||
- NEXT_PRIVATE_SMTP_APIKEY=${NEXT_PRIVATE_SMTP_APIKEY}
|
||||
- NEXT_PRIVATE_SMTP_SECURE=${NEXT_PRIVATE_SMTP_SECURE}
|
||||
- NEXT_PRIVATE_SMTP_FROM_NAME=${NEXT_PRIVATE_SMTP_FROM_NAME:?err}
|
||||
- NEXT_PRIVATE_SMTP_FROM_ADDRESS=${NEXT_PRIVATE_SMTP_FROM_ADDRESS:?err}
|
||||
- NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY}
|
||||
- NEXT_PRIVATE_MAILCHANNELS_API_KEY=${NEXT_PRIVATE_MAILCHANNELS_API_KEY}
|
||||
- NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=${NEXT_PRIVATE_MAILCHANNELS_ENDPOINT}
|
||||
- NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=${NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN}
|
||||
- NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=${NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR}
|
||||
- NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=${NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY}
|
||||
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
|
||||
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
|
||||
ports:
|
||||
- ${PORT:-3000}:${PORT:-3000}
|
||||
|
||||
volumes:
|
||||
database:
|
||||
7
docker/start.sh
Executable file
7
docker/start.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -x
|
||||
|
||||
npx prisma migrate deploy --schema ./packages/prisma/schema.prisma
|
||||
|
||||
node apps/web/server.js
|
||||
51
docker/testing/compose.yml
Normal file
51
docker/testing/compose.yml
Normal file
@ -0,0 +1,51 @@
|
||||
name: documenso-test
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=documenso
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=documenso
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U documenso']
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- 54322:5432
|
||||
|
||||
inbucket:
|
||||
image: inbucket/inbucket
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 2500:2500
|
||||
- 1100:1100
|
||||
|
||||
documenso:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: docker/Dockerfile
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
inbucket:
|
||||
condition: service_started
|
||||
env_file:
|
||||
- ../../.env.example
|
||||
environment:
|
||||
- NEXTAUTH_SECRET=secret
|
||||
- NEXT_PRIVATE_ENCRYPTION_KEY=CAFEBABE
|
||||
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=DEADBEEF
|
||||
- NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PUBLIC_UPLOAD_TRANSPORT=database
|
||||
- NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
|
||||
- NEXT_PRIVATE_SMTP_HOST=inbucket
|
||||
- NEXT_PRIVATE_SMTP_PORT=2500
|
||||
- NEXT_PRIVATE_SMTP_USERNAME=documenso
|
||||
- NEXT_PRIVATE_SMTP_PASSWORD=password
|
||||
- NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
||||
- NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@documenso.com
|
||||
ports:
|
||||
- 3000:3000
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -135,7 +135,6 @@
|
||||
"@documenso/ee": "*",
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
@ -162,18 +161,19 @@
|
||||
"remeda": "^1.27.1",
|
||||
"sharp": "0.33.1",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"typescript": "5.2.2",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uqr": "^0.1.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/ua-parser-js": "^0.7.39"
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
},
|
||||
"apps/web/node_modules/@types/node": {
|
||||
@ -225,6 +225,7 @@
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -9700,6 +9701,7 @@
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -9711,6 +9713,7 @@
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.3.0.tgz",
|
||||
"integrity": "sha512-314CA4TyK34YEJ6ntBf80eUY+t1XaFLyem1k9P0sX1gn30qThZ5qZr/ZwE318gEnzyYP9yj9HJk6SqwE0upkfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"dotenv": "^16.3.0",
|
||||
@ -9725,6 +9728,7 @@
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
|
||||
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -21419,12 +21423,12 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.4.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"prisma": "5.4.2",
|
||||
"ts-pattern": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@
|
||||
"clean": "turbo run clean && rimraf node_modules",
|
||||
"d": "npm run dx && npm run dev",
|
||||
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev",
|
||||
"dx:up": "docker compose -f docker/compose-services.yml up -d",
|
||||
"dx:down": "docker compose -f docker/compose-services.yml down",
|
||||
"ci": "turbo run build test:e2e",
|
||||
"dx:up": "docker compose -f docker/development/compose.yml up -d",
|
||||
"dx:down": "docker compose -f docker/development/compose.yml down",
|
||||
"ci": "turbo run test:e2e",
|
||||
"prisma:generate": "npm run with:env -- npm run prisma:generate -w @documenso/prisma",
|
||||
"prisma:migrate-dev": "npm run with:env -- npm run prisma:migrate-dev -w @documenso/prisma",
|
||||
"prisma:migrate-deploy": "npm run with:env -- npm run prisma:migrate-deploy -w @documenso/prisma",
|
||||
|
||||
@ -33,7 +33,7 @@ type TransferStripeSubscriptionOptions = {
|
||||
* Will create a new subscription for the new owner and cancel the old one.
|
||||
*
|
||||
* Returns the subscription that should be associated with the team, null if
|
||||
* no subscription is needed (for community plan).
|
||||
* no subscription is needed (for early adopter plan).
|
||||
*/
|
||||
export const transferTeamSubscription = async ({
|
||||
user,
|
||||
|
||||
@ -15,12 +15,10 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps)
|
||||
type: 'application/pdf',
|
||||
});
|
||||
|
||||
const [baseTitle] = fileName?.includes('.pdf')
|
||||
? fileName.split('.pdf')
|
||||
: [fileName ?? 'document'];
|
||||
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
|
||||
|
||||
downloadFile({
|
||||
filename: baseTitle,
|
||||
filename: `${baseTitle}.pdf`,
|
||||
data: blob,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema';
|
||||
import type { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema';
|
||||
|
||||
import { useCopyToClipboard } from './use-copy-to-clipboard';
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
|
||||
*/
|
||||
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
||||
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
|
||||
app_teams: true,
|
||||
app_document_page_view_history_sheet: false,
|
||||
marketing_header_single_player_mode: false,
|
||||
marketing_profiles_announcement_bar: true,
|
||||
|
||||
26
packages/lib/server-only/admin/get-entire-document.ts
Normal file
26
packages/lib/server-only/admin/get-entire-document.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetEntireDocumentOptions = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
include: {
|
||||
Field: {
|
||||
include: {
|
||||
Signature: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
};
|
||||
30
packages/lib/server-only/admin/update-recipient.ts
Normal file
30
packages/lib/server-only/admin/update-recipient.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type UpdateRecipientOptions = {
|
||||
id: number;
|
||||
name: string | undefined;
|
||||
email: string | undefined;
|
||||
};
|
||||
|
||||
export const updateRecipient = async ({ id, name, email }: UpdateRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
throw new Error('Cannot update a recipient that has already signed.');
|
||||
}
|
||||
|
||||
return await prisma.recipient.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { PDFDocument, PDFSignature, rectangle } from 'pdf-lib';
|
||||
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -22,12 +22,14 @@ import { sendCompletedEmail } from './send-completed-email';
|
||||
export type SealDocumentOptions = {
|
||||
documentId: number;
|
||||
sendEmail?: boolean;
|
||||
isResealing?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const sealDocument = async ({
|
||||
documentId,
|
||||
sendEmail = true,
|
||||
isResealing = false,
|
||||
requestMetadata,
|
||||
}: SealDocumentOptions) => {
|
||||
'use server';
|
||||
@ -78,11 +80,43 @@ export const sealDocument = async ({
|
||||
throw new Error(`Document ${document.id} has unsigned fields`);
|
||||
}
|
||||
|
||||
if (isResealing) {
|
||||
// If we're resealing we want to use the initial data for the document
|
||||
// so we aren't placing fields on top of eachother.
|
||||
documentData.data = documentData.initialData;
|
||||
}
|
||||
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFile(documentData);
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
const form = doc.getForm();
|
||||
|
||||
// Remove old signatures
|
||||
for (const field of form.getFields()) {
|
||||
if (field instanceof PDFSignature) {
|
||||
field.acroField.getWidgets().forEach((widget) => {
|
||||
widget.ensureAP();
|
||||
|
||||
try {
|
||||
widget.getNormalAppearance();
|
||||
} catch (e) {
|
||||
const { context } = widget.dict;
|
||||
|
||||
const xobj = context.formXObject([rectangle(0, 0, 0, 0)]);
|
||||
|
||||
const streamRef = context.register(xobj);
|
||||
|
||||
widget.setNormalAppearance(streamRef);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten the form to stop annotation layers from appearing above documenso fields
|
||||
form.flatten();
|
||||
|
||||
for (const field of fields) {
|
||||
await insertFieldInPDF(doc, field);
|
||||
}
|
||||
@ -134,7 +168,7 @@ export const sealDocument = async ({
|
||||
});
|
||||
});
|
||||
|
||||
if (sendEmail) {
|
||||
if (sendEmail && !isResealing) {
|
||||
await sendCompletedEmail({ documentId, requestMetadata });
|
||||
}
|
||||
|
||||
|
||||
@ -4,20 +4,18 @@ import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { deletedAccountServiceAccount } from './service-accounts/deleted-account';
|
||||
|
||||
export type DeleteUserOptions = {
|
||||
email: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export const deleteUser = async ({ email }: DeleteUserOptions) => {
|
||||
export const deleteUser = async ({ id }: DeleteUserOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: {
|
||||
contains: email,
|
||||
},
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User with email ${email} not found`);
|
||||
throw new Error(`User with ID ${id} not found`);
|
||||
}
|
||||
|
||||
const serviceAccount = await deletedAccountServiceAccount();
|
||||
|
||||
@ -19,12 +19,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.4.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"prisma": "5.4.2",
|
||||
"ts-pattern": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import signer from 'node-signpdf';
|
||||
import { PDFArray, PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from 'pdf-lib';
|
||||
import {
|
||||
PDFArray,
|
||||
PDFDocument,
|
||||
PDFHexString,
|
||||
PDFName,
|
||||
PDFNumber,
|
||||
PDFString,
|
||||
rectangle,
|
||||
} from 'pdf-lib';
|
||||
|
||||
export type AddSigningPlaceholderOptions = {
|
||||
pdf: Buffer;
|
||||
@ -39,6 +47,12 @@ export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOption
|
||||
P: pages[0].ref,
|
||||
});
|
||||
|
||||
const xobj = widget.context.formXObject([rectangle(0, 0, 0, 0)]);
|
||||
|
||||
const streamRef = widget.context.register(xobj);
|
||||
|
||||
widget.set(PDFName.of('AP'), widget.context.obj({ N: streamRef }));
|
||||
|
||||
const widgetRef = doc.context.register(widget);
|
||||
|
||||
let widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { QueryClientConfig } from '@tanstack/react-query';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { httpBatchLink } from '@trpc/client';
|
||||
import { createTRPCReact } from '@trpc/react-query';
|
||||
@ -27,7 +28,27 @@ export interface TrpcProviderProps {
|
||||
}
|
||||
|
||||
export function TrpcProvider({ children }: TrpcProviderProps) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
let queryClientConfig: QueryClientConfig | undefined;
|
||||
|
||||
const isDevelopingOffline =
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.hostname === 'localhost' &&
|
||||
!window.navigator.onLine;
|
||||
|
||||
if (isDevelopingOffline) {
|
||||
queryClientConfig = {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
networkMode: 'always',
|
||||
},
|
||||
mutations: {
|
||||
networkMode: 'always',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [queryClient] = useState(() => new QueryClient(queryClientConfig));
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
|
||||
@ -1,14 +1,39 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
|
||||
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
|
||||
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
|
||||
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
||||
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
|
||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
|
||||
import { adminProcedure, router } from '../trpc';
|
||||
import { ZUpdateProfileMutationByAdminSchema, ZUpdateSiteSettingMutationSchema } from './schema';
|
||||
import {
|
||||
ZAdminDeleteUserMutationSchema,
|
||||
ZAdminFindDocumentsQuerySchema,
|
||||
ZAdminResealDocumentMutationSchema,
|
||||
ZAdminUpdateProfileMutationSchema,
|
||||
ZAdminUpdateRecipientMutationSchema,
|
||||
ZAdminUpdateSiteSettingMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
export const adminRouter = router({
|
||||
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
||||
const { term, page, perPage } = input;
|
||||
|
||||
try {
|
||||
return await findDocuments({ term, page, perPage });
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to retrieve the documents. Please try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
updateUser: adminProcedure
|
||||
.input(ZUpdateProfileMutationByAdminSchema)
|
||||
.input(ZAdminUpdateProfileMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, name, email, roles } = input;
|
||||
|
||||
@ -22,8 +47,23 @@ export const adminRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
updateRecipient: adminProcedure
|
||||
.input(ZAdminUpdateRecipientMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, name, email } = input;
|
||||
|
||||
try {
|
||||
return await updateRecipient({ id, name, email });
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to update the recipient provided.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
updateSiteSetting: adminProcedure
|
||||
.input(ZUpdateSiteSettingMutationSchema)
|
||||
.input(ZAdminUpdateSiteSettingMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const { id, enabled, data } = input;
|
||||
@ -41,4 +81,41 @@ export const adminRouter = router({
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
resealDocument: adminProcedure
|
||||
.input(ZAdminResealDocumentMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id } = input;
|
||||
|
||||
try {
|
||||
return await sealDocument({ documentId: id, isResealing: true });
|
||||
} catch (err) {
|
||||
console.log('resealDocument error', err);
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to reseal the document provided.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
|
||||
const { id, email } = input;
|
||||
|
||||
try {
|
||||
const user = await getUserById({ id });
|
||||
|
||||
if (user.email !== email) {
|
||||
throw new Error('Email does not match');
|
||||
}
|
||||
|
||||
return await deleteUser({ id });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to delete the specified account. Please try again.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@ -3,17 +3,48 @@ import z from 'zod';
|
||||
|
||||
import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
|
||||
|
||||
export const ZUpdateProfileMutationByAdminSchema = z.object({
|
||||
export const ZAdminFindDocumentsQuerySchema = z.object({
|
||||
term: z.string().optional(),
|
||||
page: z.number().optional().default(1),
|
||||
perPage: z.number().optional().default(20),
|
||||
});
|
||||
|
||||
export type TAdminFindDocumentsQuerySchema = z.infer<typeof ZAdminFindDocumentsQuerySchema>;
|
||||
|
||||
export const ZAdminUpdateProfileMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
name: z.string().nullish(),
|
||||
email: z.string().email().optional(),
|
||||
roles: z.array(z.nativeEnum(Role)).optional(),
|
||||
});
|
||||
|
||||
export type TUpdateProfileMutationByAdminSchema = z.infer<
|
||||
typeof ZUpdateProfileMutationByAdminSchema
|
||||
export type TAdminUpdateProfileMutationSchema = z.infer<typeof ZAdminUpdateProfileMutationSchema>;
|
||||
|
||||
export const ZAdminUpdateRecipientMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
});
|
||||
|
||||
export type TAdminUpdateRecipientMutationSchema = z.infer<
|
||||
typeof ZAdminUpdateRecipientMutationSchema
|
||||
>;
|
||||
|
||||
export const ZUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
|
||||
export const ZAdminUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
|
||||
|
||||
export type TUpdateSiteSettingMutationSchema = z.infer<typeof ZUpdateSiteSettingMutationSchema>;
|
||||
export type TAdminUpdateSiteSettingMutationSchema = z.infer<
|
||||
typeof ZAdminUpdateSiteSettingMutationSchema
|
||||
>;
|
||||
|
||||
export const ZAdminResealDocumentMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export type TAdminResealDocumentMutationSchema = z.infer<typeof ZAdminResealDocumentMutationSchema>;
|
||||
|
||||
export const ZAdminDeleteUserMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export type TAdminDeleteUserMutationSchema = z.infer<typeof ZAdminDeleteUserMutationSchema>;
|
||||
|
||||
@ -25,7 +25,7 @@ export const authRouter = router({
|
||||
|
||||
const { name, email, password, signature, url } = input;
|
||||
|
||||
if ((true || IS_BILLING_ENABLED()) && url && url.length <= 6) {
|
||||
if (IS_BILLING_ENABLED() && url && url.length < 6) {
|
||||
throw new AppError(
|
||||
AppErrorCode.PREMIUM_PROFILE_URL,
|
||||
'Only subscribers can have a username shorter than 6 characters',
|
||||
|
||||
@ -207,9 +207,9 @@ export const profileRouter = router({
|
||||
|
||||
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
|
||||
try {
|
||||
const user = ctx.user;
|
||||
|
||||
return await deleteUser(user);
|
||||
return await deleteUser({
|
||||
id: ctx.user.id,
|
||||
});
|
||||
} catch (err) {
|
||||
let message = 'We were unable to delete your account. Please try again.';
|
||||
|
||||
|
||||
@ -27,7 +27,8 @@
|
||||
"cache": false
|
||||
},
|
||||
"test:e2e": {
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false
|
||||
}
|
||||
},
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
|
||||
Reference in New Issue
Block a user