mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 18:21:32 +10:00
Compare commits
4 Commits
fix/467-bu
...
feat/singl
| Author | SHA1 | Date | |
|---|---|---|---|
| 2749520e10 | |||
| 8c023b092d | |||
| 8d2badf75e | |||
| 9d6e149f56 |
@ -15,11 +15,6 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen
|
|||||||
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
|
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
|
||||||
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||||
|
|
||||||
# [[E2E Tests]]
|
|
||||||
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
|
|
||||||
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
|
|
||||||
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_password"
|
|
||||||
|
|
||||||
# [[STORAGE]]
|
# [[STORAGE]]
|
||||||
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
|
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
|
||||||
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
|
NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
|
||||||
@ -73,7 +68,6 @@ NEXT_PRIVATE_STRIPE_API_KEY=
|
|||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID=
|
|
||||||
|
|
||||||
# [[FEATURES]]
|
# [[FEATURES]]
|
||||||
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
||||||
|
|||||||
51
.github/workflows/e2e-tests.yml
vendored
51
.github/workflows/e2e-tests.yml
vendored
@ -1,51 +0,0 @@
|
|||||||
name: Playwright Tests
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [feat/refresh]
|
|
||||||
pull_request:
|
|
||||||
branches: [feat/refresh]
|
|
||||||
jobs:
|
|
||||||
e2e_tests:
|
|
||||||
timeout-minutes: 60
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Copy env
|
|
||||||
run: cp .env.example .env
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: npx playwright install --with-deps
|
|
||||||
- name: Generate Prisma Client
|
|
||||||
run: npm run prisma:generate -w @documenso/prisma
|
|
||||||
- name: Create the database
|
|
||||||
run: npm run prisma:migrate-dev
|
|
||||||
- name: Run Playwright tests
|
|
||||||
run: npm run ci
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: playwright-report/
|
|
||||||
retention-days: 30
|
|
||||||
env:
|
|
||||||
NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
|
|
||||||
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
||||||
55
.gitpod.yml
55
.gitpod.yml
@ -1,55 +0,0 @@
|
|||||||
tasks:
|
|
||||||
- init: |
|
|
||||||
npm i &&
|
|
||||||
npm run dx:up &&
|
|
||||||
cp .env.example .env &&
|
|
||||||
set -a; source .env &&
|
|
||||||
export NEXTAUTH_URL="$(gp url 3000)" &&
|
|
||||||
export NEXT_PUBLIC_WEBAPP_URL="$(gp url 3000)" &&
|
|
||||||
export NEXT_PUBLIC_MARKETING_URL="$(gp url 3001)"
|
|
||||||
command: npm run d
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- port: 3000
|
|
||||||
visibility: public
|
|
||||||
onOpen: open-preview
|
|
||||||
- port: 3001
|
|
||||||
visibility: public
|
|
||||||
onOpen: open-preview
|
|
||||||
- port: 9000
|
|
||||||
visibility: public
|
|
||||||
onOpen: ignore
|
|
||||||
- port: 1100
|
|
||||||
visibility: private
|
|
||||||
onOpen: ignore
|
|
||||||
- port: 2500
|
|
||||||
visibility: private
|
|
||||||
onOpen: ignore
|
|
||||||
- port: 54320
|
|
||||||
visibility: private
|
|
||||||
onOpen: ignore
|
|
||||||
|
|
||||||
|
|
||||||
github:
|
|
||||||
prebuilds:
|
|
||||||
master: true
|
|
||||||
pullRequests: true
|
|
||||||
pullRequestsFromForks: true
|
|
||||||
addCheck: true
|
|
||||||
addComment: true
|
|
||||||
addBadge: true
|
|
||||||
|
|
||||||
vscode:
|
|
||||||
extensions:
|
|
||||||
- aaron-bond.better-comments
|
|
||||||
- bradlc.vscode-tailwindcss
|
|
||||||
- dbaeumer.vscode-eslint
|
|
||||||
- esbenp.prettier-vscode
|
|
||||||
- mikestead.dotenv
|
|
||||||
- unifiedjs.vscode-mdx
|
|
||||||
- GitHub.copilot-chat
|
|
||||||
- GitHub.copilot-labs
|
|
||||||
- GitHub.copilot
|
|
||||||
- GitHub.vscode-pull-request-github
|
|
||||||
- Prisma.prisma
|
|
||||||
- VisualStudioExptTeam.vscodeintellicode
|
|
||||||
1
apps/marketing/process-env.d.ts
vendored
1
apps/marketing/process-env.d.ts
vendored
@ -7,7 +7,6 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { Caveat, Inter } from 'next/font/google';
|
|||||||
|
|
||||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||||
|
|
||||||
@ -64,9 +63,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
<body>
|
<body>
|
||||||
<FeatureFlagProvider initialFlags={flags}>
|
<FeatureFlagProvider initialFlags={flags}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<PlausibleProvider>
|
<PlausibleProvider>{children}</PlausibleProvider>
|
||||||
<TrpcProvider>{children}</TrpcProvider>
|
|
||||||
</PlausibleProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</FeatureFlagProvider>
|
</FeatureFlagProvider>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||||
|
|
||||||
export type PasswordRevealProps = {
|
export type PasswordRevealProps = {
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Share } from 'lucide-react';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
|
import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
|
||||||
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
|
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -86,11 +87,11 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
|
|||||||
<div className="relative mt-8 w-full">
|
<div className="relative mt-8 w-full">
|
||||||
<div className={cn('flex flex-col items-center', className)}>
|
<div className={cn('flex flex-col items-center', className)}>
|
||||||
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
|
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
|
||||||
<DocumentShareButton
|
{/* TODO: Hook this up */}
|
||||||
documentId={document.id}
|
<Button variant="outline" className="flex-1 bg-transparent backdrop-blur-sm" disabled>
|
||||||
token={document.Recipient.token}
|
<Share className="mr-2 h-5 w-5" />
|
||||||
className="flex-1 bg-transparent backdrop-blur-sm"
|
Share
|
||||||
/>
|
</Button>
|
||||||
|
|
||||||
<DocumentDownloadButton
|
<DocumentDownloadButton
|
||||||
className="flex-1 bg-transparent backdrop-blur-sm"
|
className="flex-1 bg-transparent backdrop-blur-sm"
|
||||||
@ -102,7 +103,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
|
|||||||
<Button
|
<Button
|
||||||
onClick={async () => onShowDocumentClick()}
|
onClick={async () => onShowDocumentClick()}
|
||||||
loading={isFetchingDocumentFile}
|
loading={isFetchingDocumentFile}
|
||||||
className="z-10 col-span-2"
|
className="col-span-2"
|
||||||
>
|
>
|
||||||
Show document
|
Show document
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
28
apps/marketing/src/hooks/use-copy-to-clipboard.ts
Normal file
28
apps/marketing/src/hooks/use-copy-to-clipboard.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type CopiedValue = string | null;
|
||||||
|
export type CopyFn = (_text: string) => Promise<boolean>;
|
||||||
|
|
||||||
|
export function useCopyToClipboard(): [CopiedValue, CopyFn] {
|
||||||
|
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
|
||||||
|
|
||||||
|
const copy: CopyFn = async (text) => {
|
||||||
|
if (!navigator?.clipboard) {
|
||||||
|
console.warn('Clipboard not supported');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to save to clipboard then save it in the state if worked
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopiedText(text);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Copy failed', error);
|
||||||
|
setCopiedText(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [copiedText, copy];
|
||||||
|
}
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import * as trpcNext from '@documenso/trpc/server/adapters/next';
|
|
||||||
import { createTrpcContext } from '@documenso/trpc/server/context';
|
|
||||||
import { appRouter } from '@documenso/trpc/server/router';
|
|
||||||
|
|
||||||
export default trpcNext.createNextApiHandler({
|
|
||||||
router: appRouter,
|
|
||||||
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
|
|
||||||
});
|
|
||||||
1
apps/web/process-env.d.ts
vendored
1
apps/web/process-env.d.ts
vendored
@ -7,7 +7,6 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
@ -1,99 +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 { FindResultSet } from '@documenso/lib/types/find-result-set';
|
|
||||||
import { 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 { 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>{row.original.title}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Owner',
|
|
||||||
accessorKey: 'owner',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<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-gray-400">
|
|
||||||
<span className="text-xs">{row.original.User.name}</span>
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-4xl font-semibold">Manage documents</h2>
|
|
||||||
<div className="mt-8">
|
|
||||||
<DocumentsDataTable results={results} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@ import { HTMLAttributes } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react';
|
import { BarChart3, User2 } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -37,40 +37,10 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
|
|||||||
'justify-start md:w-full',
|
'justify-start md:w-full',
|
||||||
pathname?.startsWith('/admin/users') && 'bg-secondary',
|
pathname?.startsWith('/admin/users') && 'bg-secondary',
|
||||||
)}
|
)}
|
||||||
asChild
|
disabled
|
||||||
>
|
>
|
||||||
<Link href="/admin/users">
|
<User2 className="mr-2 h-5 w-5" />
|
||||||
<User2 className="mr-2 h-5 w-5" />
|
Users (Coming Soon)
|
||||||
Users
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
'justify-start md:w-full',
|
|
||||||
pathname?.startsWith('/admin/documents') && 'bg-secondary',
|
|
||||||
)}
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href="/admin/documents">
|
|
||||||
<FileStack className="mr-2 h-5 w-5" />
|
|
||||||
Documents
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
'justify-start md:w-full',
|
|
||||||
pathname?.startsWith('/admin/subscriptions') && 'bg-secondary',
|
|
||||||
)}
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href="/admin/subscriptions">
|
|
||||||
<Wallet2 className="mr-2 h-5 w-5" />
|
|
||||||
Subscriptions
|
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { findSubscriptions } from '@documenso/lib/server-only/admin/get-all-subscriptions';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@documenso/ui/primitives/table';
|
|
||||||
|
|
||||||
export default async function Subscriptions() {
|
|
||||||
const subscriptions = await findSubscriptions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-4xl font-semibold">Manage subscriptions</h2>
|
|
||||||
<div className="mt-8">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>ID</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Created At</TableHead>
|
|
||||||
<TableHead>Ends On</TableHead>
|
|
||||||
<TableHead>User ID</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{subscriptions.map((subscription, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>{subscription.id}</TableCell>
|
|
||||||
<TableCell>{subscription.status}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{subscription.createdAt
|
|
||||||
? new Date(subscription.createdAt).toLocaleDateString(undefined, {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
: 'N/A'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{subscription.periodEnd
|
|
||||||
? new Date(subscription.periodEnd).toLocaleDateString(undefined, {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
: 'N/A'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Link href={`/admin/users/${subscription.userId}`}>{subscription.userId}</Link>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { Combobox } from '@documenso/ui/primitives/combobox';
|
|
||||||
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';
|
|
||||||
|
|
||||||
import { TUserFormSchema, ZUserFormSchema } from '~/providers/admin-user-profile-update.types';
|
|
||||||
|
|
||||||
export default function UserPage({ params }: { params: { id: number } }) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { data: user } = trpc.profile.getUser.useQuery(
|
|
||||||
{
|
|
||||||
id: Number(params.id),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!params.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const roles = user?.roles ?? [];
|
|
||||||
|
|
||||||
const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<TUserFormSchema>({
|
|
||||||
resolver: zodResolver(ZUserFormSchema),
|
|
||||||
values: {
|
|
||||||
name: user?.name ?? '',
|
|
||||||
email: user?.email ?? '',
|
|
||||||
roles: user?.roles ?? [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async ({ name, email, roles }: TUserFormSchema) => {
|
|
||||||
try {
|
|
||||||
await updateUserMutation({
|
|
||||||
id: Number(user?.id),
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
roles,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Profile updated',
|
|
||||||
description: 'Your profile has been updated.',
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: 'An error occurred while updating your profile.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-4xl font-semibold">Manage {user?.name}'s profile</h2>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
||||||
<fieldset className="mt-6 flex w-full flex-col gap-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-muted-foreground">Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="text" {...field} value={field.value ?? ''} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-muted-foreground">Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="text" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="roles"
|
|
||||||
render={({ field: { onChange } }) => (
|
|
||||||
<FormItem>
|
|
||||||
<fieldset className="flex flex-col gap-2">
|
|
||||||
<FormLabel className="text-muted-foreground">Roles</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Combobox
|
|
||||||
listValues={roles}
|
|
||||||
onChange={(values: string[]) => onChange(values)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</fieldset>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
|
||||||
Update user
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState, useTransition } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Edit, 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 { Document, Role, Subscription } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
|
||||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
roles: Role[];
|
|
||||||
Subscription: SubscriptionLite[];
|
|
||||||
Document: DocumentLite[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionLite = Pick<
|
|
||||||
Subscription,
|
|
||||||
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
|
|
||||||
>;
|
|
||||||
|
|
||||||
type DocumentLite = Pick<Document, 'id'>;
|
|
||||||
|
|
||||||
type UsersDataTableProps = {
|
|
||||||
users: User[];
|
|
||||||
totalPages: number;
|
|
||||||
perPage: number;
|
|
||||||
page: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTableProps) => {
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
|
||||||
const [searchString, setSearchString] = useState('');
|
|
||||||
const debouncedSearchString = useDebouncedValue(searchString, 1000);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
startTransition(() => {
|
|
||||||
updateSearchParams({
|
|
||||||
search: debouncedSearchString,
|
|
||||||
page: 1,
|
|
||||||
perPage,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [debouncedSearchString]);
|
|
||||||
|
|
||||||
const onPaginationChange = (page: number, perPage: number) => {
|
|
||||||
startTransition(() => {
|
|
||||||
updateSearchParams({
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSearchString(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
className="my-6 flex flex-row gap-4"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search by name or email"
|
|
||||||
value={searchString}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<DataTable
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'ID',
|
|
||||||
accessorKey: 'id',
|
|
||||||
cell: ({ row }) => <div>{row.original.id}</div>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
cell: ({ row }) => <div>{row.original.name}</div>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
cell: ({ row }) => <div>{row.original.email}</div>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Roles',
|
|
||||||
accessorKey: 'roles',
|
|
||||||
cell: ({ row }) => row.original.roles.join(', '),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Subscription',
|
|
||||||
accessorKey: 'subscription',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (row.original.Subscription && row.original.Subscription.length > 0) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{row.original.Subscription.map((subscription: SubscriptionLite, i: number) => {
|
|
||||||
return <span key={i}>{subscription.status}</span>;
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <span>NONE</span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Documents',
|
|
||||||
accessorKey: 'documents',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return <div>{row.original.Document.length}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: '',
|
|
||||||
accessorKey: 'edit',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<Button className="w-24" asChild>
|
|
||||||
<Link href={`/admin/users/${row.original.id}`}>
|
|
||||||
<Edit className="-ml-1 mr-2 h-4 w-4" />
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={users}
|
|
||||||
perPage={perPage}
|
|
||||||
currentPage={page}
|
|
||||||
totalPages={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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
|
|
||||||
|
|
||||||
export async function search(search: string, page: number, perPage: number) {
|
|
||||||
const results = await findUsers({ username: search, email: search, page, perPage });
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { UsersDataTable } from './data-table-users';
|
|
||||||
import { search } from './fetch-users.actions';
|
|
||||||
|
|
||||||
type AdminManageUsersProps = {
|
|
||||||
searchParams?: {
|
|
||||||
search?: string;
|
|
||||||
page?: number;
|
|
||||||
perPage?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) {
|
|
||||||
const page = Number(searchParams.page) || 1;
|
|
||||||
const perPage = Number(searchParams.perPage) || 10;
|
|
||||||
const searchString = searchParams.search || '';
|
|
||||||
|
|
||||||
const { users, totalPages } = await search(searchString, page, perPage);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-4xl font-semibold">Manage users</h2>
|
|
||||||
<UsersDataTable users={users} totalPages={totalPages} page={page} perPage={perPage} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -6,15 +6,13 @@ import { Edit, Pencil, Share } from 'lucide-react';
|
|||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
|
|
||||||
import {
|
|
||||||
TOAST_DOCUMENT_SHARE_ERROR,
|
|
||||||
TOAST_DOCUMENT_SHARE_SUCCESS,
|
|
||||||
} from '@documenso/lib/constants/toast';
|
|
||||||
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
|
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||||
|
|
||||||
export type DataTableActionButtonProps = {
|
export type DataTableActionButtonProps = {
|
||||||
row: Document & {
|
row: Document & {
|
||||||
User: Pick<User, 'id' | 'name' | 'email'>;
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
@ -24,18 +22,16 @@ export type DataTableActionButtonProps = {
|
|||||||
|
|
||||||
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
const { createAndCopyShareLink, isCopyingShareLink } = useCopyShareLink({
|
|
||||||
onSuccess: () => toast(TOAST_DOCUMENT_SHARE_SUCCESS),
|
|
||||||
onError: () => toast(TOAST_DOCUMENT_SHARE_ERROR),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
|
||||||
|
trpc.shareLink.createOrGetShareLink.useMutation();
|
||||||
|
|
||||||
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||||
|
|
||||||
const isOwner = row.User.id === session.user.id;
|
const isOwner = row.User.id === session.user.id;
|
||||||
@ -45,6 +41,20 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
const onShareClick = async () => {
|
||||||
|
const { slug } = await createOrGetShareLink({
|
||||||
|
token: recipient?.token,
|
||||||
|
documentId: row.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
description: 'The sharing link has been copied to your clipboard.',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return match({
|
return match({
|
||||||
isOwner,
|
isOwner,
|
||||||
isRecipient,
|
isRecipient,
|
||||||
@ -70,17 +80,8 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
<Button
|
<Button className="w-24" loading={isCreatingShareLink} onClick={onShareClick}>
|
||||||
className="w-24"
|
{!isCreatingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />}
|
||||||
loading={isCopyingShareLink}
|
|
||||||
onClick={async () =>
|
|
||||||
createAndCopyShareLink({
|
|
||||||
token: recipient?.token,
|
|
||||||
documentId: row.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!isCopyingShareLink && <Share className="-ml-1 mr-2 h-4 w-4" />}
|
|
||||||
Share
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
|
|||||||
@ -18,15 +18,11 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
|
|
||||||
import {
|
|
||||||
TOAST_DOCUMENT_SHARE_ERROR,
|
|
||||||
TOAST_DOCUMENT_SHARE_SUCCESS,
|
|
||||||
} from '@documenso/lib/constants/toast';
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
|
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -36,6 +32,8 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||||
|
|
||||||
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
|
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
|
||||||
|
|
||||||
export type DataTableActionDropdownProps = {
|
export type DataTableActionDropdownProps = {
|
||||||
@ -47,13 +45,8 @@ export type DataTableActionDropdownProps = {
|
|||||||
|
|
||||||
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
|
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
const { createAndCopyShareLink, isCopyingShareLink } = useCopyShareLink({
|
|
||||||
onSuccess: () => toast(TOAST_DOCUMENT_SHARE_SUCCESS),
|
|
||||||
onError: () => toast(TOAST_DOCUMENT_SHARE_ERROR),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
@ -61,16 +54,33 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
|
||||||
|
trpcReact.shareLink.createOrGetShareLink.useMutation();
|
||||||
|
|
||||||
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||||
|
|
||||||
const isOwner = row.User.id === session.user.id;
|
const isOwner = row.User.id === session.user.id;
|
||||||
// const isRecipient = !!recipient;
|
// const isRecipient = !!recipient;
|
||||||
const isDraft = row.status === DocumentStatus.DRAFT;
|
// const isDraft = row.status === DocumentStatus.DRAFT;
|
||||||
// const isPending = row.status === DocumentStatus.PENDING;
|
// const isPending = row.status === DocumentStatus.PENDING;
|
||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT;
|
const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT;
|
||||||
|
|
||||||
|
const onShareClick = async () => {
|
||||||
|
const { slug } = await createOrGetShareLink({
|
||||||
|
token: recipient?.token,
|
||||||
|
documentId: row.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
description: 'The sharing link has been copied to your clipboard.',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
const onDownloadClick = async () => {
|
||||||
let document: DocumentWithData | null = null;
|
let document: DocumentWithData | null = null;
|
||||||
|
|
||||||
@ -156,16 +166,8 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
Resend
|
Resend
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={onShareClick}>
|
||||||
disabled={isDraft}
|
{isCreatingShareLink ? (
|
||||||
onClick={async () =>
|
|
||||||
createAndCopyShareLink({
|
|
||||||
token: recipient?.token,
|
|
||||||
documentId: row.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isCopyingShareLink ? (
|
|
||||||
<Loader className="mr-2 h-4 w-4" />
|
<Loader className="mr-2 h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
<Share className="mr-2 h-4 w-4" />
|
<Share className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
|
|
||||||
import { PriceIntervals } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
|
||||||
import { toHumanPrice } from '@documenso/lib/universal/stripe/to-human-price';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { createCheckout } from './create-checkout.action';
|
|
||||||
|
|
||||||
type Interval = keyof PriceIntervals;
|
|
||||||
|
|
||||||
const INTERVALS: Interval[] = ['day', 'week', 'month', 'year'];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const isInterval = (value: unknown): value is Interval => INTERVALS.includes(value as Interval);
|
|
||||||
|
|
||||||
const FRIENDLY_INTERVALS: Record<Interval, string> = {
|
|
||||||
day: 'Daily',
|
|
||||||
week: 'Weekly',
|
|
||||||
month: 'Monthly',
|
|
||||||
year: 'Yearly',
|
|
||||||
};
|
|
||||||
|
|
||||||
const MotionCard = motion(Card);
|
|
||||||
|
|
||||||
export type BillingPlansProps = {
|
|
||||||
prices: PriceIntervals;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BillingPlans = ({ prices }: BillingPlansProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const isMounted = useIsMounted();
|
|
||||||
|
|
||||||
const [interval, setInterval] = useState<Interval>('month');
|
|
||||||
const [isFetchingCheckoutSession, setIsFetchingCheckoutSession] = useState(false);
|
|
||||||
|
|
||||||
const onSubscribeClick = async (priceId: string) => {
|
|
||||||
try {
|
|
||||||
setIsFetchingCheckoutSession(true);
|
|
||||||
|
|
||||||
const url = await createCheckout({ priceId });
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
throw new Error('Unable to create session');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(url);
|
|
||||||
} catch (_err) {
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: 'An error occurred while trying to create a checkout session.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsFetchingCheckoutSession(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Tabs value={interval} onValueChange={(value) => isInterval(value) && setInterval(value)}>
|
|
||||||
<TabsList>
|
|
||||||
{INTERVALS.map(
|
|
||||||
(interval) =>
|
|
||||||
prices[interval].length > 0 && (
|
|
||||||
<TabsTrigger key={interval} className="min-w-[150px]" value={interval}>
|
|
||||||
{FRIENDLY_INTERVALS[interval]}
|
|
||||||
</TabsTrigger>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<div className="mt-8 grid gap-8 lg:grid-cols-2 2xl:grid-cols-3">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{prices[interval].map((price) => (
|
|
||||||
<MotionCard
|
|
||||||
key={price.id}
|
|
||||||
initial={{ opacity: isMounted ? 0 : 1, y: isMounted ? 20 : 0 }}
|
|
||||||
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
|
||||||
exit={{ opacity: 0, transition: { duration: 0.3 } }}
|
|
||||||
>
|
|
||||||
<CardContent className="flex h-full flex-col p-6">
|
|
||||||
<CardTitle>{price.product.name}</CardTitle>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-2 text-lg font-medium">
|
|
||||||
${toHumanPrice(price.unit_amount ?? 0)} {price.currency.toUpperCase()}{' '}
|
|
||||||
<span className="text-xs">per {interval}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-1.5 text-sm">
|
|
||||||
{price.product.description}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{price.product.features && price.product.features.length > 0 && (
|
|
||||||
<div className="text-muted-foreground mt-4">
|
|
||||||
<div className="text-sm font-medium">Includes:</div>
|
|
||||||
|
|
||||||
<ul className="mt-1 divide-y text-sm">
|
|
||||||
{price.product.features.map((feature, index) => (
|
|
||||||
<li key={index} className="py-2">
|
|
||||||
{feature.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="mt-4"
|
|
||||||
loading={isFetchingCheckoutSession}
|
|
||||||
onClick={() => void onSubscribeClick(price.id)}
|
|
||||||
>
|
|
||||||
Subscribe
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</MotionCard>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { createBillingPortal } from './create-billing-portal.action';
|
|
||||||
|
|
||||||
export const BillingPortalButton = () => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const [isFetchingPortalUrl, setIsFetchingPortalUrl] = useState(false);
|
|
||||||
|
|
||||||
const handleFetchPortalUrl = async () => {
|
|
||||||
if (isFetchingPortalUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFetchingPortalUrl(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionUrl = await createBillingPortal();
|
|
||||||
|
|
||||||
if (!sessionUrl) {
|
|
||||||
throw new Error('NO_SESSION');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(sessionUrl, '_blank');
|
|
||||||
} catch (e) {
|
|
||||||
let description =
|
|
||||||
'We are unable to proceed to the billing portal at this time. Please try again, or contact support.';
|
|
||||||
|
|
||||||
if (e.message === 'CUSTOMER_NOT_FOUND') {
|
|
||||||
description =
|
|
||||||
'You do not currently have a customer record, this should not happen. Please contact support for assistance.';
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description,
|
|
||||||
variant: 'destructive',
|
|
||||||
duration: 10000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFetchingPortalUrl(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={async () => handleFetchPortalUrl()} loading={isFetchingPortalUrl}>
|
|
||||||
Manage Subscription
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getStripeCustomerByEmail,
|
|
||||||
getStripeCustomerById,
|
|
||||||
} from '@documenso/ee/server-only/stripe/get-customer';
|
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
|
||||||
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
|
||||||
|
|
||||||
export const createBillingPortal = async () => {
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
|
||||||
|
|
||||||
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
|
|
||||||
|
|
||||||
let stripeCustomer: Stripe.Customer | null = null;
|
|
||||||
|
|
||||||
// Find the Stripe customer for the current user subscription.
|
|
||||||
if (existingSubscription) {
|
|
||||||
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
|
||||||
|
|
||||||
if (!stripeCustomer) {
|
|
||||||
throw new Error('Missing Stripe customer for subscription');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to check if a Stripe customer already exists for the current user email.
|
|
||||||
if (!stripeCustomer) {
|
|
||||||
stripeCustomer = await getStripeCustomerByEmail(user.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Stripe customer if it does not exist for the current user.
|
|
||||||
if (!stripeCustomer) {
|
|
||||||
stripeCustomer = await stripe.customers.create({
|
|
||||||
name: user.name ?? undefined,
|
|
||||||
email: user.email,
|
|
||||||
metadata: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return getPortalSession({
|
|
||||||
customerId: stripeCustomer.id,
|
|
||||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
|
|
||||||
import {
|
|
||||||
getStripeCustomerByEmail,
|
|
||||||
getStripeCustomerById,
|
|
||||||
} from '@documenso/ee/server-only/stripe/get-customer';
|
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
|
||||||
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
|
||||||
|
|
||||||
export type CreateCheckoutOptions = {
|
|
||||||
priceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
|
||||||
|
|
||||||
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
|
|
||||||
|
|
||||||
let stripeCustomer: Stripe.Customer | null = null;
|
|
||||||
|
|
||||||
// Find the Stripe customer for the current user subscription.
|
|
||||||
if (existingSubscription) {
|
|
||||||
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
|
||||||
|
|
||||||
if (!stripeCustomer) {
|
|
||||||
throw new Error('Missing Stripe customer for subscription');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getPortalSession({
|
|
||||||
customerId: stripeCustomer.id,
|
|
||||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to check if a Stripe customer already exists for the current user email.
|
|
||||||
if (!stripeCustomer) {
|
|
||||||
stripeCustomer = await getStripeCustomerByEmail(user.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Stripe customer if it does not exist for the current user.
|
|
||||||
if (!stripeCustomer) {
|
|
||||||
stripeCustomer = await stripe.customers.create({
|
|
||||||
name: user.name ?? undefined,
|
|
||||||
email: user.email,
|
|
||||||
metadata: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCheckoutSession({
|
|
||||||
customerId: stripeCustomer.id,
|
|
||||||
priceId,
|
|
||||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,18 +1,16 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||||
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||||
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|
||||||
import { BillingPlans } from './billing-plans';
|
|
||||||
import { BillingPortalButton } from './billing-portal-button';
|
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
@ -23,75 +21,57 @@ export default async function BillingSettingsPage() {
|
|||||||
redirect('/settings/profile');
|
redirect('/settings/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [subscription, prices] = await Promise.all([
|
const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => {
|
||||||
getSubscriptionByUserId({ userId: user.id }),
|
if (sub) {
|
||||||
getPricesByInterval(),
|
return sub;
|
||||||
]);
|
}
|
||||||
|
|
||||||
let subscriptionProduct: Stripe.Product | null = null;
|
// If we don't have a customer record, create one as well as an empty subscription.
|
||||||
|
return createCustomer({ user });
|
||||||
|
});
|
||||||
|
|
||||||
if (subscription?.planId) {
|
let billingPortalUrl = '';
|
||||||
const foundSubscriptionProduct = (await stripe.products.list()).data.find(
|
|
||||||
(item) => item.default_price === subscription.planId,
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptionProduct = foundSubscriptionProduct ?? null;
|
if (subscription.customerId) {
|
||||||
|
billingPortalUrl = await getPortalSession({
|
||||||
|
customerId: subscription.customerId,
|
||||||
|
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMissingOrInactiveOrFreePlan = !subscription || subscription.status === 'INACTIVE';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Billing</h3>
|
<h3 className="text-lg font-medium">Billing</h3>
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
{isMissingOrInactiveOrFreePlan && (
|
Your subscription is{' '}
|
||||||
<p>
|
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
|
||||||
You are currently on the <span className="font-semibold">Free Plan</span>.
|
{subscription?.periodEnd && (
|
||||||
</p>
|
<>
|
||||||
|
{' '}
|
||||||
|
Your next payment is due on{' '}
|
||||||
|
<span className="font-semibold">
|
||||||
|
<LocaleDate date={subscription.periodEnd} />
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
</p>
|
||||||
{!isMissingOrInactiveOrFreePlan &&
|
|
||||||
match(subscription.status)
|
|
||||||
.with('ACTIVE', () => (
|
|
||||||
<p>
|
|
||||||
{subscriptionProduct ? (
|
|
||||||
<span>
|
|
||||||
You are currently subscribed to{' '}
|
|
||||||
<span className="font-semibold">{subscriptionProduct.name}</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>You currently have an active plan</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{subscription.periodEnd && (
|
|
||||||
<span>
|
|
||||||
{' '}
|
|
||||||
which is set to{' '}
|
|
||||||
{subscription.cancelAtPeriodEnd ? (
|
|
||||||
<span>
|
|
||||||
end on{' '}
|
|
||||||
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
automatically renew on{' '}
|
|
||||||
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
))
|
|
||||||
.with('PAST_DUE', () => (
|
|
||||||
<p>Your current plan is past due. Please update your payment information.</p>
|
|
||||||
))
|
|
||||||
.otherwise(() => null)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
{isMissingOrInactiveOrFreePlan ? <BillingPlans prices={prices} /> : <BillingPortalButton />}
|
{billingPortalUrl && (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href={billingPortalUrl}>Manage Subscription</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!billingPortalUrl && (
|
||||||
|
<p className="text-muted-foreground max-w-[60ch] text-base">
|
||||||
|
You do not currently have a customer record, this should not happen. Please contact
|
||||||
|
support for assistance.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,11 +9,12 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
|||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
|
|
||||||
import signingCelebration from '~/assets/signing-celebration.png';
|
import signingCelebration from '~/assets/signing-celebration.png';
|
||||||
|
|
||||||
|
import { ShareButton } from './share-button';
|
||||||
|
|
||||||
export type CompletedSigningPageProps = {
|
export type CompletedSigningPageProps = {
|
||||||
params: {
|
params: {
|
||||||
token?: string;
|
token?: string;
|
||||||
@ -88,7 +89,7 @@ export default async function CompletedSigningPage({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||||
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
<ShareButton documentId={document.id} token={recipient.token} />
|
||||||
|
|
||||||
<DocumentDownloadButton
|
<DocumentDownloadButton
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
|||||||
@ -5,14 +5,8 @@ import { HTMLAttributes, useState } from 'react';
|
|||||||
import { Copy, Share } from 'lucide-react';
|
import { Copy, Share } from 'lucide-react';
|
||||||
import { FaXTwitter } from 'react-icons/fa6';
|
import { FaXTwitter } from 'react-icons/fa6';
|
||||||
|
|
||||||
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
|
|
||||||
import {
|
|
||||||
TOAST_DOCUMENT_SHARE_ERROR,
|
|
||||||
TOAST_DOCUMENT_SHARE_SUCCESS,
|
|
||||||
} from '@documenso/lib/constants/toast';
|
|
||||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -24,18 +18,16 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DocumentShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||||
|
|
||||||
|
export type ShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||||
token: string;
|
token: string;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentShareButton = ({ token, documentId, className }: DocumentShareButtonProps) => {
|
export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
const { copyShareLink, createAndCopyShareLink, isCopyingShareLink } = useCopyShareLink({
|
|
||||||
onSuccess: () => toast(TOAST_DOCUMENT_SHARE_SUCCESS),
|
|
||||||
onError: () => toast(TOAST_DOCUMENT_SHARE_ERROR),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
@ -57,15 +49,24 @@ export const DocumentShareButton = ({ token, documentId, className }: DocumentSh
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCopyClick = async () => {
|
const onCopyClick = async () => {
|
||||||
if (shareLink) {
|
let { slug = '' } = shareLink || {};
|
||||||
await copyShareLink(`${window.location.origin}/share/${shareLink.slug}`);
|
|
||||||
} else {
|
if (!slug) {
|
||||||
await createAndCopyShareLink({
|
const result = await createOrGetShareLink({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
slug = result.slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
description: 'The sharing link has been copied to your clipboard.',
|
||||||
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ export const DocumentShareButton = ({ token, documentId, className }: DocumentSh
|
|||||||
window.open(
|
window.open(
|
||||||
generateTwitterIntent(
|
generateTwitterIntent(
|
||||||
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
|
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
|
||||||
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${slug}`,
|
`${window.location.origin}/share/${slug}`,
|
||||||
),
|
),
|
||||||
'_blank',
|
'_blank',
|
||||||
);
|
);
|
||||||
@ -98,10 +99,10 @@ export const DocumentShareButton = ({ token, documentId, className }: DocumentSh
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!token || !documentId}
|
disabled={!token || !documentId}
|
||||||
className={cn('flex-1', className)}
|
className="flex-1"
|
||||||
loading={isLoading || isCopyingShareLink}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
{!isLoading && !isCopyingShareLink && <Share className="mr-2 h-5 w-5" />}
|
{!isLoading && <Share className="mr-2 h-5 w-5" />}
|
||||||
Share
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@ -119,12 +120,8 @@ export const DocumentShareButton = ({ token, documentId, className }: DocumentSh
|
|||||||
<span className="font-medium text-blue-400">@documenso</span>
|
<span className="font-medium text-blue-400">@documenso</span>
|
||||||
. Check it out!
|
. Check it out!
|
||||||
<span className="mt-2 block" />
|
<span className="mt-2 block" />
|
||||||
<span
|
<span className="break-all font-medium text-blue-400">
|
||||||
className={cn('break-all font-medium text-blue-400', {
|
{window.location.origin}/share/{shareLink?.slug || '...'}
|
||||||
'animate-pulse': !shareLink?.slug,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{process.env.NEXT_PUBLIC_WEBAPP_URL}/share/{shareLink?.slug || '...'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useTransition } from 'react';
|
import { useMemo, useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
@ -48,7 +48,6 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
|
|
||||||
const [showSignatureModal, setShowSignatureModal] = useState(false);
|
const [showSignatureModal, setShowSignatureModal] = useState(false);
|
||||||
const [localSignature, setLocalSignature] = useState<string | null>(null);
|
const [localSignature, setLocalSignature] = useState<string | null>(null);
|
||||||
const [isLocalSignatureSet, setIsLocalSignatureSet] = useState(false);
|
|
||||||
|
|
||||||
const state = useMemo<SignatureFieldState>(() => {
|
const state = useMemo<SignatureFieldState>(() => {
|
||||||
if (!field.inserted) {
|
if (!field.inserted) {
|
||||||
@ -62,16 +61,9 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
return 'signed-text';
|
return 'signed-text';
|
||||||
}, [field.inserted, signature?.signatureImageAsBase64]);
|
}, [field.inserted, signature?.signatureImageAsBase64]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showSignatureModal && !isLocalSignatureSet) {
|
|
||||||
setLocalSignature(null);
|
|
||||||
}
|
|
||||||
}, [showSignatureModal, isLocalSignatureSet]);
|
|
||||||
|
|
||||||
const onSign = async (source: 'local' | 'provider' = 'provider') => {
|
const onSign = async (source: 'local' | 'provider' = 'provider') => {
|
||||||
try {
|
try {
|
||||||
if (!providedSignature && !localSignature) {
|
if (!providedSignature && !localSignature) {
|
||||||
setIsLocalSignatureSet(false);
|
|
||||||
setShowSignatureModal(true);
|
setShowSignatureModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -186,7 +178,6 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
disabled={!localSignature}
|
disabled={!localSignature}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowSignatureModal(false);
|
setShowSignatureModal(false);
|
||||||
setIsLocalSignatureSet(true);
|
|
||||||
void onSign('local');
|
void onSign('local');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -117,8 +117,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
name="signature"
|
name="signature"
|
||||||
render={({ field: { onChange } }) => (
|
render={({ field: { onChange } }) => (
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
className="h-44 w-full rounded-lg border bg-white backdrop-blur-sm dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
|
||||||
containerClassName="rounded-lg border bg-background"
|
|
||||||
defaultValue={user.signature ?? undefined}
|
defaultValue={user.signature ?? undefined}
|
||||||
onChange={(v) => onChange(v ?? '')}
|
onChange={(v) => onChange(v ?? '')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -147,8 +147,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
name="signature"
|
name="signature"
|
||||||
render={({ field: { onChange } }) => (
|
render={({ field: { onChange } }) => (
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-36 w-full"
|
className="mt-2 h-36 w-full rounded-lg border bg-white dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
|
||||||
containerClassName="mt-2 rounded-lg border bg-background"
|
|
||||||
onChange={(v) => onChange(v ?? '')}
|
onChange={(v) => onChange(v ?? '')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
28
apps/web/src/hooks/use-copy-to-clipboard.ts
Normal file
28
apps/web/src/hooks/use-copy-to-clipboard.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type CopiedValue = string | null;
|
||||||
|
export type CopyFn = (_text: string) => Promise<boolean>;
|
||||||
|
|
||||||
|
export function useCopyToClipboard(): [CopiedValue, CopyFn] {
|
||||||
|
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
|
||||||
|
|
||||||
|
const copy: CopyFn = async (text) => {
|
||||||
|
if (!navigator?.clipboard) {
|
||||||
|
console.warn('Clipboard not supported');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to save to clipboard then save it in the state if worked
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopiedText(text);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Copy failed', error);
|
||||||
|
setCopiedText(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [copiedText, copy];
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { buffer } from 'micro';
|
import { buffer } from 'micro';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
||||||
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
||||||
@ -17,7 +16,6 @@ import {
|
|||||||
ReadStatus,
|
ReadStatus,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
SubscriptionStatus,
|
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
|
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
|
||||||
@ -56,18 +54,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
);
|
);
|
||||||
log('event-type:', event.type);
|
log('event-type:', event.type);
|
||||||
|
|
||||||
if (event.type === 'customer.subscription.updated') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
|
||||||
|
|
||||||
await handleCustomerSubscriptionUpdated(subscription);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Webhook received',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'checkout.session.completed') {
|
if (event.type === 'checkout.session.completed') {
|
||||||
// This is required since we don't want to create a guard for every event type
|
// This is required since we don't want to create a guard for every event type
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
@ -209,29 +195,3 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
message: 'Unhandled webhook event',
|
message: 'Unhandled webhook event',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCustomerSubscriptionUpdated = async (subscription: Stripe.Subscription) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const { plan } = subscription as unknown as Stripe.SubscriptionItem;
|
|
||||||
|
|
||||||
const customerId =
|
|
||||||
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id;
|
|
||||||
|
|
||||||
const status = match(subscription.status)
|
|
||||||
.with('active', () => SubscriptionStatus.ACTIVE)
|
|
||||||
.with('past_due', () => SubscriptionStatus.PAST_DUE)
|
|
||||||
.otherwise(() => SubscriptionStatus.INACTIVE);
|
|
||||||
|
|
||||||
await prisma.subscription.update({
|
|
||||||
where: {
|
|
||||||
customerId: customerId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
planId: plan.id,
|
|
||||||
status,
|
|
||||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
||||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
|
|
||||||
|
|
||||||
export const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
|
|
||||||
export type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
|
|
||||||
350
package-lock.json
generated
350
package-lock.json
generated
@ -1848,10 +1848,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@documenso/app-tests": {
|
|
||||||
"resolved": "packages/app-tests",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/@documenso/ee": {
|
"node_modules/@documenso/ee": {
|
||||||
"resolved": "packages/ee",
|
"resolved": "packages/ee",
|
||||||
"link": true
|
"link": true
|
||||||
@ -2465,19 +2461,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hapi/hoek": {
|
|
||||||
"version": "9.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
|
||||||
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@hapi/topo": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hookform/resolvers": {
|
"node_modules/@hookform/resolvers": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz",
|
||||||
@ -3814,21 +3797,6 @@
|
|||||||
"url": "https://opencollective.com/unts"
|
"url": "https://opencollective.com/unts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
|
||||||
"version": "1.38.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
|
|
||||||
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"playwright": "1.38.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz",
|
||||||
@ -5478,24 +5446,6 @@
|
|||||||
"url": "https://ko-fi.com/killymxi"
|
"url": "https://ko-fi.com/killymxi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sideway/address": {
|
|
||||||
"version": "4.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
|
|
||||||
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/formula": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/pinpoint": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@sindresorhus/slugify": {
|
"node_modules/@sindresorhus/slugify": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
|
||||||
@ -7680,14 +7630,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/check-more-types": {
|
|
||||||
"version": "2.24.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
|
|
||||||
"integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
@ -9190,11 +9132,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/duplexer": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
|
||||||
},
|
|
||||||
"node_modules/duplexer2": {
|
"node_modules/duplexer2": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||||
@ -10399,20 +10336,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/event-stream": {
|
|
||||||
"version": "3.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
|
|
||||||
"integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==",
|
|
||||||
"dependencies": {
|
|
||||||
"duplexer": "~0.1.1",
|
|
||||||
"from": "~0",
|
|
||||||
"map-stream": "~0.1.0",
|
|
||||||
"pause-stream": "0.0.11",
|
|
||||||
"split": "0.3",
|
|
||||||
"stream-combiner": "~0.0.4",
|
|
||||||
"through": "~2.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
@ -10812,11 +10735,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/from": {
|
|
||||||
"version": "0.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
|
|
||||||
"integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="
|
|
||||||
},
|
|
||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
@ -12255,18 +12173,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
|
||||||
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="
|
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="
|
||||||
},
|
},
|
||||||
"node_modules/joi": {
|
|
||||||
"version": "17.10.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.10.2.tgz",
|
|
||||||
"integrity": "sha512-hcVhjBxRNW/is3nNLdGLIjkgXetkeGc2wyhydhz8KumG23Aerk4HPjU5zaPAMRqXQFc0xNqXTC7+zQjxr0GlKA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0",
|
|
||||||
"@hapi/topo": "^5.0.0",
|
|
||||||
"@sideway/address": "^4.1.3",
|
|
||||||
"@sideway/formula": "^3.0.1",
|
|
||||||
"@sideway/pinpoint": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jose": {
|
"node_modules/jose": {
|
||||||
"version": "4.14.4",
|
"version": "4.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
|
||||||
@ -12501,14 +12407,6 @@
|
|||||||
"language-subtag-registry": "~0.3.2"
|
"language-subtag-registry": "~0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lazy-ass": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
|
|
||||||
"engines": {
|
|
||||||
"node": "> 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/leac": {
|
"node_modules/leac": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||||
@ -13056,11 +12954,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/map-stream": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
|
|
||||||
},
|
|
||||||
"node_modules/markdown-extensions": {
|
"node_modules/markdown-extensions": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
|
||||||
@ -15114,14 +15007,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pause-stream": {
|
|
||||||
"version": "0.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
|
|
||||||
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"through": "~2.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pdf-lib": {
|
"node_modules/pdf-lib": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||||
@ -15220,36 +15105,6 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
|
||||||
"version": "1.38.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
|
|
||||||
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.38.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright-core": {
|
|
||||||
"version": "1.38.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
|
|
||||||
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.27",
|
"version": "8.4.27",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
|
||||||
@ -15800,20 +15655,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||||
},
|
},
|
||||||
"node_modules/ps-tree": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
|
|
||||||
"dependencies": {
|
|
||||||
"event-stream": "=3.3.4"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"ps-tree": "bin/ps-tree.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
@ -17457,14 +17298,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
|
"resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
|
||||||
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA=="
|
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA=="
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
|
||||||
"version": "7.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
|
||||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@ -17840,17 +17673,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
|
||||||
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w=="
|
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w=="
|
||||||
},
|
},
|
||||||
"node_modules/split": {
|
|
||||||
"version": "0.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
|
|
||||||
"integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==",
|
|
||||||
"dependencies": {
|
|
||||||
"through": "2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/split2": {
|
"node_modules/split2": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
|
||||||
@ -17882,121 +17704,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
},
|
},
|
||||||
"node_modules/start-server-and-test": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-8PFo4DLLLCDMuS51/BEEtE1m9CAXw1LNVtZSS1PzkYQh6Qf9JUwM4huYeSoUumaaoAyuwYBwCa9OsrcpMqcOdQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"arg": "^5.0.2",
|
|
||||||
"bluebird": "3.7.2",
|
|
||||||
"check-more-types": "2.24.0",
|
|
||||||
"debug": "4.3.4",
|
|
||||||
"execa": "5.1.1",
|
|
||||||
"lazy-ass": "1.6.0",
|
|
||||||
"ps-tree": "1.2.0",
|
|
||||||
"wait-on": "7.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"server-test": "src/bin/start.js",
|
|
||||||
"start-server-and-test": "src/bin/start.js",
|
|
||||||
"start-test": "src/bin/start.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/arg": {
|
|
||||||
"version": "5.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
|
||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/bluebird": {
|
|
||||||
"version": "3.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
|
||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/execa": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
|
||||||
"dependencies": {
|
|
||||||
"cross-spawn": "^7.0.3",
|
|
||||||
"get-stream": "^6.0.0",
|
|
||||||
"human-signals": "^2.1.0",
|
|
||||||
"is-stream": "^2.0.0",
|
|
||||||
"merge-stream": "^2.0.0",
|
|
||||||
"npm-run-path": "^4.0.1",
|
|
||||||
"onetime": "^5.1.2",
|
|
||||||
"signal-exit": "^3.0.3",
|
|
||||||
"strip-final-newline": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/human-signals": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/is-stream": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/mimic-fn": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/npm-run-path": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/onetime": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-fn": "^2.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/start-server-and-test/node_modules/strip-final-newline": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||||
@ -18005,14 +17712,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stream-combiner": {
|
|
||||||
"version": "0.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
|
|
||||||
"integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
|
|
||||||
"dependencies": {
|
|
||||||
"duplexer": "~0.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stream-shift": {
|
"node_modules/stream-shift": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
|
||||||
@ -18558,7 +18257,8 @@
|
|||||||
"node_modules/through": {
|
"node_modules/through": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/through2": {
|
"node_modules/through2": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
@ -19422,12 +19122,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "5.25.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
|
|
||||||
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/unified": {
|
"node_modules/unified": {
|
||||||
"version": "10.1.2",
|
"version": "10.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
|
||||||
@ -19825,24 +19519,6 @@
|
|||||||
"d3-timer": "^3.0.1"
|
"d3-timer": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wait-on": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==",
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^0.27.2",
|
|
||||||
"joi": "^17.7.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"minimist": "^1.2.7",
|
|
||||||
"rxjs": "^7.8.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"wait-on": "bin/wait-on"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||||
@ -20112,28 +19788,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/app-tests": {
|
|
||||||
"name": "@documenso/app-tests",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "to-update",
|
|
||||||
"dependencies": {
|
|
||||||
"start-server-and-test": "^2.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@documenso/web": "*",
|
|
||||||
"@playwright/test": "^1.18.1",
|
|
||||||
"@types/node": "^20.8.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/app-tests/node_modules/@types/node": {
|
|
||||||
"version": "20.8.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz",
|
|
||||||
"integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~5.25.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/ee": {
|
"packages/ee": {
|
||||||
"name": "@documenso/ee",
|
"name": "@documenso/ee",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
"dx": "npm i && npm run dx:up && npm run prisma:migrate-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:up": "docker compose -f docker/compose-services.yml up -d",
|
||||||
"dx:down": "docker compose -f docker/compose-services.yml down",
|
"dx:down": "docker compose -f docker/compose-services.yml down",
|
||||||
"ci": "turbo run build test:e2e",
|
|
||||||
"prisma:migrate-dev": "npm run with:env -- npm run prisma:migrate-dev -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",
|
"prisma:migrate-deploy": "npm run with:env -- npm run prisma:migrate-deploy -w @documenso/prisma",
|
||||||
"with:env": "dotenv -e .env -e .env.local --"
|
"with:env": "dotenv -e .env -e .env.local --"
|
||||||
|
|||||||
4
packages/app-tests/.gitignore
vendored
4
packages/app-tests/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { type Page, expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
|
||||||
|
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
|
||||||
|
|
||||||
/*
|
|
||||||
Using them sequentially so the 2nd test
|
|
||||||
uses the details from the 1st (registration) test
|
|
||||||
*/
|
|
||||||
test.describe.configure({ mode: 'serial' });
|
|
||||||
|
|
||||||
const username = process.env.E2E_TEST_AUTHENTICATE_USERNAME;
|
|
||||||
const email = process.env.E2E_TEST_AUTHENTICATE_USER_EMAIL;
|
|
||||||
const password = process.env.E2E_TEST_AUTHENTICATE_USER_PASSWORD;
|
|
||||||
|
|
||||||
test('user can sign up with email and password', async ({ page }: { page: Page }) => {
|
|
||||||
await page.goto('/signup');
|
|
||||||
await page.getByLabel('Name').fill(username);
|
|
||||||
await page.getByLabel('Email').fill(email);
|
|
||||||
await page.getByLabel('Password', { exact: true }).fill(password);
|
|
||||||
|
|
||||||
const canvas = page.locator('canvas');
|
|
||||||
const box = await canvas.boundingBox();
|
|
||||||
|
|
||||||
if (box) {
|
|
||||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
|
|
||||||
await page.mouse.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Sign Up' }).click();
|
|
||||||
await page.waitForURL('/documents');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL('/documents');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('user can login with user and password', async ({ page }: { page: Page }) => {
|
|
||||||
await page.goto('/signin');
|
|
||||||
await page.getByLabel('Email').fill(email);
|
|
||||||
await page.getByLabel('Password', { exact: true }).fill(password);
|
|
||||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
|
||||||
|
|
||||||
await page.waitForURL('/documents');
|
|
||||||
await expect(page).toHaveURL('/documents');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterAll('Teardown', async () => {
|
|
||||||
try {
|
|
||||||
await deleteUser({ email });
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Error deleting user: ${e}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@documenso/app-tests",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "to-update",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test:dev": "playwright test",
|
|
||||||
"test:e2e": "start-server-and-test \"(cd ../../apps/web && npm run start)\" http://localhost:3000 \"playwright test\""
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.18.1",
|
|
||||||
"@types/node": "^20.8.2",
|
|
||||||
"@documenso/web": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"start-server-and-test": "^2.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read environment variables from file.
|
|
||||||
* https://github.com/motdotla/dotenv
|
|
||||||
*/
|
|
||||||
// require('dotenv').config();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './e2e',
|
|
||||||
/* Run tests in files in parallel */
|
|
||||||
fullyParallel: true,
|
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
/* Retry on CI only */
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
/* Opt out of parallel tests on CI. */
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
||||||
reporter: 'html',
|
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
||||||
use: {
|
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
||||||
baseURL: 'http://localhost:3000',
|
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
||||||
trace: 'on-first-retry',
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
|
|
||||||
// {
|
|
||||||
// name: 'firefox',
|
|
||||||
// use: { ...devices['Desktop Firefox'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
// {
|
|
||||||
// name: 'webkit',
|
|
||||||
// use: { ...devices['Desktop Safari'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Chrome',
|
|
||||||
// use: { ...devices['Pixel 5'] },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Safari',
|
|
||||||
// use: { ...devices['iPhone 12'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against branded browsers. */
|
|
||||||
// {
|
|
||||||
// name: 'Microsoft Edge',
|
|
||||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Google Chrome',
|
|
||||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
|
||||||
// webServer: {
|
|
||||||
// command: 'npm run start',
|
|
||||||
// url: 'http://127.0.0.1:3000',
|
|
||||||
// reuseExistingServer: !process.env.CI,
|
|
||||||
// },
|
|
||||||
});
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
|
|
||||||
export type GetCheckoutSessionOptions = {
|
|
||||||
customerId: string;
|
|
||||||
priceId: string;
|
|
||||||
returnUrl: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCheckoutSession = async ({
|
|
||||||
customerId,
|
|
||||||
priceId,
|
|
||||||
returnUrl,
|
|
||||||
}: GetCheckoutSessionOptions) => {
|
|
||||||
'use server';
|
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
|
||||||
customer: customerId,
|
|
||||||
line_items: [
|
|
||||||
{
|
|
||||||
price: priceId,
|
|
||||||
quantity: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
success_url: `${returnUrl}?success=true`,
|
|
||||||
cancel_url: `${returnUrl}?canceled=true`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return session.url;
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
|
|
||||||
export const getStripeCustomerByEmail = async (email: string) => {
|
|
||||||
const foundStripeCustomers = await stripe.customers.list({
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
|
|
||||||
return foundStripeCustomers.data[0] ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStripeCustomerById = async (stripeCustomerId: string) => {
|
|
||||||
try {
|
|
||||||
const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId);
|
|
||||||
|
|
||||||
return !stripeCustomer.deleted ? stripeCustomer : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import Stripe from 'stripe';
|
|
||||||
|
|
||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
|
|
||||||
// Utility type to handle usage of the `expand` option.
|
|
||||||
type PriceWithProduct = Stripe.Price & { product: Stripe.Product };
|
|
||||||
|
|
||||||
export type PriceIntervals = Record<Stripe.Price.Recurring.Interval, PriceWithProduct[]>;
|
|
||||||
|
|
||||||
export const getPricesByInterval = async () => {
|
|
||||||
let { data: prices } = await stripe.prices.search({
|
|
||||||
query: `active:'true' type:'recurring'`,
|
|
||||||
expand: ['data.product'],
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
prices = prices.filter((price) => {
|
|
||||||
// We use `expand` to get the product, but it's not typed as part of the Price type.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const product = price.product as Stripe.Product;
|
|
||||||
|
|
||||||
// Filter out prices for products that are not active.
|
|
||||||
return product.active;
|
|
||||||
});
|
|
||||||
|
|
||||||
const intervals: PriceIntervals = {
|
|
||||||
day: [],
|
|
||||||
week: [],
|
|
||||||
month: [],
|
|
||||||
year: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add each price to the correct interval.
|
|
||||||
for (const price of prices) {
|
|
||||||
if (price.recurring?.interval) {
|
|
||||||
// We use `expand` to get the product, but it's not typed as part of the Price type.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
intervals[price.recurring.interval].push(price as PriceWithProduct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order all prices by unit_amount.
|
|
||||||
intervals.day.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
|
||||||
intervals.week.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
|
||||||
intervals.month.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
|
||||||
intervals.year.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
|
||||||
|
|
||||||
return intervals;
|
|
||||||
};
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema';
|
|
||||||
|
|
||||||
import { useCopyToClipboard } from './use-copy-to-clipboard';
|
|
||||||
|
|
||||||
export type UseCopyShareLinkOptions = {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onError?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useCopyShareLink({ onSuccess, onError }: UseCopyShareLinkOptions) {
|
|
||||||
const [, copyToClipboard] = useCopyToClipboard();
|
|
||||||
|
|
||||||
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
|
|
||||||
trpc.shareLink.createOrGetShareLink.useMutation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a newly created, or pre-existing share link to the user's clipboard.
|
|
||||||
*
|
|
||||||
* @param payload The payload to create or get a share link.
|
|
||||||
*/
|
|
||||||
const createAndCopyShareLink = async (payload: TCreateOrGetShareLinkMutationSchema) => {
|
|
||||||
const valueToCopy = createOrGetShareLink(payload).then(
|
|
||||||
(result) => `${window.location.origin}/share/${result.slug}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await copyShareLink(valueToCopy);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a share link to the user's clipboard.
|
|
||||||
*
|
|
||||||
* @param shareLink Either the share link itself or a promise that returns a shared link.
|
|
||||||
*/
|
|
||||||
const copyShareLink = async (shareLink: Promise<string> | string) => {
|
|
||||||
try {
|
|
||||||
const isCopySuccess = await copyToClipboard(shareLink);
|
|
||||||
if (!isCopySuccess) {
|
|
||||||
throw new Error('Copy to clipboard failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
onSuccess?.();
|
|
||||||
} catch (e) {
|
|
||||||
onError?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
createAndCopyShareLink,
|
|
||||||
copyShareLink,
|
|
||||||
isCopyingShareLink: isCreatingShareLink,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export type CopiedValue = string | null;
|
|
||||||
export type CopyFn = (_text: CopyValue, _blobType?: string) => Promise<boolean>;
|
|
||||||
|
|
||||||
type CopyValue = Promise<string> | string;
|
|
||||||
|
|
||||||
export function useCopyToClipboard(): [CopiedValue, CopyFn] {
|
|
||||||
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
|
|
||||||
|
|
||||||
const copy: CopyFn = async (text, blobType = 'text/plain') => {
|
|
||||||
if (!navigator?.clipboard) {
|
|
||||||
console.warn('Clipboard not supported');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isClipboardApiSupported = Boolean(typeof ClipboardItem && navigator.clipboard.write);
|
|
||||||
|
|
||||||
// Try to save to clipboard then save it in the state if worked
|
|
||||||
try {
|
|
||||||
isClipboardApiSupported
|
|
||||||
? await handleClipboardApiCopy(text, blobType)
|
|
||||||
: await handleWriteTextCopy(text);
|
|
||||||
|
|
||||||
setCopiedText(await text);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Copy failed', error);
|
|
||||||
setCopiedText(null);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle copying values to the clipboard using the ClipboardItem API.
|
|
||||||
*
|
|
||||||
* Works in all browsers except FireFox.
|
|
||||||
*
|
|
||||||
* https://caniuse.com/mdn-api_clipboarditem
|
|
||||||
*/
|
|
||||||
const handleClipboardApiCopy = async (value: CopyValue, blobType = 'text/plain') => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.write([new ClipboardItem({ [blobType]: value })]);
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback attempt.
|
|
||||||
await handleWriteTextCopy(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle copying values to the clipboard using `writeText`.
|
|
||||||
*
|
|
||||||
* Works in all browsers except Safari for async values.
|
|
||||||
*/
|
|
||||||
const handleWriteTextCopy = async (value: CopyValue) => {
|
|
||||||
await navigator.clipboard.writeText(await value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [copiedText, copy];
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Toast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
export const TOAST_DOCUMENT_SHARE_SUCCESS: Toast = {
|
|
||||||
title: 'Copied to clipboard',
|
|
||||||
description: 'The sharing link has been copied to your clipboard.',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const TOAST_DOCUMENT_SHARE_ERROR: Toast = {
|
|
||||||
variant: 'destructive',
|
|
||||||
title: 'Something went wrong',
|
|
||||||
description: 'The sharing link could not be created at this time. Please try again.',
|
|
||||||
duration: 5000,
|
|
||||||
};
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { Prisma } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export interface FindDocumentsOptions {
|
|
||||||
term?: string;
|
|
||||||
page?: number;
|
|
||||||
perPage?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findDocuments = async ({ term, page = 1, perPage = 10 }: FindDocumentsOptions) => {
|
|
||||||
const termFilters: Prisma.DocumentWhereInput | undefined = !term
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
title: {
|
|
||||||
contains: term,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const [data, count] = await Promise.all([
|
|
||||||
prisma.document.findMany({
|
|
||||||
where: {
|
|
||||||
...termFilters,
|
|
||||||
},
|
|
||||||
skip: Math.max(page - 1, 0) * perPage,
|
|
||||||
take: perPage,
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
User: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Recipient: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.document.count({
|
|
||||||
where: {
|
|
||||||
...termFilters,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
count,
|
|
||||||
currentPage: Math.max(page, 1),
|
|
||||||
perPage,
|
|
||||||
totalPages: Math.ceil(count / perPage),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
export const findSubscriptions = async () => {
|
|
||||||
return prisma.subscription.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
status: true,
|
|
||||||
createdAt: true,
|
|
||||||
periodEnd: true,
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { Role } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export type UpdateUserOptions = {
|
|
||||||
id: number;
|
|
||||||
name: string | null | undefined;
|
|
||||||
email: string | undefined;
|
|
||||||
roles: Role[] | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions) => {
|
|
||||||
await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
roles,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
/// <reference types="./stripe.d.ts" />
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
|
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
|
||||||
|
|||||||
7
packages/lib/server-only/stripe/stripe.d.ts
vendored
7
packages/lib/server-only/stripe/stripe.d.ts
vendored
@ -1,7 +0,0 @@
|
|||||||
declare module 'stripe' {
|
|
||||||
namespace Stripe {
|
|
||||||
interface Product {
|
|
||||||
features?: Array<{ name: string }>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
export type DeleteUserOptions = {
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteUser = async ({ email }: DeleteUserOptions) => {
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
email: {
|
|
||||||
contains: email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error(`User with email ${email} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.user.delete({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { Prisma } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
type GetAllUsersProps = {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
page: number;
|
|
||||||
perPage: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findUsers = async ({
|
|
||||||
username = '',
|
|
||||||
email = '',
|
|
||||||
page = 1,
|
|
||||||
perPage = 10,
|
|
||||||
}: GetAllUsersProps) => {
|
|
||||||
const whereClause = Prisma.validator<Prisma.UserWhereInput>()({
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
contains: username,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: {
|
|
||||||
contains: email,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [users, count] = await Promise.all([
|
|
||||||
await prisma.user.findMany({
|
|
||||||
include: {
|
|
||||||
Subscription: true,
|
|
||||||
Document: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: whereClause,
|
|
||||||
skip: Math.max(page - 1, 0) * perPage,
|
|
||||||
take: perPage,
|
|
||||||
}),
|
|
||||||
await prisma.user.count({
|
|
||||||
where: whereClause,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
users,
|
|
||||||
totalPages: Math.ceil(count / perPage),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export const toHumanPrice = (price: number) => {
|
|
||||||
return Number(price / 100).toFixed(2);
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- A unique constraint covering the columns `[userId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
- Made the column `customerId` on table `Subscription` required. This step will fail if there are existing NULL values in that column.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
DELETE FROM "Subscription"
|
|
||||||
WHERE "customerId" IS NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Subscription" ADD COLUMN "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
ALTER COLUMN "customerId" SET NOT NULL;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "DocumentShareLink" DROP CONSTRAINT "DocumentShareLink_documentId_fkey";
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "DocumentShareLink" ADD CONSTRAINT "DocumentShareLink_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@ -51,16 +51,15 @@ enum SubscriptionStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Subscription {
|
model Subscription {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
status SubscriptionStatus @default(INACTIVE)
|
status SubscriptionStatus @default(INACTIVE)
|
||||||
planId String?
|
planId String?
|
||||||
priceId String?
|
priceId String?
|
||||||
customerId String
|
customerId String?
|
||||||
periodEnd DateTime?
|
periodEnd DateTime?
|
||||||
userId Int @unique
|
userId Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
cancelAtPeriodEnd Boolean @default(false)
|
|
||||||
|
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@ -220,7 +219,7 @@ model DocumentShareLink {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
document Document @relation(fields: [documentId], references: [id])
|
||||||
|
|
||||||
@@unique([documentId, email])
|
@@unique([documentId, email])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
|
||||||
|
|
||||||
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
|
|
||||||
|
|
||||||
import { adminProcedure, router } from '../trpc';
|
|
||||||
import { ZUpdateProfileMutationByAdminSchema } from './schema';
|
|
||||||
|
|
||||||
export const adminRouter = router({
|
|
||||||
updateUser: adminProcedure
|
|
||||||
.input(ZUpdateProfileMutationByAdminSchema)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
const { id, name, email, roles } = input;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await updateUser({ id, name, email, roles });
|
|
||||||
} catch (err) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'We were unable to retrieve the specified account. Please try again.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Role } from '@prisma/client';
|
|
||||||
import z from 'zod';
|
|
||||||
|
|
||||||
export const ZUpdateProfileMutationByAdminSchema = 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
|
|
||||||
>;
|
|
||||||
@ -1,34 +1,19 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
|
||||||
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
||||||
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||||
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
||||||
|
|
||||||
import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
ZForgotPasswordFormSchema,
|
ZForgotPasswordFormSchema,
|
||||||
ZResetPasswordFormSchema,
|
ZResetPasswordFormSchema,
|
||||||
ZRetrieveUserByIdQuerySchema,
|
|
||||||
ZUpdatePasswordMutationSchema,
|
ZUpdatePasswordMutationSchema,
|
||||||
ZUpdateProfileMutationSchema,
|
ZUpdateProfileMutationSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
export const profileRouter = router({
|
export const profileRouter = router({
|
||||||
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => {
|
|
||||||
try {
|
|
||||||
const { id } = input;
|
|
||||||
|
|
||||||
return await getUserById({ id });
|
|
||||||
} catch (err) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'We were unable to retrieve the specified account. Please try again.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateProfile: authenticatedProcedure
|
updateProfile: authenticatedProcedure
|
||||||
.input(ZUpdateProfileMutationSchema)
|
.input(ZUpdateProfileMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ZRetrieveUserByIdQuerySchema = z.object({
|
|
||||||
id: z.number().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZUpdateProfileMutationSchema = z.object({
|
export const ZUpdateProfileMutationSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
signature: z.string(),
|
signature: z.string(),
|
||||||
@ -23,7 +19,6 @@ export const ZResetPasswordFormSchema = z.object({
|
|||||||
token: z.string().min(1),
|
token: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TRetrieveUserByIdQuerySchema = z.infer<typeof ZRetrieveUserByIdQuerySchema>;
|
|
||||||
export type TUpdateProfileMutationSchema = z.infer<typeof ZUpdateProfileMutationSchema>;
|
export type TUpdateProfileMutationSchema = z.infer<typeof ZUpdateProfileMutationSchema>;
|
||||||
export type TUpdatePasswordMutationSchema = z.infer<typeof ZUpdatePasswordMutationSchema>;
|
export type TUpdatePasswordMutationSchema = z.infer<typeof ZUpdatePasswordMutationSchema>;
|
||||||
export type TForgotPasswordFormSchema = z.infer<typeof ZForgotPasswordFormSchema>;
|
export type TForgotPasswordFormSchema = z.infer<typeof ZForgotPasswordFormSchema>;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { adminRouter } from './admin-router/router';
|
|
||||||
import { authRouter } from './auth-router/router';
|
import { authRouter } from './auth-router/router';
|
||||||
import { documentRouter } from './document-router/router';
|
import { documentRouter } from './document-router/router';
|
||||||
import { fieldRouter } from './field-router/router';
|
import { fieldRouter } from './field-router/router';
|
||||||
@ -14,7 +13,6 @@ export const appRouter = router({
|
|||||||
profile: profileRouter,
|
profile: profileRouter,
|
||||||
document: documentRouter,
|
document: documentRouter,
|
||||||
field: fieldRouter,
|
field: fieldRouter,
|
||||||
admin: adminRouter,
|
|
||||||
shareLink: shareLinkRouter,
|
shareLink: shareLinkRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { TRPCError, initTRPC } from '@trpc/server';
|
import { TRPCError, initTRPC } from '@trpc/server';
|
||||||
import SuperJSON from 'superjson';
|
import SuperJSON from 'superjson';
|
||||||
|
|
||||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
|
||||||
|
|
||||||
import { TrpcContext } from './context';
|
import { TrpcContext } from './context';
|
||||||
|
|
||||||
const t = initTRPC.context<TrpcContext>().create({
|
const t = initTRPC.context<TrpcContext>().create({
|
||||||
@ -30,37 +28,9 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const adminMiddleware = t.middleware(async ({ ctx, next }) => {
|
|
||||||
if (!ctx.session || !ctx.user) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'You must be logged in to perform this action.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserAdmin = isAdmin(ctx.user);
|
|
||||||
|
|
||||||
if (!isUserAdmin) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: 'Not authorized to perform this action.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await next({
|
|
||||||
ctx: {
|
|
||||||
...ctx,
|
|
||||||
|
|
||||||
user: ctx.user,
|
|
||||||
session: ctx.session,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routers and Procedures
|
* Routers and Procedures
|
||||||
*/
|
*/
|
||||||
export const router = t.router;
|
export const router = t.router;
|
||||||
export const procedure = t.procedure;
|
export const procedure = t.procedure;
|
||||||
export const authenticatedProcedure = t.procedure.use(authenticatedMiddleware);
|
export const authenticatedProcedure = t.procedure.use(authenticatedMiddleware);
|
||||||
export const adminProcedure = t.procedure.use(adminMiddleware);
|
|
||||||
|
|||||||
1
packages/tsconfig/process-env.d.ts
vendored
1
packages/tsconfig/process-env.d.ts
vendored
@ -10,7 +10,6 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Role } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
} from '@documenso/ui/primitives/command';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
|
||||||
|
|
||||||
type ComboboxProps = {
|
|
||||||
listValues: string[];
|
|
||||||
onChange: (_values: string[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Combobox = ({ listValues, onChange }: ComboboxProps) => {
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const [selectedValues, setSelectedValues] = React.useState<string[]>([]);
|
|
||||||
const dbRoles = Object.values(Role);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setSelectedValues(listValues);
|
|
||||||
}, [listValues]);
|
|
||||||
|
|
||||||
const allRoles = [...new Set([...dbRoles, ...selectedValues])];
|
|
||||||
|
|
||||||
const handleSelect = (currentValue: string) => {
|
|
||||||
let newSelectedValues;
|
|
||||||
if (selectedValues.includes(currentValue)) {
|
|
||||||
newSelectedValues = selectedValues.filter((value) => value !== currentValue);
|
|
||||||
} else {
|
|
||||||
newSelectedValues = [...selectedValues, currentValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedValues(newSelectedValues);
|
|
||||||
onChange(newSelectedValues);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={open}
|
|
||||||
className="w-[200px] justify-between"
|
|
||||||
>
|
|
||||||
{selectedValues.length > 0 ? selectedValues.join(', ') : 'Select values...'}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[200px] p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder={selectedValues.join(', ')} />
|
|
||||||
<CommandEmpty>No value found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{allRoles.map((value: string, i: number) => (
|
|
||||||
<CommandItem key={i} onSelect={() => handleSelect(value)}>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
'mr-2 h-4 w-4',
|
|
||||||
selectedValues.includes(value) ? 'opacity-100' : 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{value}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Combobox };
|
|
||||||
@ -22,12 +22,10 @@ const DPI = 2;
|
|||||||
|
|
||||||
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
|
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
|
||||||
onChange?: (_signatureDataUrl: string | null) => void;
|
onChange?: (_signatureDataUrl: string | null) => void;
|
||||||
containerClassName?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignaturePad = ({
|
export const SignaturePad = ({
|
||||||
className,
|
className,
|
||||||
containerClassName,
|
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
...props
|
||||||
@ -212,7 +210,7 @@ export const SignaturePad = ({
|
|||||||
}, [defaultValue]);
|
}, [defaultValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative block', containerClassName)}>
|
<div className="relative block">
|
||||||
<canvas
|
<canvas
|
||||||
ref={$el}
|
ref={$el}
|
||||||
className={cn('relative block dark:invert', className)}
|
className={cn('relative block dark:invert', className)}
|
||||||
@ -228,7 +226,7 @@ export const SignaturePad = ({
|
|||||||
<div className="absolute bottom-4 right-4">
|
<div className="absolute bottom-4 right-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="focus-visible:ring-ring ring-offset-background text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
|
className="focus-visible:ring-ring ring-offset-background rounded-full p-0 text-xs text-slate-500 focus-visible:outline-none focus-visible:ring-2"
|
||||||
onClick={() => onClearClick()}
|
onClick={() => onClearClick()}
|
||||||
>
|
>
|
||||||
Clear Signature
|
Clear Signature
|
||||||
|
|||||||
@ -133,7 +133,7 @@ function dispatch(action: Action) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Toast = Omit<ToasterToast, 'id'>;
|
type Toast = Omit<ToasterToast, 'id'>;
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
function toast({ ...props }: Toast) {
|
||||||
const id = genId();
|
const id = genId();
|
||||||
|
|||||||
25
turbo.json
25
turbo.json
@ -2,8 +2,13 @@
|
|||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
"outputs": [".next/**", "!.next/cache/**"]
|
"^build"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
".next/**",
|
||||||
|
"!.next/cache/**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"lint": {},
|
"lint": {},
|
||||||
"clean": {
|
"clean": {
|
||||||
@ -12,15 +17,11 @@
|
|||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
},
|
|
||||||
"dev:test": {
|
|
||||||
"cache": false
|
|
||||||
},
|
|
||||||
"test:e2e": {
|
|
||||||
"dependsOn": ["^build"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globalDependencies": ["**/.env.*local"],
|
"globalDependencies": [
|
||||||
|
"**/.env.*local"
|
||||||
|
],
|
||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"APP_VERSION",
|
"APP_VERSION",
|
||||||
"NEXTAUTH_URL",
|
"NEXTAUTH_URL",
|
||||||
@ -33,7 +34,6 @@
|
|||||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
||||||
"NEXT_PUBLIC_STRIPE_FREE_PLAN_ID",
|
|
||||||
"NEXT_PRIVATE_DATABASE_URL",
|
"NEXT_PRIVATE_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
||||||
@ -73,9 +73,6 @@
|
|||||||
"POSTGRES_URL",
|
"POSTGRES_URL",
|
||||||
"DATABASE_URL",
|
"DATABASE_URL",
|
||||||
"POSTGRES_PRISMA_URL",
|
"POSTGRES_PRISMA_URL",
|
||||||
"POSTGRES_URL_NON_POOLING",
|
"POSTGRES_URL_NON_POOLING"
|
||||||
"E2E_TEST_AUTHENTICATE_USERNAME",
|
|
||||||
"E2E_TEST_AUTHENTICATE_USER_EMAIL",
|
|
||||||
"E2E_TEST_AUTHENTICATE_USER_PASSWORD"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user