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');