From 03180712710b44f098a58367abd3c8948ce38d37 Mon Sep 17 00:00:00 2001 From: Gautam Raj Date: Sun, 28 Jun 2026 20:58:15 +0530 Subject: [PATCH] feat: implement admin-only access for payments in production and add RestrictedAccess component --- apps/portfolio/app/(workspace)/layout.tsx | 9 +++ apps/portfolio/app/pricing/page.tsx | 14 +++- .../portfolio/components/RestrictedAccess.tsx | 64 +++++++++++++++++++ .../src/controllers/billingController.ts | 42 ++++++++++-- apps/site/app/pricing/pricing-experience.tsx | 47 ++++++++++++-- apps/studio/features/billing/BillingPage.tsx | 29 +++++++-- 6 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 apps/portfolio/components/RestrictedAccess.tsx diff --git a/apps/portfolio/app/(workspace)/layout.tsx b/apps/portfolio/app/(workspace)/layout.tsx index 2a10b58c..7fcb20e7 100644 --- a/apps/portfolio/app/(workspace)/layout.tsx +++ b/apps/portfolio/app/(workspace)/layout.tsx @@ -1,5 +1,8 @@ import type { PortfolioWorkspaceBootstrap } from "@/store/portfolio-store"; + import { fetchServerApiData } from "@/lib/server-api"; + +import { RestrictedAccess } from "@/components/RestrictedAccess"; import { WorkspaceProvider } from "@/components/WorkspaceProvider"; export default async function WorkspaceLayout({ children }: { children: React.ReactNode }) { @@ -9,6 +12,12 @@ export default async function WorkspaceLayout({ children }: { children: React.Re fetchServerApiData("/portfolios/analytics"), ]); + const isProd = process.env.NODE_ENV === "production"; + const adminEmail = (process.env.ADMIN_EMAIL || "ashragautam25@gmail.com").toLowerCase(); + const isUserAdmin = user && user.email && user.email.toLowerCase() === adminEmail; + + if (isProd && user && !isUserAdmin) return ; + return ( {children} ); diff --git a/apps/portfolio/app/pricing/page.tsx b/apps/portfolio/app/pricing/page.tsx index 213f85db..d16e9e0b 100644 --- a/apps/portfolio/app/pricing/page.tsx +++ b/apps/portfolio/app/pricing/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { siteConfig } from "@/config/site"; +import { fetchServerApiData } from "@/lib/server-api"; import { pricingFaqs } from "@/features/faq/constants"; @@ -48,7 +49,13 @@ export const metadata: Metadata = { }, }; -export default function PricingPage() { +export default async function PricingPage() { + const user = await fetchServerApiData<{ email: string | null }>("/users/me"); + const isProd = process.env.NODE_ENV === "production"; + const adminEmail = (process.env.ADMIN_EMAIL || "ashragautam25@gmail.com").toLowerCase(); + const isAdmin = user && user.email && user.email.toLowerCase() === adminEmail; + const paymentsBlocked = isProd && !isAdmin; + const pricingSchema = { "@context": "https://schema.org", "@type": "Product", @@ -159,6 +166,11 @@ export default function PricingPage() {
+ {paymentsBlocked && ( +
+ Payments are disabled in production during this phase. Only system administrators can perform checkouts. +
+ )} diff --git a/apps/portfolio/components/RestrictedAccess.tsx b/apps/portfolio/components/RestrictedAccess.tsx new file mode 100644 index 00000000..0b754ef5 --- /dev/null +++ b/apps/portfolio/components/RestrictedAccess.tsx @@ -0,0 +1,64 @@ +"use client"; + +import Link from "next/link"; +import { Lock, ArrowLeft, ArrowRight, AppWindow } from "lucide-react"; + +import { siteConfig, veriworklyProductLinks } from "@/config/site"; + +export function RestrictedAccess() { + return ( +
+
+
+ +
+
+ +
+ +

Private Preview Mode

+ +

+ The VeriWorkly Portfolio Builder is currently in active development. To guarantee platform + stability and design refinement, production access is restricted to administrators. +

+ +
+

+ What can I do? +

+
    +
  • + + Visit published portfolios (e.g. template examples) +
  • +
  • + + Access public pages like templates, FAQ, and pricing +
  • +
  • + + Manage your resumes and document tools in VeriWorkly Studio +
  • +
+
+ +
+ + Go to VeriWorkly Studio + + + + Back to Homepage + +
+
+
+ ); +} diff --git a/apps/server/src/controllers/billingController.ts b/apps/server/src/controllers/billingController.ts index 26c418a9..077208be 100644 --- a/apps/server/src/controllers/billingController.ts +++ b/apps/server/src/controllers/billingController.ts @@ -3,6 +3,7 @@ import type { NextFunction, Request, Response } from "express"; import { z } from "zod"; import { requireAuthUser } from "#middleware/auth"; +import { config } from "#config"; import { BillingService } from "#services/billingService"; import { CreditService } from "#services/creditService"; @@ -52,10 +53,20 @@ export class BillingController { try { const input = checkoutSchema.parse(req.body); + const isProd = config.nodeEnv === "production"; + const user = requireAuthUser(req); + const isAdmin = config.admin.email && user.email?.toLowerCase() === config.admin.email; + + if (isProd && !isAdmin) + throw new ApiError( + 403, + "Payments are disabled in production during this phase. Only administrators can perform payments.", + ); + res.json( createSuccessResponse( await BillingService.createCheckout( - requireAuthUser(req).id, + user.id, input.productKey, input.interval, input.redirectUrl, @@ -77,7 +88,17 @@ export class BillingController { static async portal(req: Request, res: Response, next: NextFunction) { try { - res.json(createSuccessResponse(await BillingService.createPortal(requireAuthUser(req).id))); + const isProd = config.nodeEnv === "production"; + const user = requireAuthUser(req); + const isAdmin = config.admin.email && user.email?.toLowerCase() === config.admin.email; + + if (isProd && !isAdmin) + throw new ApiError( + 403, + "Payments are disabled in production during this phase. Only administrators can perform payments.", + ); + + res.json(createSuccessResponse(await BillingService.createPortal(user.id))); } catch (error) { next(error); } @@ -86,13 +107,20 @@ export class BillingController { static async creditPackCheckout(req: Request, res: Response, next: NextFunction) { try { const input = creditPackCheckoutSchema.parse(req.body); + + const isProd = config.nodeEnv === "production"; + const user = requireAuthUser(req); + const isAdmin = config.admin.email && user.email?.toLowerCase() === config.admin.email; + + if (isProd && !isAdmin) + throw new ApiError( + 403, + "Payments are disabled in production during this phase. Only administrators can perform payments.", + ); + res.json( createSuccessResponse( - await BillingService.createCreditPackCheckout( - requireAuthUser(req).id, - input.packKey, - input.redirectUrl, - ), + await BillingService.createCreditPackCheckout(user.id, input.packKey, input.redirectUrl), ), ); } catch (error) { diff --git a/apps/site/app/pricing/pricing-experience.tsx b/apps/site/app/pricing/pricing-experience.tsx index 210b7936..6763128c 100644 --- a/apps/site/app/pricing/pricing-experience.tsx +++ b/apps/site/app/pricing/pricing-experience.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { ArrowRight, Check, @@ -67,6 +67,25 @@ export function PricingExperience() { const [customPlan, setCustomPlan] = useState<"portfolio_pro" | "ai_credits">("portfolio_pro"); const [loading, setLoading] = useState(""); const [error, setError] = useState(""); + const [user, setUser] = useState<{ email?: string | null } | null>(null); + const [checkingUser, setCheckingUser] = useState(true); + + useEffect(() => { + fetchApiData<{ email: string | null; name: string | null }>("/users/me") + .then((data) => { + setUser(data); + setCheckingUser(false); + }) + .catch(() => { + setUser(null); + setCheckingUser(false); + }); + }, []); + + const isProd = process.env.NODE_ENV === "production"; + const adminEmail = (process.env.NEXT_PUBLIC_ADMIN_EMAIL || "ashragautam25@gmail.com").toLowerCase(); + const isAdmin = user && user.email && user.email.toLowerCase() === adminEmail; + const paymentsBlocked = isProd && !isAdmin; const checkout = async (productKey: ProductKey, interval: BillingInterval) => { const checkoutKey = `${productKey}:${interval}`; @@ -131,6 +150,12 @@ export function PricingExperience() { + {paymentsBlocked ? ( +
+ Payments are disabled in production during this phase. Only system administrators can perform checkouts. +
+ ) : null} + {error ? (

{error} @@ -146,6 +171,7 @@ export function PricingExperience() { description="A tiny commitment for one focused application, portfolio update, or deadline." features={primaryFeatures} loading={loading === "bundle:one_day"} + disabled={paymentsBlocked} onCheckout={() => void checkout("bundle", "one_day")} /> void checkout("bundle", "seven_day")} /> void checkout("bundle", bundleInterval)} toggle={} /> @@ -239,9 +267,10 @@ export function PricingExperience() { void checkout(customPlan, "monthly")} > - Choose {selectedCustom.title} + {paymentsBlocked ? "Payments disabled" : `Choose ${selectedCustom.title}`} @@ -305,9 +334,10 @@ export function PricingExperience() { void checkout("bundle", "annual")} > - Get the yearly bundle + {paymentsBlocked ? "Payments disabled" : "Get the yearly bundle"} @@ -354,6 +384,7 @@ function PriceCard({ loading, toggle, onCheckout, + disabled, }: { marker: string; title: string; @@ -367,6 +398,7 @@ function PriceCard({ loading: boolean; toggle?: React.ReactNode; onCheckout: () => void; + disabled?: boolean; }) { return (

- Choose {title} + {disabled ? "Payments disabled" : `Choose ${title}`}
); @@ -423,16 +456,18 @@ function CheckoutButton({ className, loading, onClick, + disabled, }: { children: React.ReactNode; className: string; loading: boolean; onClick: () => void; + disabled?: boolean; }) { return ( View usage and action costs