From 773566f19384921b98f720fb8222a87f17d87a20 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 18 Sep 2023 22:33:07 +1000 Subject: [PATCH 001/133] feat: add free tier Stripe subscription --- .env.example | 1 + apps/marketing/process-env.d.ts | 1 + apps/web/process-env.d.ts | 1 + .../billing/billing-portal-button.tsx | 54 +++++++++ .../billing/create-billing-portal.action.ts | 80 ++++++++++++++ .../app/(dashboard)/settings/billing/page.tsx | 103 ++++++++++-------- .../web/src/pages/api/stripe/webhook/index.ts | 40 +++++++ .../ee/server-only/stripe/get-customer.ts | 19 ++++ .../migration.sql | 17 +++ packages/prisma/schema.prisma | 19 ++-- packages/tsconfig/process-env.d.ts | 1 + packages/ui/primitives/button.tsx | 4 +- turbo.json | 16 ++- 13 files changed, 298 insertions(+), 58 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx create mode 100644 apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts create mode 100644 packages/ee/server-only/stripe/get-customer.ts create mode 100644 packages/prisma/migrations/20230918111438_update_subscription_constraints_and_columns/migration.sql diff --git a/.env.example b/.env.example index 6f32b5a63..4450ef310 100644 --- a/.env.example +++ b/.env.example @@ -66,6 +66,7 @@ NEXT_PRIVATE_STRIPE_API_KEY= NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET= NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID= NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID= +NEXT_PUBLIC_STRIPE_FREE_PLAN_ID= # [[FEATURES]] # OPTIONAL: Leave blank to disable PostHog and feature flags. diff --git a/apps/marketing/process-env.d.ts b/apps/marketing/process-env.d.ts index ac170a616..53b126e57 100644 --- a/apps/marketing/process-env.d.ts +++ b/apps/marketing/process-env.d.ts @@ -6,6 +6,7 @@ declare namespace NodeJS { NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_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_WEBHOOK_SECRET: string; diff --git a/apps/web/process-env.d.ts b/apps/web/process-env.d.ts index 1cb0018ac..dfada0e57 100644 --- a/apps/web/process-env.d.ts +++ b/apps/web/process-env.d.ts @@ -6,6 +6,7 @@ declare namespace NodeJS { NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_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_WEBHOOK_SECRET: string; diff --git a/apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx b/apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx new file mode 100644 index 000000000..994f7c221 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx @@ -0,0 +1,54 @@ +'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 default function 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 ( + + ); +} diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts new file mode 100644 index 000000000..f235cb846 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -0,0 +1,80 @@ +'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'; +import { prisma } from '@documenso/prisma'; + +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. + 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, + }, + }); + } + + const stripeCustomerSubsriptions = stripeCustomer.subscriptions?.data ?? []; + + // Create a free subscription for user if it does not exist. + if (!existingSubscription && stripeCustomerSubsriptions.length === 0) { + const newSubscription = await stripe.subscriptions.create({ + customer: stripeCustomer.id, + items: [ + { + plan: process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID, + }, + ], + }); + + await prisma.subscription.upsert({ + where: { + userId: user.id, + customerId: stripeCustomer.id, + }, + create: { + userId: user.id, + customerId: stripeCustomer.id, + planId: process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID, + periodEnd: new Date(newSubscription.current_period_end * 1000), + status: 'ACTIVE', + }, + update: { + planId: process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID, + periodEnd: new Date(newSubscription.current_period_end * 1000), + status: 'ACTIVE', + }, + }); + } + + return getPortalSession({ + customerId: stripeCustomer.id, + returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`, + }); +}; diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 555c645ce..bbc158fe0 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -1,16 +1,16 @@ -import Link from 'next/link'; import { redirect } from 'next/navigation'; -import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer'; -import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; +import { match } from 'ts-pattern'; + 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'; -import { SubscriptionStatus } from '@documenso/prisma/client'; -import { Button } from '@documenso/ui/primitives/button'; import { LocaleDate } from '~/components/formatter/locale-date'; import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag'; +import BillingPortalButton from './billing-portal-button'; + export default async function BillingSettingsPage() { const user = await getRequiredServerComponentSession(); @@ -21,57 +21,74 @@ export default async function BillingSettingsPage() { redirect('/settings/profile'); } - const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => { - if (sub) { - return sub; - } + const subscription = await getSubscriptionByUserId({ userId: user.id }); - // If we don't have a customer record, create one as well as an empty subscription. - return createCustomer({ user }); - }); + let subscriptionProduct: Stripe.Product | null = null; - let billingPortalUrl = ''; + if (subscription?.planId) { + const foundSubscriptionProduct = (await stripe.products.list()).data.find( + (item) => item.default_price === subscription.planId, + ); - if (subscription.customerId) { - billingPortalUrl = await getPortalSession({ - customerId: subscription.customerId, - returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`, - }); + subscriptionProduct = foundSubscriptionProduct ?? null; } + const isMissingOrInactiveOrFreePlan = + !subscription || + subscription.status === 'INACTIVE' || + subscription?.planId === process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID; + return (

Billing

-

