From 2b447167f7eb47f465b79b544c0f986115b01953 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 8 Apr 2024 11:47:26 +0800 Subject: [PATCH] Add new pages and refactor existing code This commit adds new Admin and Accounts pages, while also improving code by refactoring various portions such as extracting services from the join page and dynamically importing packages in logging and monitoring code. The build command is also removed from the WordPress package, and SWC minification is enabled in the Next.js configuration. Updated marketing content is also included in this commit. --- apps/web/app/(marketing)/page.tsx | 18 ++---- .../app/admin/_components/admin-sidebar.tsx | 35 +++++++++++ .../web/app/admin/accounts/[account]/page.tsx | 0 apps/web/app/admin/accounts/page.tsx | 10 +++ apps/web/app/admin/layout.tsx | 7 +++ apps/web/app/admin/page.tsx | 11 ++++ .../app/join/_lib/server/join-team.service.ts | 47 ++++++++++++++ apps/web/app/join/page.tsx | 61 +++---------------- apps/web/instrumentation.ts | 10 +-- apps/web/next.config.mjs | 1 + packages/cms/wordpress/package.json | 1 - packages/monitoring/baselime/src/index.ts | 12 ++-- packages/monitoring/sentry/src/index.ts | 21 ++++--- packages/monitoring/src/instrumentation.ts | 5 +- packages/shared/src/logger/index.ts | 4 +- 15 files changed, 154 insertions(+), 89 deletions(-) create mode 100644 apps/web/app/admin/_components/admin-sidebar.tsx create mode 100644 apps/web/app/admin/accounts/[account]/page.tsx create mode 100644 apps/web/app/admin/accounts/page.tsx create mode 100644 apps/web/app/admin/layout.tsx create mode 100644 apps/web/app/admin/page.tsx create mode 100644 apps/web/app/join/_lib/server/join-team.service.ts diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx index b0be8c644..9f2e587d7 100644 --- a/apps/web/app/(marketing)/page.tsx +++ b/apps/web/app/(marketing)/page.tsx @@ -28,8 +28,9 @@ function Home() { - The SaaS Solution for - developers and founders + The SaaS Starter Kit + + straight from the future
@@ -221,8 +222,8 @@ function HeroTitle({ children }: React.PropsWithChildren) { return (

{children} @@ -232,14 +233,7 @@ function HeroTitle({ children }: React.PropsWithChildren) { function Pill(props: React.PropsWithChildren) { return ( -

+

{props.children}

); diff --git a/apps/web/app/admin/_components/admin-sidebar.tsx b/apps/web/app/admin/_components/admin-sidebar.tsx new file mode 100644 index 000000000..de651a9d4 --- /dev/null +++ b/apps/web/app/admin/_components/admin-sidebar.tsx @@ -0,0 +1,35 @@ +import { Home, Users } from 'lucide-react'; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarItem, +} from '@kit/ui/sidebar'; + +import { AppLogo } from '~/components/app-logo'; + +export function AdminSidebar() { + return ( + + + + + + + + }> + Home + + + } + > + Accounts + + + + + ); +} diff --git a/apps/web/app/admin/accounts/[account]/page.tsx b/apps/web/app/admin/accounts/[account]/page.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/web/app/admin/accounts/page.tsx b/apps/web/app/admin/accounts/page.tsx new file mode 100644 index 000000000..9129598de --- /dev/null +++ b/apps/web/app/admin/accounts/page.tsx @@ -0,0 +1,10 @@ +import { PageBody, PageHeader } from '@kit/ui/page'; + +export default function AccountsPage() { + return ( + <> + + ; + + ); +} diff --git a/apps/web/app/admin/layout.tsx b/apps/web/app/admin/layout.tsx new file mode 100644 index 000000000..7a858e9c4 --- /dev/null +++ b/apps/web/app/admin/layout.tsx @@ -0,0 +1,7 @@ +import { Page } from '@kit/ui/page'; + +import { AdminSidebar } from '~/admin/_components/admin-sidebar'; + +export default function AdminLayout(props: React.PropsWithChildren) { + return }>{props.children}; +} diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx new file mode 100644 index 000000000..146377d9e --- /dev/null +++ b/apps/web/app/admin/page.tsx @@ -0,0 +1,11 @@ +import { PageBody, PageHeader } from '@kit/ui/page'; + +export default function AdminPage() { + return ( + <> + + + + + ); +} diff --git a/apps/web/app/join/_lib/server/join-team.service.ts b/apps/web/app/join/_lib/server/join-team.service.ts new file mode 100644 index 000000000..a77ebc08a --- /dev/null +++ b/apps/web/app/join/_lib/server/join-team.service.ts @@ -0,0 +1,47 @@ +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; + +export class JoinTeamService { + async isCurrentUserAlreadyInAccount(accountId: string) { + const client = getSupabaseServerComponentClient(); + + const { data } = await client + .from('accounts') + .select('id') + .eq('id', accountId) + .maybeSingle(); + + return !!data?.id; + } + + async getInviteDataFromInviteToken(token: string) { + // we use an admin client to be able to read the pending membership + // without having to be logged in + const adminClient = getSupabaseServerComponentClient({ admin: true }); + + const { data: invitation, error } = await adminClient + .from('invitations') + .select< + string, + { + id: string; + account: { + id: string; + name: string; + slug: string; + picture_url: string; + }; + } + >( + 'id, expires_at, account: account_id !inner (id, name, slug, picture_url)', + ) + .eq('invite_token', token) + .gte('expires_at', new Date().toISOString()) + .single(); + + if (!invitation ?? error) { + return null; + } + + return invitation; + } +} diff --git a/apps/web/app/join/page.tsx b/apps/web/app/join/page.tsx index a16aca495..879c1071a 100644 --- a/apps/web/app/join/page.tsx +++ b/apps/web/app/join/page.tsx @@ -3,7 +3,6 @@ import { notFound, redirect } from 'next/navigation'; import { ArrowLeft } from 'lucide-react'; -import { Logger } from '@kit/shared/logger'; import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { AcceptInvitationContainer } from '@kit/team-accounts/components'; @@ -15,6 +14,8 @@ import pathsConfig from '~/config/paths.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import { JoinTeamService } from './_lib/server/join-team.service'; + interface Context { searchParams: { invite_token: string; @@ -47,19 +48,23 @@ async function JoinTeamAccountPage({ searchParams }: Context) { redirect(pathsConfig.auth.signUp + '?invite_token=' + token); } + const service = new JoinTeamService(); + // the user is logged in, we can now check if the token is valid - const invitation = await getInviteDataFromInviteToken(token); + const invitation = await service.getInviteDataFromInviteToken(token); if (!invitation) { return ; } // we need to verify the user isn't already in the account - const isInAccount = await isCurrentUserAlreadyInAccount( + const isInAccount = await service.isCurrentUserAlreadyInAccount( invitation.account.id, ); if (isInAccount) { + const { Logger } = await import('@kit/shared/logger'); + Logger.warn( { name: 'join-team-account', @@ -97,56 +102,6 @@ async function JoinTeamAccountPage({ searchParams }: Context) { export default withI18n(JoinTeamAccountPage); -/** - * Verifies that the current user is not already in the account by - * reading the document from the `accounts` table. If the user can read it - * it means they are already in the account. - * @param accountId - */ -async function isCurrentUserAlreadyInAccount(accountId: string) { - const client = getSupabaseServerComponentClient(); - - const { data } = await client - .from('accounts') - .select('id') - .eq('id', accountId) - .maybeSingle(); - - return !!data?.id; -} - -async function getInviteDataFromInviteToken(token: string) { - // we use an admin client to be able to read the pending membership - // without having to be logged in - const adminClient = getSupabaseServerComponentClient({ admin: true }); - - const { data: invitation, error } = await adminClient - .from('invitations') - .select< - string, - { - id: string; - account: { - id: string; - name: string; - slug: string; - picture_url: string; - }; - } - >( - 'id, expires_at, account: account_id !inner (id, name, slug, picture_url)', - ) - .eq('invite_token', token) - .gte('expires_at', new Date().toISOString()) - .single(); - - if (!invitation ?? error) { - return null; - } - - return invitation; -} - function InviteNotFoundOrExpired() { return (
diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts index a1dfadbb7..ca20f6ef0 100644 --- a/apps/web/instrumentation.ts +++ b/apps/web/instrumentation.ts @@ -1,7 +1,9 @@ import { registerInstrumentation } from '@kit/monitoring'; -export async function register() { - // Register monitoring instrumentation based on the - // MONITORING_INSTRUMENTATION_PROVIDER environment variable. - await registerInstrumentation(); +export function register() { + if (process.env.NEXT_RUNTIME !== 'nodejs') { + // Register monitoring instrumentation based on the + // MONITORING_INSTRUMENTATION_PROVIDER environment variable. + return registerInstrumentation(); + } } diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 910641b88..edf006881 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -23,6 +23,7 @@ const INTERNAL_PACKAGES = [ /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, + swcMinify: true, /** Enables hot reloading for local packages without a build step */ transpilePackages: INTERNAL_PACKAGES, pageExtensions: ['ts', 'tsx'], diff --git a/packages/cms/wordpress/package.json b/packages/cms/wordpress/package.json index 60fc579e4..2dc702777 100644 --- a/packages/cms/wordpress/package.json +++ b/packages/cms/wordpress/package.json @@ -7,7 +7,6 @@ "format": "prettier --check \"**/*.{ts,tsx}\"", "lint": "eslint .", "typecheck": "tsc --noEmit", - "build": "contentlayer build", "start": "docker compose up" }, "prettier": "@kit/prettier-config", diff --git a/packages/monitoring/baselime/src/index.ts b/packages/monitoring/baselime/src/index.ts index e1bc23d25..590437486 100644 --- a/packages/monitoring/baselime/src/index.ts +++ b/packages/monitoring/baselime/src/index.ts @@ -1,9 +1,3 @@ -import { - BaselimeSDK, - BetterHttpInstrumentation, - VercelPlugin, -} from '@baselime/node-opentelemetry'; - const INSTRUMENTATION_SERVICE_NAME = process.env.INSTRUMENTATION_SERVICE_NAME; if (!INSTRUMENTATION_SERVICE_NAME) { @@ -18,7 +12,11 @@ if (!INSTRUMENTATION_SERVICE_NAME) { * * Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to 'baselime' to register Baselime instrumentation. */ -export function registerBaselimeInstrumentation() { +export async function registerBaselimeInstrumentation() { + const { BaselimeSDK, BetterHttpInstrumentation, VercelPlugin } = await import( + '@baselime/node-opentelemetry' + ); + const sdk = new BaselimeSDK({ serverless: true, service: INSTRUMENTATION_SERVICE_NAME, diff --git a/packages/monitoring/sentry/src/index.ts b/packages/monitoring/sentry/src/index.ts index cc9c91ced..67385ced0 100644 --- a/packages/monitoring/sentry/src/index.ts +++ b/packages/monitoring/sentry/src/index.ts @@ -1,11 +1,3 @@ -import { Resource } from '@opentelemetry/resources'; -import { NodeSDK } from '@opentelemetry/sdk-node'; -import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; -import { - SentryPropagator, - SentrySpanProcessor, -} from '@sentry/opentelemetry-node'; - const INSTRUMENTATION_SERVICE_NAME = process.env.INSTRUMENTATION_SERVICE_NAME; if (!INSTRUMENTATION_SERVICE_NAME) { @@ -20,7 +12,18 @@ if (!INSTRUMENTATION_SERVICE_NAME) { * * Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to 'sentry' to register Sentry instrumentation. */ -export function registerSentryInstrumentation() { +export async function registerSentryInstrumentation() { + const { Resource } = await import('@opentelemetry/resources'); + const { NodeSDK } = await import('@opentelemetry/sdk-node'); + + const { SEMRESATTRS_SERVICE_NAME } = await import( + '@opentelemetry/semantic-conventions' + ); + + const { SentrySpanProcessor, SentryPropagator } = await import( + '@sentry/opentelemetry-node' + ); + const sdk = new NodeSDK({ resource: new Resource({ [SEMRESATTRS_SERVICE_NAME]: INSTRUMENTATION_SERVICE_NAME, diff --git a/packages/monitoring/src/instrumentation.ts b/packages/monitoring/src/instrumentation.ts index aa6abd654..70e228ac4 100644 --- a/packages/monitoring/src/instrumentation.ts +++ b/packages/monitoring/src/instrumentation.ts @@ -17,7 +17,6 @@ const DEFAULT_INSTRUMENTATION_PROVIDER = process.env * Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to register the monitoring instrumentation provider. */ export async function registerInstrumentation() { - // Only run instrumentation in Node.js environment if ( process.env.NEXT_RUNTIME !== 'nodejs' || !DEFAULT_INSTRUMENTATION_PROVIDER @@ -39,6 +38,8 @@ export async function registerInstrumentation() { } default: - throw new Error(`Unknown instrumentation provider`); + throw new Error( + `Unknown instrumentation provider: ${DEFAULT_INSTRUMENTATION_PROVIDER as string}`, + ); } } diff --git a/packages/shared/src/logger/index.ts b/packages/shared/src/logger/index.ts index 0323c4dca..e6610e16b 100644 --- a/packages/shared/src/logger/index.ts +++ b/packages/shared/src/logger/index.ts @@ -1,12 +1,14 @@ import { Logger as LoggerInstance } from './logger'; +const LOGGER = process.env.LOGGER ?? 'pino'; + /* * Logger * By default, the logger is set to use Pino. To change the logger, update the import statement below. * to your desired logger implementation. */ async function getLogger(): Promise { - switch (process.env.LOGGER ?? 'pino') { + switch (LOGGER) { case 'pino': { const { Logger: PinoLogger } = await import('./impl/pino');