From 171a5ba4ee5a167d5fb225b29d12908c808003c9 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 09:16:31 +0300 Subject: [PATCH 01/11] feat: creating the admin ui for metrics --- .../(dashboard)/layout/profile-dropdown.tsx | 13 ++++++++++++- packages/lib/index.ts | 6 +++++- .../20230907075057_user_roles/migration.sql | 5 +++++ packages/prisma/schema.prisma | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 packages/prisma/migrations/20230907075057_user_roles/migration.sql diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 02af86d70..19a15564b 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -11,10 +11,12 @@ import { Monitor, Moon, Sun, + UserCog, } from 'lucide-react'; import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; +import { isAdmin } from '@documenso/lib/'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -35,8 +37,8 @@ export type ProfileDropdownProps = { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const { theme, setTheme } = useTheme(); - const { getFlag } = useFeatureFlags(); + const userIsAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -67,6 +69,15 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { + {userIsAdmin && ( + + + + Admin + + + )} + diff --git a/packages/lib/index.ts b/packages/lib/index.ts index cb0ff5c3b..2801305dd 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -1 +1,5 @@ -export {}; +import { Role, User } from '@documenso/prisma/client'; + +const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); + +export { isAdmin }; diff --git a/packages/prisma/migrations/20230907075057_user_roles/migration.sql b/packages/prisma/migrations/20230907075057_user_roles/migration.sql new file mode 100644 index 000000000..f47e48361 --- /dev/null +++ b/packages/prisma/migrations/20230907075057_user_roles/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "roles" "Role"[] DEFAULT ARRAY['USER']::"Role"[]; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 2e016f5ec..22955310b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -13,6 +13,11 @@ enum IdentityProvider { GOOGLE } +enum Role { + ADMIN + USER +} + model User { id Int @id @default(autoincrement()) name String? @@ -21,6 +26,7 @@ model User { password String? source String? signature String? + roles Role[] @default([USER]) identityProvider IdentityProvider @default(DOCUMENSO) accounts Account[] sessions Session[] From 67571158e88912620e2aa33c537a91ea7da6443f Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 11:28:50 +0300 Subject: [PATCH 02/11] feat: add the admin page --- apps/web/src/app/(dashboard)/admin/layout.tsx | 23 ++++ apps/web/src/app/(dashboard)/admin/page.tsx | 107 ++++++++++++++++++ .../(dashboard)/layout/profile-dropdown.tsx | 4 +- .../lib/server-only/admin/get-documents.ts | 5 + .../lib/server-only/admin/get-recipients.ts | 20 ++++ packages/lib/server-only/admin/get-users.ts | 18 +++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/layout.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/page.tsx create mode 100644 packages/lib/server-only/admin/get-documents.ts create mode 100644 packages/lib/server-only/admin/get-recipients.ts create mode 100644 packages/lib/server-only/admin/get-users.ts diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx new file mode 100644 index 000000000..340605bc7 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -0,0 +1,23 @@ +import { redirect } from 'next/navigation'; + +import { isAdmin } from '@documenso/lib/'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; + +export type AdminLayoutProps = { + children: React.ReactNode; +}; + +export default async function AdminLayout({ children }: AdminLayoutProps) { + const user = await getRequiredServerComponentSession(); + const isUserAdmin = isAdmin(user); + + if (!user) { + redirect('/signin'); + } + + if (!isUserAdmin) { + redirect('/dashboard'); + } + + return
{children}
; +} diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx new file mode 100644 index 000000000..e4a62f725 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -0,0 +1,107 @@ +import { + Archive, + File, + FileX2, + LucideIcon, + User as LucideUser, + Mail, + MailOpen, + PenTool, + Send, + UserPlus2, + UserSquare2, +} from 'lucide-react'; + +import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients'; +import { + getUsersCount, + getUsersWithSubscriptionsCount, +} from '@documenso/lib/server-only/admin/get-users'; +import { + ReadStatus as InternalReadStatus, + SendStatus as InternalSendStatus, + SigningStatus as InternalSigningStatus, +} from '@documenso/prisma/client'; + +import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; + +type TCardData = { + icon: LucideIcon; + title: string; + status: + | 'TOTAL_RECIPIENTS' + | 'OPENED' + | 'NOT_OPENED' + | 'SIGNED' + | 'NOT_SIGNED' + | 'SENT' + | 'NOT_SENT'; +}[]; + +const CARD_DATA: TCardData = [ + { + icon: UserSquare2, + title: 'Total recipients in the database', + status: 'TOTAL_RECIPIENTS', + }, + { + icon: MailOpen, + title: 'Total recipients with opened count', + status: InternalReadStatus.OPENED, + }, + { + icon: Mail, + title: 'Total recipients with unopened count', + status: InternalReadStatus.NOT_OPENED, + }, + { + icon: Send, + title: 'Total recipients with sent count', + status: InternalSendStatus.SENT, + }, + { + icon: Archive, + title: 'Total recipients with unsent count', + status: InternalSendStatus.NOT_SENT, + }, + { + icon: PenTool, + title: 'Total recipients with signed count', + status: InternalSigningStatus.SIGNED, + }, + { + icon: FileX2, + title: 'Total recipients with unsigned count', + status: InternalSigningStatus.NOT_SIGNED, + }, +]; + +export default async function Admin() { + const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ + getUsersCount(), + getUsersWithSubscriptionsCount(), + getDocsCount(), + getRecipientsStats(), + ]); + + return ( +
+

Documenso instance metrics

+
+ + + + {CARD_DATA.map((card) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 19a15564b..0bea64565 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -38,7 +38,7 @@ export type ProfileDropdownProps = { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const { theme, setTheme } = useTheme(); const { getFlag } = useFeatureFlags(); - const userIsAdmin = isAdmin(user); + const isUserAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -69,7 +69,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
- {userIsAdmin && ( + {isUserAdmin && ( diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents.ts new file mode 100644 index 000000000..9100a886c --- /dev/null +++ b/packages/lib/server-only/admin/get-documents.ts @@ -0,0 +1,5 @@ +import { prisma } from '@documenso/prisma'; + +export const getDocsCount = async () => { + return await prisma.document.count(); +}; diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts new file mode 100644 index 000000000..0be612e55 --- /dev/null +++ b/packages/lib/server-only/admin/get-recipients.ts @@ -0,0 +1,20 @@ +import { prisma } from '@documenso/prisma'; +import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client'; + +export const getRecipientsStats = async () => { + const results = await prisma.recipient.groupBy({ + by: ['readStatus', 'signingStatus', 'sendStatus'], + _count: true, + }); + + return { + TOTAL_RECIPIENTS: results.length, + [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0, + [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0, + [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0, + [SigningStatus.NOT_SIGNED]: + results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0, + [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0, + [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0, + }; +}; diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users.ts new file mode 100644 index 000000000..09892171a --- /dev/null +++ b/packages/lib/server-only/admin/get-users.ts @@ -0,0 +1,18 @@ +import { prisma } from '@documenso/prisma'; +import { SubscriptionStatus } from '@documenso/prisma/client'; + +export const getUsersCount = async () => { + return await prisma.user.count(); +}; + +export const getUsersWithSubscriptionsCount = async () => { + return await prisma.user.count({ + where: { + Subscription: { + some: { + status: SubscriptionStatus.ACTIVE, + }, + }, + }, + }); +}; From 6cdba45396299cc1e06a7d185280acaddda0fb59 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:39:13 +0300 Subject: [PATCH 03/11] chore: implemented feedback --- apps/web/src/app/(dashboard)/admin/page.tsx | 4 +-- .../lib/server-only/admin/get-recipients.ts | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index e4a62f725..e72d35dc3 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -37,9 +37,9 @@ type TCardData = { | 'NOT_SIGNED' | 'SENT' | 'NOT_SENT'; -}[]; +}; -const CARD_DATA: TCardData = [ +const CARD_DATA: TCardData[] = [ { icon: UserSquare2, title: 'Total recipients in the database', diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts index 0be612e55..92c0c3527 100644 --- a/packages/lib/server-only/admin/get-recipients.ts +++ b/packages/lib/server-only/admin/get-recipients.ts @@ -7,14 +7,21 @@ export const getRecipientsStats = async () => { _count: true, }); - return { - TOTAL_RECIPIENTS: results.length, - [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0, - [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0, - [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0, - [SigningStatus.NOT_SIGNED]: - results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0, - [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0, - [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0, + const stats = { + TOTAL_RECIPIENTS: 0, + [ReadStatus.OPENED]: 0, + [ReadStatus.NOT_OPENED]: 0, + [SigningStatus.SIGNED]: 0, + [SigningStatus.NOT_SIGNED]: 0, + [SendStatus.SENT]: 0, + [SendStatus.NOT_SENT]: 0, }; + results.forEach((result) => { + const { readStatus, signingStatus, sendStatus, _count } = result; + stats[readStatus] += _count; + stats[signingStatus] += _count; + stats[sendStatus] += _count; + stats.TOTAL_RECIPIENTS += _count; + }); + return stats; }; From 77058220a8975f01d4b9fda48c29bc6089a3bef0 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:42:14 +0300 Subject: [PATCH 04/11] chore: rename files --- apps/web/src/app/(dashboard)/admin/page.tsx | 6 +++--- .../admin/{get-documents.ts => get-documents-stats.ts} | 0 .../admin/{get-recipients.ts => get-recipients-stats.ts} | 0 .../server-only/admin/{get-users.ts => get-users-stats.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/lib/server-only/admin/{get-documents.ts => get-documents-stats.ts} (100%) rename packages/lib/server-only/admin/{get-recipients.ts => get-recipients-stats.ts} (100%) rename packages/lib/server-only/admin/{get-users.ts => get-users-stats.ts} (100%) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index e72d35dc3..aabdbfa35 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -12,12 +12,12 @@ import { UserSquare2, } from 'lucide-react'; -import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents'; -import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients'; +import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents-stats'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { getUsersCount, getUsersWithSubscriptionsCount, -} from '@documenso/lib/server-only/admin/get-users'; +} from '@documenso/lib/server-only/admin/get-users-stats'; import { ReadStatus as InternalReadStatus, SendStatus as InternalSendStatus, diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-documents.ts rename to packages/lib/server-only/admin/get-documents-stats.ts diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-recipients.ts rename to packages/lib/server-only/admin/get-recipients-stats.ts diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-users.ts rename to packages/lib/server-only/admin/get-users-stats.ts From 660f5894a6f66baa5fd9394efec21306e640dc7b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:56:44 +0300 Subject: [PATCH 05/11] chore: feedback improvements --- apps/web/src/app/(dashboard)/admin/layout.tsx | 2 +- .../src/components/(dashboard)/layout/profile-dropdown.tsx | 2 +- packages/lib/index.ts | 6 +----- packages/lib/next-auth/guards/is-admin.ts | 5 +++++ 4 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 packages/lib/next-auth/guards/is-admin.ts diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index 340605bc7..a221d92ba 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; -import { isAdmin } from '@documenso/lib/'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; export type AdminLayoutProps = { children: React.ReactNode; diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 0bea64565..e3fd4c6d6 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -16,7 +16,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; -import { isAdmin } from '@documenso/lib/'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 2801305dd..cb0ff5c3b 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -1,5 +1 @@ -import { Role, User } from '@documenso/prisma/client'; - -const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); - -export { isAdmin }; +export {}; diff --git a/packages/lib/next-auth/guards/is-admin.ts b/packages/lib/next-auth/guards/is-admin.ts new file mode 100644 index 000000000..2801305dd --- /dev/null +++ b/packages/lib/next-auth/guards/is-admin.ts @@ -0,0 +1,5 @@ +import { Role, User } from '@documenso/prisma/client'; + +const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); + +export { isAdmin }; From 5969f148c861bbc1d3e05a68cf359542bdff481c Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 14:51:55 +0300 Subject: [PATCH 06/11] chore: changed the cards titles --- apps/web/src/app/(dashboard)/admin/page.tsx | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index aabdbfa35..fdb54dc07 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -42,37 +42,37 @@ type TCardData = { const CARD_DATA: TCardData[] = [ { icon: UserSquare2, - title: 'Total recipients in the database', + title: 'Recipients in the database', status: 'TOTAL_RECIPIENTS', }, { icon: MailOpen, - title: 'Total recipients with opened count', + title: 'Opened documents', status: InternalReadStatus.OPENED, }, { icon: Mail, - title: 'Total recipients with unopened count', + title: 'Unopened documents', status: InternalReadStatus.NOT_OPENED, }, { icon: Send, - title: 'Total recipients with sent count', + title: 'Sent documents', status: InternalSendStatus.SENT, }, { icon: Archive, - title: 'Total recipients with unsent count', + title: 'Unsent documents', status: InternalSendStatus.NOT_SENT, }, { icon: PenTool, - title: 'Total recipients with signed count', + title: 'Signed documents', status: InternalSigningStatus.SIGNED, }, { icon: FileX2, - title: 'Total recipients with unsigned count', + title: 'Unsigned documents', status: InternalSigningStatus.NOT_SIGNED, }, ]; @@ -87,15 +87,22 @@ export default async function Admin() { return (
-

Documenso instance metrics

-
+

Instance metrics

+
+
+

Document metrics

+
+
+ +

Recipients metrics

+
{CARD_DATA.map((card) => (
From 326743d8a1f3a2f363f5df4ebe30947cea2a476b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 11 Sep 2023 10:59:50 +0300 Subject: [PATCH 07/11] chore: added app version --- apps/web/next.config.js | 4 +++- apps/web/src/app/(dashboard)/admin/page.tsx | 2 +- turbo.json | 16 +++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 09760f806..1e98b98fc 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -18,7 +18,9 @@ const config = { '@documenso/ui', '@documenso/email', ], - env, + env: { + APP_VERSION: process.env.npm_package_version, + }, modularizeImports: { 'lucide-react': { transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index fdb54dc07..78358c95a 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -87,7 +87,7 @@ export default async function Admin() { return (
-

Instance metrics

+

Instance version: {process.env.APP_VERSION}

Date: Mon, 11 Sep 2023 11:34:10 +0300 Subject: [PATCH 08/11] chore: fix version in nextjs config --- apps/web/next.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 1e98b98fc..fa6c0d1ac 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); +const { version } = require('./package.json'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), @@ -19,7 +20,8 @@ const config = { '@documenso/email', ], env: { - APP_VERSION: process.env.npm_package_version, + ...env, + APP_VERSION: version, }, modularizeImports: { 'lucide-react': { From 00574325b90e11d64f552cbd6b809acc98897ea2 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 11 Sep 2023 13:43:17 +0300 Subject: [PATCH 09/11] chore: implemented feedback --- apps/web/src/app/(dashboard)/admin/page.tsx | 8 ++++---- turbo.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 78358c95a..073056478 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -3,11 +3,11 @@ import { File, FileX2, LucideIcon, - User as LucideUser, Mail, MailOpen, PenTool, Send, + User as UserIcon, UserPlus2, UserSquare2, } from 'lucide-react'; @@ -26,7 +26,7 @@ import { import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; -type TCardData = { +type CardData = { icon: LucideIcon; title: string; status: @@ -39,7 +39,7 @@ type TCardData = { | 'NOT_SENT'; }; -const CARD_DATA: TCardData[] = [ +const CARD_DATA: CardData[] = [ { icon: UserSquare2, title: 'Recipients in the database', @@ -89,7 +89,7 @@ export default async function Admin() {

Instance version: {process.env.APP_VERSION}

- + Date: Tue, 12 Sep 2023 07:25:44 +0000 Subject: [PATCH 10/11] fix: update layout and wording --- apps/web/src/app/(dashboard)/admin/layout.tsx | 28 ++--- apps/web/src/app/(dashboard)/admin/nav.tsx | 47 +++++++ apps/web/src/app/(dashboard)/admin/page.tsx | 115 +----------------- .../src/app/(dashboard)/admin/stats/page.tsx | 75 ++++++++++++ .../(dashboard)/layout/profile-dropdown.tsx | 22 ++-- .../(dashboard)/metric-card/metric-card.tsx | 6 +- .../server-only/admin/get-documents-stats.ts | 25 +++- 7 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/nav.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/stats/page.tsx diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a221d92ba..a04c7b693 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,23 +1,19 @@ -import { redirect } from 'next/navigation'; +import React from 'react'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; -import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; +import { AdminNav } from './nav'; -export type AdminLayoutProps = { +export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default async function AdminLayout({ children }: AdminLayoutProps) { - const user = await getRequiredServerComponentSession(); - const isUserAdmin = isAdmin(user); +export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + return ( +
+
+ - if (!user) { - redirect('/signin'); - } - - if (!isUserAdmin) { - redirect('/dashboard'); - } - - return
{children}
; +
{children}
+
+
+ ); } diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx new file mode 100644 index 000000000..3b87a9b13 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { HTMLAttributes } from 'react'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { BarChart3, User2 } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +export type AdminNavProps = HTMLAttributes; + +export const AdminNav = ({ className, ...props }: AdminNavProps) => { + const pathname = usePathname(); + + return ( +
+ + + +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 073056478..5fe030685 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -1,114 +1,5 @@ -import { - Archive, - File, - FileX2, - LucideIcon, - Mail, - MailOpen, - PenTool, - Send, - User as UserIcon, - UserPlus2, - UserSquare2, -} from 'lucide-react'; +import { redirect } from 'next/navigation'; -import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents-stats'; -import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; -import { - getUsersCount, - getUsersWithSubscriptionsCount, -} from '@documenso/lib/server-only/admin/get-users-stats'; -import { - ReadStatus as InternalReadStatus, - SendStatus as InternalSendStatus, - SigningStatus as InternalSigningStatus, -} from '@documenso/prisma/client'; - -import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; - -type CardData = { - icon: LucideIcon; - title: string; - status: - | 'TOTAL_RECIPIENTS' - | 'OPENED' - | 'NOT_OPENED' - | 'SIGNED' - | 'NOT_SIGNED' - | 'SENT' - | 'NOT_SENT'; -}; - -const CARD_DATA: CardData[] = [ - { - icon: UserSquare2, - title: 'Recipients in the database', - status: 'TOTAL_RECIPIENTS', - }, - { - icon: MailOpen, - title: 'Opened documents', - status: InternalReadStatus.OPENED, - }, - { - icon: Mail, - title: 'Unopened documents', - status: InternalReadStatus.NOT_OPENED, - }, - { - icon: Send, - title: 'Sent documents', - status: InternalSendStatus.SENT, - }, - { - icon: Archive, - title: 'Unsent documents', - status: InternalSendStatus.NOT_SENT, - }, - { - icon: PenTool, - title: 'Signed documents', - status: InternalSigningStatus.SIGNED, - }, - { - icon: FileX2, - title: 'Unsigned documents', - status: InternalSigningStatus.NOT_SIGNED, - }, -]; - -export default async function Admin() { - const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ - getUsersCount(), - getUsersWithSubscriptionsCount(), - getDocsCount(), - getRecipientsStats(), - ]); - - return ( -
-

Instance version: {process.env.APP_VERSION}

-
- - -
-

Document metrics

-
- -
- -

Recipients metrics

-
- {CARD_DATA.map((card) => ( -
- -
- ))} -
-
- ); +export default function Admin() { + redirect('/admin/stats'); } diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx new file mode 100644 index 000000000..b93af5a03 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -0,0 +1,75 @@ +import { + File, + FileCheck, + FileClock, + FileEdit, + Mail, + MailOpen, + PenTool, + User as UserIcon, + UserPlus2, + UserSquare2, +} from 'lucide-react'; + +import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; +import { + getUsersCount, + getUsersWithSubscriptionsCount, +} from '@documenso/lib/server-only/admin/get-users-stats'; + +import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; + +export default async function AdminStatsPage() { + const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([ + getUsersCount(), + getUsersWithSubscriptionsCount(), + getDocumentStats(), + getRecipientsStats(), + ]); + + return ( +
+

Instance Stats

+ +
+ + + + +
+ +
+
+

Document metrics

+ +
+ + + + +
+
+ +
+

Recipients metrics

+ +
+ + + + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e3fd4c6d6..3f7a02e60 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -62,6 +62,19 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { Account + {isUserAdmin && ( + <> + + + + Admin + + + + + + )} + @@ -69,15 +82,6 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - {isUserAdmin && ( - - - - Admin - - - )} - diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx index f59d42096..a2248ccdc 100644 --- a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx +++ b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx @@ -18,10 +18,10 @@ export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricPr )} >
-
- {Icon && } +
+ {Icon && } -

{title}

+

{title}

diff --git a/packages/lib/server-only/admin/get-documents-stats.ts b/packages/lib/server-only/admin/get-documents-stats.ts index 9100a886c..e0d53373f 100644 --- a/packages/lib/server-only/admin/get-documents-stats.ts +++ b/packages/lib/server-only/admin/get-documents-stats.ts @@ -1,5 +1,26 @@ import { prisma } from '@documenso/prisma'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; -export const getDocsCount = async () => { - return await prisma.document.count(); +export const getDocumentStats = async () => { + const counts = await prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + }); + + const stats: Record, number> = { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.ALL]: 0, + }; + + counts.forEach((stat) => { + stats[stat.status] = stat._count._all; + + stats.ALL += stat._count._all; + }); + + return stats; }; From 599e857a1e282141fba6add03b5cf7c6e3c110d0 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 12 Sep 2023 17:53:38 +1000 Subject: [PATCH 11/11] fix: add removed layout guard --- apps/web/src/app/(dashboard)/admin/layout.tsx | 13 ++++++++++++- .../lib/server-only/admin/get-recipients-stats.ts | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a04c7b693..3aa47d1a9 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,12 +1,23 @@ import React from 'react'; +import { redirect } from 'next/navigation'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; + import { AdminNav } from './nav'; export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { +export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + const user = await getRequiredServerComponentSession(); + + if (!isAdmin(user)) { + redirect('/documents'); + } + return (

diff --git a/packages/lib/server-only/admin/get-recipients-stats.ts b/packages/lib/server-only/admin/get-recipients-stats.ts index 92c0c3527..f24d0b5a2 100644 --- a/packages/lib/server-only/admin/get-recipients-stats.ts +++ b/packages/lib/server-only/admin/get-recipients-stats.ts @@ -16,6 +16,7 @@ export const getRecipientsStats = async () => { [SendStatus.SENT]: 0, [SendStatus.NOT_SENT]: 0, }; + results.forEach((result) => { const { readStatus, signingStatus, sendStatus, _count } = result; stats[readStatus] += _count; @@ -23,5 +24,6 @@ export const getRecipientsStats = async () => { stats[sendStatus] += _count; stats.TOTAL_RECIPIENTS += _count; }); + return stats; };