- Your subscription is{' '} - {subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}. - {subscription?.periodEnd && ( - <> - {' '} - Your next payment is due on{' '} - - - - . - +

+ {isMissingOrInactiveOrFreePlan && ( +

+ You are currently on the Free Plan. +

)} -

+ + {!isMissingOrInactiveOrFreePlan && + match(subscription.status) + .with('ACTIVE', () => ( +

+ {subscriptionProduct ? ( + + You are currently subscribed to{' '} + {subscriptionProduct.name} + + ) : ( + You currently have an active plan + )} + {subscription.periodEnd && ( + + {' '} + which is set to{' '} + {subscription.cancelAtPeriodEnd ? ( + + end on{' '} + . + + ) : ( + + automatically renew on{' '} + . + + )} + + )} +

+ )) + .with('PAST_DUE', () => ( +

Your current plan is past due. Please update your payment information.

+ )) + .otherwise(() => null)} +

- {billingPortalUrl && ( - - )} - - {!billingPortalUrl && ( -

- You do not currently have a customer record, this should not happen. Please contact - support for assistance. -

- )} +
); } diff --git a/apps/web/src/pages/api/stripe/webhook/index.ts b/apps/web/src/pages/api/stripe/webhook/index.ts index 9efab2a78..06f63e293 100644 --- a/apps/web/src/pages/api/stripe/webhook/index.ts +++ b/apps/web/src/pages/api/stripe/webhook/index.ts @@ -3,6 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { randomBytes } from 'crypto'; import { readFileSync } from 'fs'; import { buffer } from 'micro'; +import { match } from 'ts-pattern'; import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf'; import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf'; @@ -16,6 +17,7 @@ import { ReadStatus, SendStatus, SigningStatus, + SubscriptionStatus, } from '@documenso/prisma/client'; const log = (...args: unknown[]) => console.log('[stripe]', ...args); @@ -54,6 +56,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ); 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') { // 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 @@ -195,3 +209,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) 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).plan; + + 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(), + }, + }); +}; diff --git a/packages/ee/server-only/stripe/get-customer.ts b/packages/ee/server-only/stripe/get-customer.ts new file mode 100644 index 000000000..11e782966 --- /dev/null +++ b/packages/ee/server-only/stripe/get-customer.ts @@ -0,0 +1,19 @@ +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; + } +}; diff --git a/packages/prisma/migrations/20230918111438_update_subscription_constraints_and_columns/migration.sql b/packages/prisma/migrations/20230918111438_update_subscription_constraints_and_columns/migration.sql new file mode 100644 index 000000000..8125b0cb1 --- /dev/null +++ b/packages/prisma/migrations/20230918111438_update_subscription_constraints_and_columns/migration.sql @@ -0,0 +1,17 @@ +/* + 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"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 1ff3d7a75..72d54f4ca 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -41,15 +41,16 @@ enum SubscriptionStatus { } model Subscription { - id Int @id @default(autoincrement()) - status SubscriptionStatus @default(INACTIVE) - planId String? - priceId String? - customerId String? - periodEnd DateTime? - userId Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + status SubscriptionStatus @default(INACTIVE) + planId String? + priceId String? + customerId String + periodEnd DateTime? + userId Int @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + cancelAtPeriodEnd Boolean @default(false) User User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index b0852b4f4..19447957f 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -9,6 +9,7 @@ declare namespace NodeJS { NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_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_WEBHOOK_SECRET: string; diff --git a/packages/ui/primitives/button.tsx b/packages/ui/primitives/button.tsx index c67117d6f..31df69dee 100644 --- a/packages/ui/primitives/button.tsx +++ b/packages/ui/primitives/button.tsx @@ -56,14 +56,14 @@ export interface ButtonProps } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ({ className, variant, size, asChild = false, loading, ...props }, ref) => { if (asChild) { return ( ); } - const showLoader = props.loading === true; + const showLoader = loading === true; const isDisabled = props.disabled || showLoader; return ( diff --git a/turbo.json b/turbo.json index a5b333c66..9fea252d0 100644 --- a/turbo.json +++ b/turbo.json @@ -2,8 +2,13 @@ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { - "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**"] + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] }, "lint": {}, "dev": { @@ -11,7 +16,9 @@ "persistent": true } }, - "globalDependencies": ["**/.env.*local"], + "globalDependencies": [ + "**/.env.*local" + ], "globalEnv": [ "APP_VERSION", "NEXTAUTH_URL", @@ -23,6 +30,7 @@ "NEXT_PUBLIC_FEATURE_BILLING_ENABLED", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID", + "NEXT_PUBLIC_STRIPE_FREE_PLAN_ID", "NEXT_PRIVATE_DATABASE_URL", "NEXT_PRIVATE_NEXT_AUTH_SECRET", "NEXT_PRIVATE_GOOGLE_CLIENT_ID", @@ -50,4 +58,4 @@ "NEXT_PRIVATE_SMTP_FROM_ADDRESS", "NEXT_PRIVATE_STRIPE_API_KEY" ] -} +} \ No newline at end of file From 027a588604504f4a1b4bb4ed7b26f1dc1db72c6b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 18 Sep 2023 22:47:46 +1000 Subject: [PATCH 002/133] feat: wip --- .../settings/billing/create-billing-portal.action.ts | 4 ++-- apps/web/src/pages/api/stripe/webhook/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts index f235cb846..d42d1e97e 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -40,10 +40,10 @@ export const createBillingPortal = async () => { }); } - const stripeCustomerSubsriptions = stripeCustomer.subscriptions?.data ?? []; + const stripeCustomerSubscriptions = stripeCustomer.subscriptions?.data ?? []; // Create a free subscription for user if it does not exist. - if (!existingSubscription && stripeCustomerSubsriptions.length === 0) { + if (!existingSubscription && stripeCustomerSubscriptions.length === 0) { const newSubscription = await stripe.subscriptions.create({ customer: stripeCustomer.id, items: [ diff --git a/apps/web/src/pages/api/stripe/webhook/index.ts b/apps/web/src/pages/api/stripe/webhook/index.ts index 06f63e293..9f1a6937a 100644 --- a/apps/web/src/pages/api/stripe/webhook/index.ts +++ b/apps/web/src/pages/api/stripe/webhook/index.ts @@ -212,7 +212,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const handleCustomerSubscriptionUpdated = async (subscription: Stripe.Subscription) => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const plan = (subscription as unknown as Stripe.SubscriptionItem).plan; + const { plan } = subscription as unknown as Stripe.SubscriptionItem; const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id; From cbe118b74fb4bbf42a27c57744f97f6a9c62b875 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 19 Sep 2023 15:14:47 +1000 Subject: [PATCH 003/133] fix: merge issues --- .../settings/billing/create-billing-portal.action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts index d42d1e97e..98c3d2a96 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -75,6 +75,6 @@ export const createBillingPortal = async () => { return getPortalSession({ customerId: stripeCustomer.id, - returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`, + returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, }); }; From 4d485940eaa01ac93d7fb49d508806af588182d3 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 19 Sep 2023 15:30:58 +1000 Subject: [PATCH 004/133] fix: stripe customer fetch logic --- .../settings/billing/create-billing-portal.action.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts index 98c3d2a96..331943648 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -26,8 +26,10 @@ export const createBillingPortal = async () => { } } - // Fallback to check if a Stripe customer already exists for the current user. - stripeCustomer = await getStripeCustomerByEmail(user.email); + // 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) { From 775de16d0af38918e5559deb60da5288d3560ccb Mon Sep 17 00:00:00 2001 From: pit Date: Thu, 21 Sep 2023 12:43:36 +0100 Subject: [PATCH 005/133] feat: admin ui for managing instance --- apps/web/src/app/(dashboard)/admin/nav.tsx | 8 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 8 ++ .../admin/users/data-table-users.tsx | 135 ++++++++++++++++++ .../src/app/(dashboard)/admin/users/page.tsx | 35 +++++ .../lib/server-only/user/get-all-users.ts | 37 +++++ 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/users/page.tsx create mode 100644 packages/lib/server-only/user/get-all-users.ts diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 3b87a9b13..0b59335bf 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -37,10 +37,12 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { 'justify-start md:w-full', pathname?.startsWith('/admin/users') && 'bg-secondary', )} - disabled + asChild > - - Users (Coming Soon) + + + Users + ); diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx new file mode 100644 index 000000000..3b8cfa287 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -0,0 +1,8 @@ +export default function UserPage() { + return ( +
+

Hey

+

Ho

+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx new file mode 100644 index 000000000..890d5cd48 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useTransition } from 'react'; + +import Link from 'next/link'; + +import { Edit, Loader } from 'lucide-react'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { Role } 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'; + +interface User { + id: number; + name: string | null; + email: string; + roles: Role[]; + Subscription: Subscription[]; +} + +interface Subscription { + id: number; + status: string; + planId: string | null; + priceId: string | null; + createdAt: Date | null; + periodEnd: Date | null; +} + +type UsersDataTableProps = { + users: User[]; + perPage: number; + page: number; + totalPages: number; +}; + +export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTableProps) => { + const [isPending, startTransition] = useTransition(); + const updateSearchParams = useUpdateSearchParams(); + console.log(users); + + const onPaginationChange = (page: number, perPage: number) => { + startTransition(() => { + updateSearchParams({ + page, + perPage, + }); + }); + }; + + return ( +
+
{row.original.id}
, + }, + { + header: 'Name', + accessorKey: 'name', + cell: ({ row }) =>
{row.original.name}
, + }, + { + header: 'Email', + accessorKey: 'email', + cell: ({ row }) =>
{row.original.email}
, + }, + { + header: 'Roles', + accessorKey: 'roles', + cell: ({ row }) => { + return ( + <> + {row.original.roles.map((role: string, idx: number) => { + return ( + + {role} {} + + ); + })} + + ); + }, + }, + { + header: 'Subscription status', + accessorKey: 'subscription', + cell: ({ row }) => { + return ( + <> + {row.original.Subscription.map((subscription: Subscription, idx: number) => { + return {subscription.status}; + })} + + ); + }, + }, + { + header: 'Edit', + accessorKey: 'edit', + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + }, + ]} + data={users} + perPage={perPage} + currentPage={page} + totalPages={totalPages} + onPaginationChange={onPaginationChange} + > + {(table) => } +
+ + {isPending && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx new file mode 100644 index 000000000..7f8b9af47 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/page.tsx @@ -0,0 +1,35 @@ +import { findUsers } from '@documenso/lib/server-only/user/get-all-users'; + +/* +1. retrieve all users from the db +2. display them in a table +*/ +import { UsersDataTable } from './data-table-users'; + +type AdminManageUsersProps = { + searchParams?: { + page?: number; + perPage?: number; + }; +}; + +export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { + const page = Number(searchParams.page) || 1; + const perPage = Number(searchParams.perPage) || 10; + + const results = await findUsers({ page, perPage }); + + return ( +
+

Manage users

+
+ +
+
+ ); +} diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts new file mode 100644 index 000000000..157a75d4a --- /dev/null +++ b/packages/lib/server-only/user/get-all-users.ts @@ -0,0 +1,37 @@ +import { prisma } from '@documenso/prisma'; + +type getAllUsersProps = { + page: number; + perPage: number; +}; + +export const findUsers = async ({ page = 1, perPage = 10 }: getAllUsersProps) => { + const [users, count] = await Promise.all([ + await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + roles: true, + Subscription: { + select: { + id: true, + status: true, + planId: true, + priceId: true, + createdAt: true, + periodEnd: true, + }, + }, + }, + skip: Math.max(page - 1, 0) * perPage, + take: perPage, + }), + await prisma.user.count(), + ]); + + return { + users, + totalPages: Math.ceil(count / perPage), + }; +}; From 07bf780c3e52d252bd9a70399f620b4f150df195 Mon Sep 17 00:00:00 2001 From: pit Date: Thu, 21 Sep 2023 15:10:20 +0100 Subject: [PATCH 006/133] feat: build individual user page --- .../app/(dashboard)/admin/users/[id]/page.tsx | 85 ++++++++++++++++++- packages/trpc/server/profile-router/router.ts | 27 ++++++ packages/trpc/server/profile-router/schema.ts | 5 ++ 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 3b8cfa287..ddeb52058 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -1,8 +1,87 @@ -export default function UserPage() { +'use client'; + +import { Loader } from 'lucide-react'; +import { Controller, useForm } from 'react-hook-form'; + +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { Input } from '@documenso/ui/primitives/input'; +import { Label } from '@documenso/ui/primitives/label'; +import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { FormErrorMessage } from '../../../../../components/form/form-error-message'; + +export default function UserPage({ params }: { params: { id: number } }) { + const toast = useToast(); + + const result = trpc.profile.getUser.useQuery( + { + id: Number(params.id), + }, + { + enabled: !!params.id, + }, + ); + + const { + register, + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm(); + + console.log(result.data); + + const onSubmit = async (data) => { + console.log(data); + }; + return (
-

Hey

-

Ho

+

Manage {result.data?.name}'s profile

+
+
+ + + +
+
+ + + +
+
+ + +
+ ( + onChange(v ?? '')} + /> + )} + /> + +
+
+
+ +
+
); } diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index bbeff675b..c5756c480 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,6 +1,8 @@ import { TRPCError } from '@trpc/server'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; 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 { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updateProfile } from '@documenso/lib/server-only/user/update-profile'; @@ -9,11 +11,36 @@ import { authenticatedProcedure, procedure, router } from '../trpc'; import { ZForgotPasswordFormSchema, ZResetPasswordFormSchema, + ZRetrieveUserByIdQuerySchema, ZUpdatePasswordMutationSchema, ZUpdateProfileMutationSchema, } from './schema'; export const profileRouter = router({ + getUser: authenticatedProcedure + .input(ZRetrieveUserByIdQuerySchema) + .query(async ({ input, ctx }) => { + const isUserAdmin = isAdmin(ctx.user); + + if (!isUserAdmin) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Not authorized to perform this action.', + }); + } + + 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 .input(ZUpdateProfileMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index 641227684..a910ec3cc 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -1,5 +1,9 @@ import { z } from 'zod'; +export const ZRetrieveUserByIdQuerySchema = z.object({ + id: z.number().min(1), +}); + export const ZUpdateProfileMutationSchema = z.object({ name: z.string().min(1), signature: z.string(), @@ -18,6 +22,7 @@ export const ZResetPasswordFormSchema = z.object({ token: z.string().min(1), }); +export type TRetrieveUserByIdQuerySchema = z.infer; export type TUpdateProfileMutationSchema = z.infer; export type TUpdatePasswordMutationSchema = z.infer; export type TForgotPasswordFormSchema = z.infer; From dc512600dce239aa7da0233b2a08b0852e252aec Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 27 Sep 2023 13:08:19 +0200 Subject: [PATCH 007/133] chore: and now his watch has ended --- .../marketing/content/blog/malfunction-mania.mdx | 3 +++ apps/marketing/content/blog/shop.mdx | 5 ++++- apps/marketing/src/app/(marketing)/open/data.ts | 16 ---------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/apps/marketing/content/blog/malfunction-mania.mdx b/apps/marketing/content/blog/malfunction-mania.mdx index cb21e951a..c1f5732f0 100644 --- a/apps/marketing/content/blog/malfunction-mania.mdx +++ b/apps/marketing/content/blog/malfunction-mania.mdx @@ -51,4 +51,7 @@ As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION M We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward. +Best from Hamburg +Timur + **[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.** diff --git a/apps/marketing/content/blog/shop.mdx b/apps/marketing/content/blog/shop.mdx index ef1f0422a..fafd98a40 100644 --- a/apps/marketing/content/blog/shop.mdx +++ b/apps/marketing/content/blog/shop.mdx @@ -56,6 +56,9 @@ If you have been following us, you know we are not big on formalities but highly - Engage in discussion about the current version and its choices - Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging) - Review the version with a video, stream, or screenshots and post about it -- Review existing or create missing documenso +- Review existing or create missing documentation + +Best from Hamburg +Timur **[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.** diff --git a/apps/marketing/src/app/(marketing)/open/data.ts b/apps/marketing/src/app/(marketing)/open/data.ts index 8ab565a36..96654099f 100644 --- a/apps/marketing/src/app/(marketing)/open/data.ts +++ b/apps/marketing/src/app/(marketing)/open/data.ts @@ -23,22 +23,6 @@ export const TEAM_MEMBERS = [ engagement: 'Part-Time', joinDate: 'June 6th, 2023', }, - { - name: 'Florent Merian', - role: 'Marketer - III', - salary: 'Project-Based', - location: 'France', - engagement: 'Full-Time', - joinDate: 'July 10th, 2023', - }, - { - name: 'Thilo Konzok', - role: 'Designer', - salary: 'Project-Based', - location: 'Germany', - engagement: 'Full-Time', - joinDate: 'April 26th, 2023', - }, { name: 'David Nguyen', role: 'Software Engineer - III', From f1bc772985b6feae7cf002b403e1db34aab8de1c Mon Sep 17 00:00:00 2001 From: pit Date: Fri, 29 Sep 2023 17:12:02 +0100 Subject: [PATCH 008/133] chore: improve the ui --- .../admin/users/data-table-users.tsx | 39 +++++++-- packages/lib/server-only/admin/update-user.ts | 30 +++++++ .../lib/server-only/user/get-all-users.ts | 5 ++ packages/ui/primitives/combobox.tsx | 84 +++++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 packages/lib/server-only/admin/update-user.ts create mode 100644 packages/ui/primitives/combobox.tsx diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index 890d5cd48..0329b6a17 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -18,6 +18,7 @@ interface User { email: string; roles: Role[]; Subscription: Subscription[]; + Document: Document[]; } interface Subscription { @@ -36,10 +37,13 @@ type UsersDataTableProps = { totalPages: number; }; +type Document = { + id: number; +}; + export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTableProps) => { const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); - console.log(users); const onPaginationChange = (page: number, perPage: number) => { startTransition(() => { @@ -75,9 +79,9 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa cell: ({ row }) => { return ( <> - {row.original.roles.map((role: string, idx: number) => { + {row.original.roles.map((role: string, i: number) => { return ( - + {role} {} ); @@ -87,15 +91,32 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa }, }, { - header: 'Subscription status', + header: 'Subscription', accessorKey: 'subscription', + cell: ({ row }) => { + if (row.original.Subscription && row.original.Subscription.length > 0) { + return ( + <> + {row.original.Subscription.map((subscription: Subscription, i: number) => { + return {subscription.status}; + })} + + ); + } else { + return NONE; + } + }, + }, + { + header: 'Documents', + accessorKey: 'documents', cell: ({ row }) => { return ( - <> - {row.original.Subscription.map((subscription: Subscription, idx: number) => { - return {subscription.status}; - })} - +
+ + {row.original.Document.length} + +
); }, }, diff --git a/packages/lib/server-only/admin/update-user.ts b/packages/lib/server-only/admin/update-user.ts new file mode 100644 index 000000000..b10e6477d --- /dev/null +++ b/packages/lib/server-only/admin/update-user.ts @@ -0,0 +1,30 @@ +import { prisma } from '@documenso/prisma'; +import { Role } from '@documenso/prisma/client'; + +export type UpdateUserOptions = { + userId: number; + name: string; + email: string; + roles: Role[]; +}; + +export const updateUser = async ({ userId, name, email, roles }: UpdateUserOptions) => { + console.log('wtf'); + await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + + const updatedUser = await prisma.user.update({ + where: { + id: userId, + }, + data: { + name, + email, + roles, + }, + }); + return updatedUser; +}; diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index 157a75d4a..35e165260 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -23,6 +23,11 @@ export const findUsers = async ({ page = 1, perPage = 10 }: getAllUsersProps) => periodEnd: true, }, }, + Document: { + select: { + id: true, + }, + }, }, skip: Math.max(page - 1, 0) * perPage, take: perPage, diff --git a/packages/ui/primitives/combobox.tsx b/packages/ui/primitives/combobox.tsx new file mode 100644 index 000000000..6e566e188 --- /dev/null +++ b/packages/ui/primitives/combobox.tsx @@ -0,0 +1,84 @@ +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([]); + 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 ( + <> + + + + + + + + No value found. + + {allRoles.map((value: string, i: number) => ( + handleSelect(value)}> + + {value} + + ))} + + + + + + ); +}; + +export { Combobox }; From c2cda0f06e907232d98e37f570dafd3194b09180 Mon Sep 17 00:00:00 2001 From: pit Date: Fri, 29 Sep 2023 17:26:37 +0100 Subject: [PATCH 009/133] feat: update user functionality --- .../app/(dashboard)/admin/users/[id]/page.tsx | 75 +++++++++++++++++-- packages/lib/server-only/admin/update-user.ts | 8 +- packages/trpc/server/admin-router/router.ts | 33 ++++++++ packages/trpc/server/admin-router/schema.ts | 13 ++++ packages/trpc/server/router.ts | 2 + 5 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 packages/trpc/server/admin-router/router.ts create mode 100644 packages/trpc/server/admin-router/schema.ts diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index ddeb52058..d6b0d9d25 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -1,10 +1,13 @@ 'use client'; +import { useRouter } from 'next/navigation'; + import { Loader } from 'lucide-react'; import { Controller, 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 { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; @@ -13,7 +16,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { FormErrorMessage } from '../../../../../components/form/form-error-message'; export default function UserPage({ params }: { params: { id: number } }) { - const toast = useToast(); + const { toast } = useToast(); + const router = useRouter(); const result = trpc.profile.getUser.useQuery( { @@ -24,6 +28,15 @@ export default function UserPage({ params }: { params: { id: number } }) { }, ); + const roles = result.data?.roles; + let rolesArr: string[] = []; + + if (roles) { + rolesArr = Object.values(roles); + } + + const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation(); + const { register, control, @@ -31,10 +44,44 @@ export default function UserPage({ params }: { params: { id: number } }) { formState: { errors, isSubmitting }, } = useForm(); - console.log(result.data); - const onSubmit = async (data) => { console.log(data); + + // const submittedRoles = data.roles + // .split(',') + // .map((role: string) => role.trim()) + // .map((role: string) => role.toUpperCase()); + + // const dbRoles = JSON.stringify(Role); + + // const roleExists = submittedRoles.some((role: string) => dbRoles.includes(role)); + // console.log('roleExists', roleExists); + + // console.log('db roles', dbRoles); + + try { + await updateUserMutation({ + id: Number(result.data?.id), + name: data.name, + email: data.email, + roles: data.roles, + }); + + router.refresh(); + + toast({ + title: 'Profile updated', + description: 'Your profile has been updated.', + duration: 5000, + }); + } catch (e) { + console.log(e); + toast({ + title: 'Error', + description: 'An error occurred while updating your profile.', + variant: 'destructive', + }); + } }; return ( @@ -45,17 +92,30 @@ export default function UserPage({ params }: { params: { id: number } }) { - +
- +
-
+
+ + ( + onChange(values)} /> + )} + /> + +
+ {/*
@@ -74,7 +134,8 @@ export default function UserPage({ params }: { params: { id: number } }) { />
-
+ */} +
+ )) + .with({ isRecipient: true, isPending: true, isSigned: false }, () => ( + + )) + .otherwise(() => ( + + )); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx new file mode 100644 index 000000000..72fdb4845 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx @@ -0,0 +1,124 @@ +'use client'; + +import Link from 'next/link'; + +import { + Copy, + Download, + Edit, + History, + MoreHorizontal, + Pencil, + Share, + Trash2, + XCircle, +} from 'lucide-react'; +import { useSession } from 'next-auth/react'; + +import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { trpc } from '@documenso/trpc/client'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; + +export type DataTableActionDropdownProps = { + row: Document & { + User: Pick; + Recipient: Recipient[]; + }; +}; + +export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { + const { data: session } = useSession(); + + if (!session) { + return null; + } + + const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); + + const isOwner = row.User.id === session.user.id; + // const isRecipient = !!recipient; + // const isDraft = row.status === DocumentStatus.DRAFT; + // const isPending = row.status === DocumentStatus.PENDING; + const isComplete = row.status === DocumentStatus.COMPLETED; + // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; + + const onDownloadClick = async () => { + let document: DocumentWithData | null = null; + + if (!recipient) { + document = await trpc.document.getDocumentById.query({ + id: row.id, + }); + } else { + document = await trpc.document.getDocumentByToken.query({ + token: recipient.token, + }); + } + + const documentData = document?.documentData; + + if (!documentData) { + return; + } + + const documentBytes = await getFile(documentData); + + const blob = new Blob([documentBytes], { + type: 'application/pdf', + }); + + const link = window.document.createElement('a'); + + link.href = window.URL.createObjectURL(blob); + link.download = row.title || 'document.pdf'; + + link.click(); + + window.URL.revokeObjectURL(link.href); + }; + + return ( + + + + + + + Action + + + + Download + + + + + Duplicate + + + + + Void + + + + + Delete + + + + + Resend + + + + ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx new file mode 100644 index 000000000..c04f9f13d --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx @@ -0,0 +1,56 @@ +'use client'; + +import Link from 'next/link'; + +import { useSession } from 'next-auth/react'; +import { match } from 'ts-pattern'; + +import { Document, Recipient, User } from '@documenso/prisma/client'; + +export type DataTableTitleProps = { + row: Document & { + User: Pick; + Recipient: Recipient[]; + }; +}; + +export const DataTableTitle = ({ row }: DataTableTitleProps) => { + const { data: session } = useSession(); + + if (!session) { + return null; + } + + const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); + + const isOwner = row.User.id === session.user.id; + const isRecipient = !!recipient; + + return match({ + isOwner, + isRecipient, + }) + .with({ isOwner: true }, () => ( + + {row.title} + + )) + .with({ isRecipient: true }, () => ( + + {row.title} + + )) + .otherwise(() => ( + + {row.title} + + )); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx new file mode 100644 index 000000000..b1ab92e42 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { useTransition } from 'react'; + +import Link from 'next/link'; + +import { Loader } from 'lucide-react'; +import { useSession } from 'next-auth/react'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { FindResultSet } from '@documenso/lib/types/find-result-set'; +import { Document, Recipient, User } from '@documenso/prisma/client'; +import { DataTable } from '@documenso/ui/primitives/data-table'; +import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; + +import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; +import { DocumentStatus } from '~/components/formatter/document-status'; +import { LocaleDate } from '~/components/formatter/locale-date'; + +import { DataTableActionButton } from './data-table-action-button'; +import { DataTableActionDropdown } from './data-table-action-dropdown'; +import { DataTableTitle } from './data-table-title'; + +export type DocumentsDataTableProps = { + results: FindResultSet< + Document & { + Recipient: Recipient[]; + User: Pick; + } + >; +}; + +export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { + const { data: session } = useSession(); + const [isPending, startTransition] = useTransition(); + + const updateSearchParams = useUpdateSearchParams(); + + const onPaginationChange = (page: number, perPage: number) => { + startTransition(() => { + updateSearchParams({ + page, + perPage, + }); + }); + }; + + if (!session) { + return null; + } + + return ( +
+ , + }, + { + header: 'Title', + cell: ({ row }) => , + }, + { + header: 'Owner', + accessorKey: 'owner', + cell: ({ row }) => { + return ( + + + + ); + }, + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => , + }, + { + header: 'Actions', + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]} + data={results.data} + perPage={results.perPage} + currentPage={results.currentPage} + totalPages={results.totalPages} + onPaginationChange={onPaginationChange} + > + {(table) => } +
+ + {isPending && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx new file mode 100644 index 000000000..d62d82ada --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/page.tsx @@ -0,0 +1,36 @@ +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +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 user = await getRequiredServerComponentSession(); + const page = Number(searchParams.page) || 1; + const perPage = Number(searchParams.perPage) || 20; + + const results = await findDocuments({ + userId: user.id, + orderBy: { + column: 'createdAt', + direction: 'desc', + }, + page, + perPage, + }); + + return ( +
+

Manage documents

+
+ +
+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 0b59335bf..8050f867a 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -5,7 +5,7 @@ import { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { BarChart3, User2 } from 'lucide-react'; +import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -44,6 +44,34 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { Users + + + +
); }; diff --git a/packages/lib/server-only/admin/get-all-documents.ts b/packages/lib/server-only/admin/get-all-documents.ts new file mode 100644 index 000000000..a1abdb186 --- /dev/null +++ b/packages/lib/server-only/admin/get-all-documents.ts @@ -0,0 +1,67 @@ +import { prisma } from '@documenso/prisma'; +import { Document } from '@documenso/prisma/client'; + +export interface FindDocumentsOptions { + term?: string; + page?: number; + perPage?: number; + orderBy?: { + column: keyof Omit; + direction: 'asc' | 'desc'; + }; +} + +export const findDocuments = async ({ + term, + page = 1, + perPage = 10, + orderBy, +}: FindDocumentsOptions) => { + const orderByColumn = orderBy?.column ?? 'createdAt'; + const orderByDirection = orderBy?.direction ?? 'desc'; + + const termFilters = !term + ? undefined + : ({ + title: { + contains: term, + mode: 'insensitive', + }, + } as const); + + const [data, count] = await Promise.all([ + prisma.document.findMany({ + where: { + ...termFilters, + }, + skip: Math.max(page - 1, 0) * perPage, + take: perPage, + orderBy: { + [orderByColumn]: orderByDirection, + }, + 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), + }; +}; From 87a5bab734bd32f7bbba99a96cd3fc217a7527ec Mon Sep 17 00:00:00 2001 From: Arjun Bharti <60930192+arjunbharti@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:28:11 +0530 Subject: [PATCH 013/133] fix: signature text overflow truncated for longer signature texts (#489) --- packages/ui/primitives/document-flow/add-fields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 5c985558c..ea7bac2cb 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -425,7 +425,7 @@ export const AddFieldsFormPartial = ({

From 97dfacd133ce654a8f5e58c429a20d2bf4befa95 Mon Sep 17 00:00:00 2001 From: Harsh Acharya <72140070+haaarsh619@users.noreply.github.com> Date: Tue, 3 Oct 2023 12:56:33 +0530 Subject: [PATCH 014/133] fix: Error in Pricing Page Validation for Signup Now Modal (#497) * fix: signup modal validation on close * fix: restore auto focus input --- .../src/components/(marketing)/claim-plan-dialog.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx b/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx index 903d691bb..b27722ea3 100644 --- a/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx +++ b/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; @@ -55,8 +55,8 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog register, handleSubmit, formState: { errors, isSubmitting }, + reset, } = useForm({ - mode: 'onBlur', defaultValues: { name: params?.get('name') ?? '', email: params?.get('email') ?? '', @@ -91,6 +91,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog } }; + useEffect(() => { + if (!isSubmitting && !open) { + reset(); + } + }, [open]); + return (

!isSubmitting && setOpen(value)}> {children} From 70ecc9a4a8da528cca9c6f495389f90bc93f5281 Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 3 Oct 2023 09:53:47 +0100 Subject: [PATCH 015/133] feat: add playwright --- .github/workflows/e2e-tests.yml | 51 +++ package-lock.json | 339 +++++++++++++++++- package.json | 3 +- packages/web-tests/.gitignore | 4 + packages/web-tests/e2e/test-auth-flow.spec.ts | 55 +++ packages/web-tests/package.json | 21 ++ packages/web-tests/playwright.config.ts | 77 ++++ turbo.json | 19 +- 8 files changed, 556 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 packages/web-tests/.gitignore create mode 100644 packages/web-tests/e2e/test-auth-flow.spec.ts create mode 100644 packages/web-tests/package.json create mode 100644 packages/web-tests/playwright.config.ts diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 000000000..ecd1d1d1a --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,51 @@ +name: Playwright Tests +on: + push: + branches: [feat/refresh, feat/add-e2e-testing-playwright] + pull_request: + branches: [feat/refresh, feat/add-e2e-testing-playwright] +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 + env: + NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso + NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Generate package-lock.json + run: npm cache clean --force && npm install + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Install Prisma Client + run: npm install @prisma/client + - name: Generate Prisma Client + run: npm run prisma:generate -w @documenso/prisma + - name: Create the database + run: npm run prisma:migrate-dev -w @documenso/prisma + - 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 diff --git a/package-lock.json b/package-lock.json index c14609103..404043d20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2461,6 +2461,19 @@ "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": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz", @@ -3797,6 +3810,21 @@ "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": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz", @@ -5446,6 +5474,24 @@ "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": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", @@ -7630,6 +7676,14 @@ "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": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -9135,6 +9189,11 @@ "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": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -10339,6 +10398,20 @@ "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": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -10738,6 +10811,11 @@ } } }, + "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -12176,6 +12254,18 @@ "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", "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": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -12410,6 +12500,14 @@ "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": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", @@ -12957,6 +13055,11 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", @@ -15010,6 +15113,14 @@ "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": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", @@ -15108,6 +15219,36 @@ "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": { "version": "8.4.27", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", @@ -15658,6 +15799,20 @@ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -17301,6 +17456,14 @@ "resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz", "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": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -17676,6 +17839,17 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", "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": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -17707,6 +17881,121 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "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": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -17715,6 +18004,14 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -18260,8 +18557,7 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, "node_modules/through2": { "version": "4.0.2", @@ -19522,6 +19818,24 @@ "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": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -19559,6 +19873,10 @@ "node": ">= 8" } }, + "node_modules/web-tests": { + "resolved": "packages/web-tests", + "link": true + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -20009,6 +20327,23 @@ "react": "18.2.0", "typescript": "^5.1.6" } + }, + "packages/web-tests": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "start-server-and-test": "^2.0.1" + }, + "devDependencies": { + "@playwright/test": "^1.38.1", + "@types/node": "^20.8.2" + } + }, + "packages/web-tests/node_modules/@types/node": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", + "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", + "dev": true } } } diff --git a/package.json b/package.json index bb574f3ca..ba6b2ebdf 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "d": "npm run dx && npm run dev", "dx": "npm i && npm run dx:up && npm run prisma:migrate-dev -w @documenso/prisma", "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 lint" }, "engines": { "npm": ">=8.6.0", diff --git a/packages/web-tests/.gitignore b/packages/web-tests/.gitignore new file mode 100644 index 000000000..75e854d8d --- /dev/null +++ b/packages/web-tests/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/packages/web-tests/e2e/test-auth-flow.spec.ts b/packages/web-tests/e2e/test-auth-flow.spec.ts new file mode 100644 index 000000000..544bdf48f --- /dev/null +++ b/packages/web-tests/e2e/test-auth-flow.spec.ts @@ -0,0 +1,55 @@ +import { type Page, expect, test } from '@playwright/test'; + +import { deleteUserAndItsData } from '@documenso/lib/server-only/user/delete-user-and-data'; + +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 = 'Test user'; +const email = 'testuser@gmail.com'; +const password = '12345678910'; + +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 deleteUserAndItsData(username); + } catch (e) { + throw new Error(`Error deleting user: ${e}`); + } +}); diff --git a/packages/web-tests/package.json b/packages/web-tests/package.json new file mode 100644 index 000000000..fdf2f3268 --- /dev/null +++ b/packages/web-tests/package.json @@ -0,0 +1,21 @@ +{ + "name": "web-tests", + "version": "1.0.0", + "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": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.18.1", + "@types/node": "^20.8.2", + "@documenso/web": "*" + }, + "dependencies": { + "start-server-and-test": "^2.0.1" + } +} diff --git a/packages/web-tests/playwright.config.ts b/packages/web-tests/playwright.config.ts new file mode 100644 index 000000000..463b6f97d --- /dev/null +++ b/packages/web-tests/playwright.config.ts @@ -0,0 +1,77 @@ +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, + // }, +}); diff --git a/turbo.json b/turbo.json index 0fc3d8f9c..368c3a880 100644 --- a/turbo.json +++ b/turbo.json @@ -2,13 +2,8 @@ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { - "dependsOn": [ - "^build" - ], - "outputs": [ - ".next/**", - "!.next/cache/**" - ] + "dependsOn": ["^build"], + "outputs": [".next/**", "!.next/cache/**"] }, "lint": {}, "clean": { @@ -17,11 +12,15 @@ "dev": { "cache": false, "persistent": true + }, + "dev:test": { + "cache": false + }, + "test:e2e": { + "dependsOn": ["^build"] } }, - "globalDependencies": [ - "**/.env.*local" - ], + "globalDependencies": ["**/.env.*local"], "globalEnv": [ "APP_VERSION", "NEXTAUTH_URL", From 2e800d0eed08780e4e1eb7c455662e5dc93d09d9 Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 3 Oct 2023 10:01:44 +0100 Subject: [PATCH 016/133] chore: removed lint step --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba6b2ebdf..d072aa76d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dx": "npm i && npm run dx:up && npm run prisma:migrate-dev -w @documenso/prisma", "dx:up": "docker compose -f docker/compose-services.yml up -d", "dx:down": "docker compose -f docker/compose-services.yml down", - "ci": "turbo run build test:e2e lint" + "ci": "turbo run build test:e2e" }, "engines": { "npm": ">=8.6.0", From 8848df701c6a7a30750607eed749182fa6b609fd Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 3 Oct 2023 10:09:40 +0100 Subject: [PATCH 017/133] chore: added delete function --- .../server-only/user/delete-user-and-data.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 packages/lib/server-only/user/delete-user-and-data.ts diff --git a/packages/lib/server-only/user/delete-user-and-data.ts b/packages/lib/server-only/user/delete-user-and-data.ts new file mode 100644 index 000000000..3cfac13f6 --- /dev/null +++ b/packages/lib/server-only/user/delete-user-and-data.ts @@ -0,0 +1,41 @@ +import { prisma } from '@documenso/prisma'; + +export const deleteUserAndItsData = async (name: string) => { + const user = await prisma.user.findFirst({ + where: { + name: { + contains: name, + }, + }, + }); + + if (!user) { + throw new Error(`User with name ${name} not found`); + } + + const document = await prisma.document.findMany({ + where: { + userId: user.id, + }, + select: { + documentData: { + select: { + data: true, + }, + }, + }, + }); + + return prisma.$transaction([ + prisma.user.delete({ + where: { + id: user.id, + }, + }), + prisma.documentData.deleteMany({ + where: { + data: document[0]?.documentData.data, + }, + }), + ]); +}; From 7bc1e9dcc845bbb02d42bac2eefdcde58115594f Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 3 Oct 2023 10:19:54 +0100 Subject: [PATCH 018/133] chore: add env step in gh action --- .github/workflows/e2e-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ecd1d1d1a..b959c1305 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -3,7 +3,7 @@ on: push: branches: [feat/refresh, feat/add-e2e-testing-playwright] pull_request: - branches: [feat/refresh, feat/add-e2e-testing-playwright] + branches: [feat/refresh] jobs: e2e_tests: timeout-minutes: 60 @@ -33,6 +33,8 @@ jobs: run: npm cache clean --force && npm install - 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: Install Prisma Client From 2efaabd2c32a4fbaf22f9028bbf39dd650c802d9 Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 3 Oct 2023 10:20:39 +0100 Subject: [PATCH 019/133] ci: trigger ci --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b959c1305..7a2388eb1 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -3,7 +3,7 @@ on: push: branches: [feat/refresh, feat/add-e2e-testing-playwright] pull_request: - branches: [feat/refresh] + branches: [feat/refresh, feat/add-e2e-testing-playwright] jobs: e2e_tests: timeout-minutes: 60 From d10713b47718aa00351766fdd5a73ad65cbb4f25 Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 3 Oct 2023 10:21:48 +0100 Subject: [PATCH 020/133] ci: trigger ci --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7a2388eb1..77b5a8da3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -3,7 +3,7 @@ on: push: branches: [feat/refresh, feat/add-e2e-testing-playwright] pull_request: - branches: [feat/refresh, feat/add-e2e-testing-playwright] + branches: [feat/refresh] jobs: e2e_tests: timeout-minutes: 60 @@ -33,7 +33,7 @@ jobs: run: npm cache clean --force && npm install - name: Install dependencies run: npm ci - - name: Copy env + - name: Copy env run: cp .env.example .env - name: Install Playwright Browsers run: npx playwright install --with-deps From 4d4b011146d9b0feb749b9e5f701022643777e55 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Tue, 3 Oct 2023 14:04:29 +0200 Subject: [PATCH 021/133] chore: add new team members --- apps/marketing/src/app/(marketing)/open/data.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/marketing/src/app/(marketing)/open/data.ts b/apps/marketing/src/app/(marketing)/open/data.ts index 96654099f..3b109ea74 100644 --- a/apps/marketing/src/app/(marketing)/open/data.ts +++ b/apps/marketing/src/app/(marketing)/open/data.ts @@ -31,6 +31,22 @@ export const TEAM_MEMBERS = [ engagement: 'Full-Time', joinDate: 'July 26th, 2023', }, + { + name: 'Catalin-Marinel Pit', + role: 'Software Engineer - II', + salary: 80_000, + location: 'Romania', + engagement: 'Full-Time', + joinDate: 'September 4th, 2023', + }, + { + name: 'Gowdhama Rajan B', + role: 'Designer - III', + salary: 100_000, + location: 'India', + engagement: 'Full-Time', + joinDate: 'October 9th, 2023', + }, ]; export const FUNDING_RAISED = [ From f637381198f014adb5d0fed2d47105924b1d6a0c Mon Sep 17 00:00:00 2001 From: "Aditya @ArchLinux" <132184385+adityadeshlahre@users.noreply.github.com> Date: Wed, 4 Oct 2023 01:39:16 +0530 Subject: [PATCH 022/133] style(ui/ux): added margin to dialogprimitive.content & dialogprimitive.close (m-4) --- packages/ui/primitives/dialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/primitives/dialog.tsx b/packages/ui/primitives/dialog.tsx index 8a0d8b21e..90e856466 100644 --- a/packages/ui/primitives/dialog.tsx +++ b/packages/ui/primitives/dialog.tsx @@ -56,13 +56,13 @@ const DialogContent = React.forwardRef< {children} - + Close From 381a248543a7234fc39d2fd0ab34ea606b9ebdfd Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 4 Oct 2023 07:57:36 +0530 Subject: [PATCH 023/133] fix: update icons (#468) * fix: update icons --- apps/marketing/package.json | 2 +- apps/marketing/src/components/(marketing)/callout.tsx | 4 ++-- apps/marketing/src/components/(marketing)/footer.tsx | 11 +++++++---- apps/marketing/src/components/(marketing)/hero.tsx | 4 ++-- .../src/components/(marketing)/mobile-navigation.tsx | 10 ++++++---- apps/web/package.json | 2 +- .../(signing)/sign/[token]/complete/share-button.tsx | 5 +++-- .../(dashboard)/layout/profile-dropdown.tsx | 4 ++-- package-lock.json | 10 +++++----- 9 files changed, 29 insertions(+), 23 deletions(-) diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 8ee8d3808..11d72ee9f 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -31,7 +31,7 @@ "react-confetti": "^6.1.0", "react-dom": "18.2.0", "react-hook-form": "^7.43.9", - "react-icons": "^4.8.0", + "react-icons": "^4.11.0", "recharts": "^2.7.2", "sharp": "0.32.5", "typescript": "5.1.6", diff --git a/apps/marketing/src/components/(marketing)/callout.tsx b/apps/marketing/src/components/(marketing)/callout.tsx index 2427337c7..72ae3907b 100644 --- a/apps/marketing/src/components/(marketing)/callout.tsx +++ b/apps/marketing/src/components/(marketing)/callout.tsx @@ -2,8 +2,8 @@ import Link from 'next/link'; -import { Github } from 'lucide-react'; import { usePlausible } from 'next-plausible'; +import { LuGithub } from 'react-icons/lu'; import { Button } from '@documenso/ui/primitives/button'; @@ -52,7 +52,7 @@ export const Callout = ({ starCount }: CalloutProps) => { onClick={() => event('view-github')} > diff --git a/apps/marketing/src/components/(marketing)/mobile-navigation.tsx b/apps/marketing/src/components/(marketing)/mobile-navigation.tsx index 3379d9789..c9bd07631 100644 --- a/apps/marketing/src/components/(marketing)/mobile-navigation.tsx +++ b/apps/marketing/src/components/(marketing)/mobile-navigation.tsx @@ -4,7 +4,9 @@ import Image from 'next/image'; import Link from 'next/link'; import { motion, useReducedMotion } from 'framer-motion'; -import { Github, MessagesSquare, Twitter } from 'lucide-react'; +import { FaXTwitter } from 'react-icons/fa6'; +import { LiaDiscord } from 'react-icons/lia'; +import { LuGithub } from 'react-icons/lu'; import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet'; @@ -111,7 +113,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat target="_blank" className="text-foreground hover:text-foreground/80" > - + - + - + diff --git a/apps/web/package.json b/apps/web/package.json index 347a39d35..8ed19aaf4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,7 +36,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.9", - "react-icons": "^4.8.0", + "react-icons": "^4.11.0", "react-rnd": "^10.4.1", "sharp": "0.32.5", "ts-pattern": "^5.0.5", diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx index caa27cc50..c76d3d7c5 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx @@ -2,7 +2,8 @@ import { HTMLAttributes, useState } from 'react'; -import { Copy, Share, Twitter } from 'lucide-react'; +import { Copy, Share } from 'lucide-react'; +import { FaXTwitter } from 'react-icons/fa6'; import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent'; import { trpc } from '@documenso/trpc/react'; @@ -125,7 +126,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => { diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index f43e3507a..d699dea4b 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -4,7 +4,6 @@ import Link from 'next/link'; import { CreditCard, - Github, Key, LogOut, User as LucideUser, @@ -16,6 +15,7 @@ import { } from 'lucide-react'; import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; +import { LuGithub } from 'react-icons/lu'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; @@ -130,7 +130,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - + Star on Github diff --git a/package-lock.json b/package-lock.json index c14609103..46ff2f745 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "react-confetti": "^6.1.0", "react-dom": "18.2.0", "react-hook-form": "^7.43.9", - "react-icons": "^4.8.0", + "react-icons": "^4.11.0", "recharts": "^2.7.2", "sharp": "0.32.5", "typescript": "5.1.6", @@ -95,7 +95,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.9", - "react-icons": "^4.8.0", + "react-icons": "^4.11.0", "react-rnd": "^10.4.1", "sharp": "0.32.5", "ts-pattern": "^5.0.5", @@ -16359,9 +16359,9 @@ } }, "node_modules/react-icons": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", - "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz", + "integrity": "sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==", "peerDependencies": { "react": "*" } From 693249916d3ce812436b4e5579fb1575617efc24 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:53:57 +0530 Subject: [PATCH 024/133] feat: require old password for password reset (#488) * feat: require old password for password reset --- apps/web/src/components/forms/password.tsx | 39 ++++++++++++++++++- .../lib/server-only/user/update-password.ts | 33 ++++++++++------ packages/trpc/server/profile-router/router.ts | 3 +- packages/trpc/server/profile-router/schema.ts | 1 + 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index 8b6a58a06..5df5005f1 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -20,6 +20,7 @@ import { FormErrorMessage } from '../form/form-error-message'; export const ZPasswordFormSchema = z .object({ + currentPassword: z.string().min(6).max(72), password: z.string().min(6).max(72), repeatedPassword: z.string().min(6).max(72), }) @@ -40,6 +41,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [showCurrentPassword, setShowCurrentPassword] = useState(false); const { register, @@ -48,6 +50,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { formState: { errors, isSubmitting }, } = useForm({ values: { + currentPassword: '', password: '', repeatedPassword: '', }, @@ -56,9 +59,10 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation(); - const onFormSubmit = async ({ password }: TPasswordFormSchema) => { + const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => { try { await updatePassword({ + currentPassword, password, }); @@ -92,6 +96,39 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { className={cn('flex w-full flex-col gap-y-4', className)} onSubmit={handleSubmit(onFormSubmit)} > +
+ + +
+ + + +
+ + +