From 682f1aa2ba92c1f562eeb206e9ceb29d82048da8 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 7 Sep 2025 16:08:22 +1000 Subject: [PATCH 01/10] Setup Supabase SSR --- apps/web/middleware.ts | 18 ++--- apps/web/package.json | 3 +- apps/web/pages/_app.tsx | 18 ++--- apps/web/pages/api/auth/callback.ts | 32 +++++++-- apps/web/utils/supabase/client.ts | 9 +++ apps/web/utils/supabase/middleware.ts | 40 +++++++++++ apps/web/utils/supabase/server.ts | 49 ++++++++++++++ apps/web/utils/supabase/supabase-admin.ts | 25 ++++--- apps/web/utils/useSSR.ts | 5 +- apps/web/utils/useUser.tsx | 82 +++++++++++++++-------- pnpm-lock.yaml | 68 ++++++------------- 11 files changed, 230 insertions(+), 119 deletions(-) create mode 100644 apps/web/utils/supabase/client.ts create mode 100644 apps/web/utils/supabase/middleware.ts create mode 100644 apps/web/utils/supabase/server.ts diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 9424aa5..a01d504 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,20 +1,20 @@ -import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { createMiddlewareClient } from "./utils/supabase/middleware"; export async function middleware(req: NextRequest) { - // We need to create a response and hand it to the supabase client to be able to modify the response headers. - const res = NextResponse.next(); - const supabase = createMiddlewareClient({ req, res }); + const { supabase, response } = createMiddlewareClient(req); + + await supabase.auth.getSession(); + const { - data: { session }, - } = await supabase.auth.getSession(); + data: { user }, + } = await supabase.auth.getUser(); - if (session?.user) { - return res; + if (user) { + return response; } - // Auth condition not met, redirect to home page. const redirectUrl = req.nextUrl.clone(); redirectUrl.pathname = "/login"; redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname); diff --git a/apps/web/package.json b/apps/web/package.json index a9138ae..c138251 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,10 +21,9 @@ "@sanity/client": "^5.4.2", "@sanity/image-url": "^1.0.2", "@sentry/nextjs": "^7.77.0", - "@supabase/auth-helpers-nextjs": "^0.8.7", - "@supabase/auth-helpers-react": "^0.4.2", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", + "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.39.3", "@tailwindcss/typography": "^0.5.1", "@types/canvas-confetti": "^1.6.4", diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 5ce57f4..7116e1d 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,12 +1,10 @@ -import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs"; -import { SessionContextProvider } from "@supabase/auth-helpers-react"; import dynamic from "next/dynamic"; import localFont from "next/font/local"; import Head from "next/head"; import { Router } from "next/router"; import posthog from "posthog-js"; import { PostHogProvider } from "posthog-js/react"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import "../styles/global.css"; import { UserContextProvider } from "../utils/useUser"; @@ -32,7 +30,6 @@ const geist = localFont({ export default function App({ Component, pageProps }) { const getLayout = Component.getLayout || ((page) => page); - const [supabaseClient] = useState(() => createPagesBrowserClient()); useEffect(() => { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { @@ -63,15 +60,10 @@ export default function App({ Component, pageProps }) { --geist-font: ${geist.style.fontFamily}; } `} - - - {getLayout()} - - - + + {getLayout()} + + ); } diff --git a/apps/web/pages/api/auth/callback.ts b/apps/web/pages/api/auth/callback.ts index 1f9e6d3..dc08941 100644 --- a/apps/web/pages/api/auth/callback.ts +++ b/apps/web/pages/api/auth/callback.ts @@ -1,18 +1,40 @@ -import { createPagesServerClient } from "@supabase/auth-helpers-nextjs"; import { NextApiHandler } from "next"; import { ROUTES } from "../../../data/routes.data"; -import { Database } from "@changes-page/supabase/types"; +import { createServerClientSSR } from "../../../utils/supabase/server"; const callback: NextApiHandler = async (req, res) => { const code = req.query.code; const redirectedFrom = req.query.redirectedFrom; if (typeof code === "string") { - const supabase = createPagesServerClient({ req, res }); - await supabase.auth.exchangeCodeForSession(code); + const supabase = createServerClientSSR({ req, res }); + + try { + const { data, error } = await supabase.auth.exchangeCodeForSession(code); + + if (error) { + console.error("Auth callback error:", error); + return res.redirect( + `/login?error=${encodeURIComponent(error.message)}` + ); + } + + if (!data.session) { + console.error("Auth callback: No session created"); + return res.redirect( + `/login?error=${encodeURIComponent("No session created")}` + ); + } + } catch (err) { + console.error("Auth callback exception:", err); + return res.redirect( + `/login?error=${encodeURIComponent("Authentication failed")}` + ); + } } - res.redirect(redirectedFrom ? `${redirectedFrom}` : ROUTES.PAGES); + const redirectTo = redirectedFrom ? `${redirectedFrom}` : ROUTES.PAGES; + res.redirect(redirectTo); }; export default callback; diff --git a/apps/web/utils/supabase/client.ts b/apps/web/utils/supabase/client.ts new file mode 100644 index 0000000..b08d18a --- /dev/null +++ b/apps/web/utils/supabase/client.ts @@ -0,0 +1,9 @@ +import { Database } from "@changes-page/supabase/types"; +import { createBrowserClient } from "@supabase/ssr"; + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} diff --git a/apps/web/utils/supabase/middleware.ts b/apps/web/utils/supabase/middleware.ts new file mode 100644 index 0000000..51d3cfe --- /dev/null +++ b/apps/web/utils/supabase/middleware.ts @@ -0,0 +1,40 @@ +import { Database } from "@changes-page/supabase/types"; +import { createServerClient } from "@supabase/ssr"; +import { NextRequest, NextResponse } from "next/server"; + +export function createMiddlewareClient(request: NextRequest) { + let response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => { + request.cookies.set({ name, value, ...options }); + }); + + response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + + cookiesToSet.forEach(({ name, value, options }) => { + response.cookies.set({ name, value, ...options }); + }); + }, + }, + } + ); + + return { supabase, response }; +} \ No newline at end of file diff --git a/apps/web/utils/supabase/server.ts b/apps/web/utils/supabase/server.ts new file mode 100644 index 0000000..f707f90 --- /dev/null +++ b/apps/web/utils/supabase/server.ts @@ -0,0 +1,49 @@ +import { Database } from "@changes-page/supabase/types"; +import { createServerClient } from "@supabase/ssr"; + +export function createServerClientSSR(context: { req: any; res: any }) { + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return Object.keys(context.req.cookies).map((name) => ({ + name, + value: context.req.cookies[name], + })); + }, + setAll(cookiesToSet) { + const cookieStrings = cookiesToSet.map(({ name, value, options }) => { + const cookieOptions = { + path: "/", + httpOnly: false, // Allow client-side access for supabase-js + secure: process.env.NODE_ENV === "production", + sameSite: "lax" as const, + ...options, + }; + + return `${name}=${value}; Path=${cookieOptions.path}; ${ + cookieOptions.httpOnly ? "HttpOnly;" : "" + } ${cookieOptions.secure ? "Secure;" : ""} SameSite=${ + cookieOptions.sameSite + };${ + cookieOptions.maxAge ? ` Max-Age=${cookieOptions.maxAge};` : "" + }`; + }); + + // Get existing cookies and add the new ones + const existingCookies = context.res.getHeader("Set-Cookie") || []; + const existingCookieArray = Array.isArray(existingCookies) + ? existingCookies + : [existingCookies].filter(Boolean); + + context.res.setHeader("Set-Cookie", [ + ...existingCookieArray, + ...cookieStrings, + ]); + }, + }, + } + ); +} diff --git a/apps/web/utils/supabase/supabase-admin.ts b/apps/web/utils/supabase/supabase-admin.ts index 251b4bb..a63f3d5 100644 --- a/apps/web/utils/supabase/supabase-admin.ts +++ b/apps/web/utils/supabase/supabase-admin.ts @@ -1,15 +1,11 @@ import { Database } from "@changes-page/supabase/types"; -import { - createPagesServerClient, - Session, - User, -} from "@supabase/auth-helpers-nextjs"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { Session, SupabaseClient, User } from "@supabase/supabase-js"; import { GetServerSidePropsContext, NextApiRequest, NextApiResponse, } from "next"; +import { createServerClientSSR } from "./server"; export const getSupabaseServerClient = async ( context: @@ -20,10 +16,19 @@ export const getSupabaseServerClient = async ( } ): Promise<{ supabase: SupabaseClient; - session: Session; - user: User; + session: Session | null; + user: User | null; }> => { - const supabase = createPagesServerClient(context); + const supabase = createServerClientSSR(context); + + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (error) { + console.error("Error getting user:", error); + } const { data: { session }, @@ -32,6 +37,6 @@ export const getSupabaseServerClient = async ( return { supabase, session, - user: session?.user, + user, }; }; diff --git a/apps/web/utils/useSSR.ts b/apps/web/utils/useSSR.ts index 34d723e..fd15fa4 100644 --- a/apps/web/utils/useSSR.ts +++ b/apps/web/utils/useSSR.ts @@ -1,6 +1,7 @@ -import { SupabaseClient } from "@supabase/auth-helpers-nextjs"; +import { SupabaseClient } from "@supabase/supabase-js"; +import { Database } from "@changes-page/supabase/types"; -export async function getPage(supabase: SupabaseClient, id: string) { +export async function getPage(supabase: SupabaseClient, id: string) { const { data: page } = await supabase .from("pages") .select("id,title,type,description,url_slug,user_id") diff --git a/apps/web/utils/useUser.tsx b/apps/web/utils/useUser.tsx index 7d3762d..5fee354 100644 --- a/apps/web/utils/useUser.tsx +++ b/apps/web/utils/useUser.tsx @@ -1,10 +1,5 @@ import { Database } from "@changes-page/supabase/types"; -import { Session, SupabaseClient, User } from "@supabase/auth-helpers-nextjs"; -import { - useSession, - useSupabaseClient, - useUser, -} from "@supabase/auth-helpers-react"; +import { Session, SupabaseClient, User } from "@supabase/supabase-js"; import { useRouter } from "next/router"; import posthog from "posthog-js"; import { @@ -16,8 +11,9 @@ import { } from "react"; import { notifyError, notifyInfo } from "../components/core/toast.component"; import { ROUTES } from "../data/routes.data"; -import { IBillingInfo, IUser } from "../data/user.interface"; +import { IBillingInfo } from "../data/user.interface"; import { httpGet } from "../utils/http"; +import { createClient } from "./supabase/client"; const UserContext = createContext<{ loading: boolean; @@ -25,7 +21,6 @@ const UserContext = createContext<{ user: User | null; billingDetails: IBillingInfo; fetchBilling: () => void; - fetchUser: () => Promise; signOut: () => Promise<{ error: Error | null }>; supabase: SupabaseClient; }>({ @@ -34,25 +29,29 @@ const UserContext = createContext<{ user: null, billingDetails: null, fetchBilling: () => null, - fetchUser: () => null, signOut: () => null, supabase: null, }); -export const UserContextProvider = (props: any) => { - const supabase = useSupabaseClient(); - const session = useSession(); - const user = useUser(); +export const UserContextProvider = ({ + children, + initialSession = null, +}: { + children: React.ReactNode; + initialSession?: Session | null; +}) => { + const [supabase] = useState(() => createClient()); + const [session, setSession] = useState(initialSession); + const [user, setUser] = useState(initialSession?.user ?? null); + const [loading, setLoading] = useState(!initialSession); + const [billingDetails, setBillingDetails] = useState(null); const router = useRouter(); - const [loading, setLoading] = useState(true); - const [billingDetails, setBillingDetails] = useState(null); const fetchBilling = useCallback(async () => { return httpGet({ url: `/api/billing` }) .then((billingDetails) => { setBillingDetails(billingDetails); - return billingDetails; }) .catch((error) => { @@ -61,6 +60,41 @@ export const UserContextProvider = (props: any) => { }); }, []); + const signOut = useCallback(async () => { + await router.replace(ROUTES.HOME); + const { error } = await supabase.auth.signOut(); + + setBillingDetails(null); + notifyInfo("Logout completed"); + + return { error }; + }, [supabase, router]); + + useEffect(() => { + // Get initial session + const getInitialSession = async () => { + const { + data: { session }, + } = await supabase.auth.getSession(); + setSession(session); + setUser(session?.user ?? null); + setLoading(false); + }; + + getInitialSession(); + + // Listen for auth changes + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(async (_, session) => { + setSession(session); + setUser(session?.user ?? null); + setLoading(false); + }); + + return () => subscription.unsubscribe(); + }, [supabase]); + useEffect(() => { if (user) { fetchBilling().then(() => { @@ -79,29 +113,19 @@ export const UserContextProvider = (props: any) => { loading, session, user, - billingDetails, fetchBilling, - - signOut: async () => { - await router.replace(ROUTES.HOME); - await supabase.auth.signOut(); - - setBillingDetails(null); - - notifyInfo("Logout completed"); - }, - + signOut, supabase, }; - return ; + return {children}; }; export const useUserData = () => { const context = useContext(UserContext); if (context === undefined) { - throw new Error(`useUser must be used within a UserContextProvider.`); + throw new Error(`useUserData must be used within a UserContextProvider.`); } return context; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2e9d45..a220e7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,18 +243,15 @@ importers: '@sentry/nextjs': specifier: ^7.77.0 version: 7.120.3(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@supabase/auth-helpers-nextjs': - specifier: ^0.8.7 - version: 0.8.7(@supabase/supabase-js@2.49.4) - '@supabase/auth-helpers-react': - specifier: ^0.4.2 - version: 0.4.2(@supabase/supabase-js@2.49.4) '@supabase/auth-ui-react': specifier: ^0.4.7 version: 0.4.7(@supabase/supabase-js@2.49.4) '@supabase/auth-ui-shared': specifier: ^0.1.8 version: 0.1.8(@supabase/supabase-js@2.49.4) + '@supabase/ssr': + specifier: ^0.7.0 + version: 0.7.0(@supabase/supabase-js@2.49.4) '@supabase/supabase-js': specifier: ^2.39.3 version: 2.49.4 @@ -1691,24 +1688,6 @@ packages: '@streamparser/json@0.0.12': resolution: {integrity: sha512-+kmRpd+EeTFd3qNt1AoKphJqbAN26ZDsbiwqjBFeoAmdCyiUO19xMXPtYi9vovAj9a7OAJnvWtiHkwwjU2Fx4Q==} - '@supabase/auth-helpers-nextjs@0.8.7': - resolution: {integrity: sha512-iYdOjFo0GkRvha340l8JdCiBiyXQuG9v8jnq7qMJ/2fakrskRgHTCOt7ryWbip1T6BExcWKC8SoJrhCzPOxhhg==} - deprecated: This package is now deprecated - please use the @supabase/ssr package instead. - peerDependencies: - '@supabase/supabase-js': ^2.19.0 - - '@supabase/auth-helpers-react@0.4.2': - resolution: {integrity: sha512-zRj1leYMKJVYQeHFvZiUzlmHM+ATWFR/V7Q9F0yXSWEnMcNHL0CKnIBqhkjtSQ2trE+YaoCvFEHjxISppxIZXQ==} - deprecated: This package is now deprecated - please use the @supabase/ssr package instead. - peerDependencies: - '@supabase/supabase-js': ^2.19.0 - - '@supabase/auth-helpers-shared@0.6.3': - resolution: {integrity: sha512-xYQRLFeFkL4ZfwC7p9VKcarshj3FB2QJMgJPydvOY7J5czJe6xSG5/wM1z63RmAzGbCkKg+dzpq61oeSyWiGBQ==} - deprecated: This package is now deprecated - please use the @supabase/ssr package instead. - peerDependencies: - '@supabase/supabase-js': ^2.19.0 - '@supabase/auth-js@2.69.1': resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==} @@ -1735,6 +1714,11 @@ packages: '@supabase/realtime-js@2.11.2': resolution: {integrity: sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==} + '@supabase/ssr@0.7.0': + resolution: {integrity: sha512-G65t5EhLSJ5c8hTCcXifSL9Q/ZRXvqgXeNo+d3P56f4U1IxwTqjB64UfmfixvmMcjuxnq2yGqEWVJqUcO+AzAg==} + peerDependencies: + '@supabase/supabase-js': ^2.43.4 + '@supabase/storage-js@2.7.1': resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} @@ -2474,6 +2458,10 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + core-js@3.42.0: resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} @@ -3478,9 +3466,6 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4636,9 +4621,6 @@ packages: resolution: {integrity: sha512-6a6dNqipzbCPlTFgztfNP2oG+IGcflMe/01zSzGrQcxGMKbIjOemBBD85pH92klWaJavAUWxAh9Z0aU28zxW6A==} deprecated: Rolling release, please update to 0.2.0 - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6423,21 +6405,6 @@ snapshots: '@streamparser/json@0.0.12': {} - '@supabase/auth-helpers-nextjs@0.8.7(@supabase/supabase-js@2.49.4)': - dependencies: - '@supabase/auth-helpers-shared': 0.6.3(@supabase/supabase-js@2.49.4) - '@supabase/supabase-js': 2.49.4 - set-cookie-parser: 2.7.1 - - '@supabase/auth-helpers-react@0.4.2(@supabase/supabase-js@2.49.4)': - dependencies: - '@supabase/supabase-js': 2.49.4 - - '@supabase/auth-helpers-shared@0.6.3(@supabase/supabase-js@2.49.4)': - dependencies: - '@supabase/supabase-js': 2.49.4 - jose: 4.15.9 - '@supabase/auth-js@2.69.1': dependencies: '@supabase/node-fetch': 2.6.15 @@ -6477,6 +6444,11 @@ snapshots: - bufferutil - utf-8-validate + '@supabase/ssr@0.7.0(@supabase/supabase-js@2.49.4)': + dependencies: + '@supabase/supabase-js': 2.49.4 + cookie: 1.0.2 + '@supabase/storage-js@2.7.1': dependencies: '@supabase/node-fetch': 2.6.15 @@ -7311,6 +7283,8 @@ snapshots: cookie-es@1.2.2: {} + cookie@1.0.2: {} + core-js@3.42.0: {} cors@2.8.5: @@ -8746,8 +8720,6 @@ snapshots: jju@1.4.0: {} - jose@4.15.9: {} - js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -10347,8 +10319,6 @@ snapshots: serialize-error-cjs@0.1.4: {} - set-cookie-parser@2.7.1: {} - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 From 7fbb3b5e1d6705fd2ce56d5d825418fb3366cd35 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 7 Sep 2025 16:43:35 +1000 Subject: [PATCH 02/10] Address code review --- apps/web/pages/api/auth/callback.ts | 11 ++++++++++- apps/web/utils/supabase/client.ts | 14 ++++++++++---- apps/web/utils/useUser.tsx | 4 +++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/web/pages/api/auth/callback.ts b/apps/web/pages/api/auth/callback.ts index dc08941..a8a5000 100644 --- a/apps/web/pages/api/auth/callback.ts +++ b/apps/web/pages/api/auth/callback.ts @@ -33,7 +33,16 @@ const callback: NextApiHandler = async (req, res) => { } } - const redirectTo = redirectedFrom ? `${redirectedFrom}` : ROUTES.PAGES; + let redirectTo = ROUTES.PAGES; + if ( + typeof redirectedFrom === "string" && + redirectedFrom.startsWith("/") && + !redirectedFrom.includes("://") && + !redirectedFrom.includes("\\") + ) { + redirectTo = redirectedFrom; + } + res.redirect(redirectTo); }; diff --git a/apps/web/utils/supabase/client.ts b/apps/web/utils/supabase/client.ts index b08d18a..9bfd5ba 100644 --- a/apps/web/utils/supabase/client.ts +++ b/apps/web/utils/supabase/client.ts @@ -2,8 +2,14 @@ import { Database } from "@changes-page/supabase/types"; import { createBrowserClient } from "@supabase/ssr"; export function createClient() { - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - ); + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!url || !anon) { + throw new Error( + "Supabase env missing: set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY" + ); + } + + return createBrowserClient(url, anon); } diff --git a/apps/web/utils/useUser.tsx b/apps/web/utils/useUser.tsx index 5fee354..b5255fa 100644 --- a/apps/web/utils/useUser.tsx +++ b/apps/web/utils/useUser.tsx @@ -61,10 +61,12 @@ export const UserContextProvider = ({ }, []); const signOut = useCallback(async () => { - await router.replace(ROUTES.HOME); const { error } = await supabase.auth.signOut(); + posthog.reset(); setBillingDetails(null); + await router.replace(ROUTES.HOME); + notifyInfo("Logout completed"); return { error }; From 70a09fbc88f7d0ac9c7b689606a56a8855fda4f1 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 7 Sep 2025 17:11:21 +1000 Subject: [PATCH 03/10] Fix Supabase server clients --- apps/web/pages/api/ai/get-streaming-url.ts | 4 +- apps/web/pages/api/ai/suggest-title.ts | 4 +- apps/web/pages/api/auth/callback.ts | 4 +- .../api/billing/create-billing-portal.ts | 4 +- .../api/billing/enable-email-notifications.ts | 6 +- apps/web/pages/api/billing/index.ts | 4 +- .../pages/api/billing/redirect-to-checkout.ts | 4 +- .../api/emails/subscribers/export-csv.ts | 6 +- .../web/pages/api/emails/subscribers/index.ts | 6 +- apps/web/pages/api/pages/new.ts | 4 +- apps/web/pages/api/pages/reactions.ts | 4 +- .../pages/api/pages/settings/add-domain.ts | 4 +- .../pages/api/pages/settings/check-domain.ts | 4 +- apps/web/pages/api/pages/settings/index.ts | 4 +- .../pages/api/pages/settings/remove-domain.ts | 4 +- apps/web/pages/api/posts/index.ts | 7 +- .../pages/api/teams/invite/accept/index.ts | 7 +- apps/web/pages/api/teams/invite/index.ts | 7 +- apps/web/pages/api/teams/member/[id]/index.ts | 6 +- apps/web/pages/integrations/zapier.tsx | 4 +- apps/web/pages/onboarding/open-page.tsx | 4 +- apps/web/pages/pages/[page_id]/[post_id].tsx | 10 +-- apps/web/pages/pages/[page_id]/analytics.tsx | 32 ++++---- apps/web/pages/pages/[page_id]/audit-logs.tsx | 10 +-- apps/web/pages/pages/[page_id]/edit.tsx | 12 +-- apps/web/pages/pages/[page_id]/index.tsx | 10 +-- .../pages/[page_id]/roadmap/[board_id].tsx | 14 ++-- .../[page_id]/roadmap/[board_id]/settings.tsx | 13 ++-- .../pages/pages/[page_id]/roadmap/index.tsx | 43 +++++++---- .../web/pages/pages/[page_id]/roadmap/new.tsx | 27 ++----- .../pages/[page_id]/settings/[activeTab].tsx | 12 +-- apps/web/pages/pages/index.tsx | 4 +- apps/web/utils/supabase/server.ts | 77 +++++++++++-------- apps/web/utils/supabase/static.ts | 10 +++ apps/web/utils/supabase/supabase-admin.ts | 43 ++++++++--- 35 files changed, 240 insertions(+), 178 deletions(-) create mode 100644 apps/web/utils/supabase/static.ts diff --git a/apps/web/pages/api/ai/get-streaming-url.ts b/apps/web/pages/api/ai/get-streaming-url.ts index 8ce28c4..25e9d85 100644 --- a/apps/web/pages/api/ai/get-streaming-url.ts +++ b/apps/web/pages/api/ai/get-streaming-url.ts @@ -1,7 +1,7 @@ import { IErrorResponse } from "@changes-page/supabase/types/api"; import type { NextApiRequest, NextApiResponse } from "next"; import { createSignedStreamingUrl } from "../../../utils/manageprompt"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; const expandConcept = async ( req: NextApiRequest, @@ -10,7 +10,7 @@ const expandConcept = async ( if (req.method === "POST") { const { workflowId } = req.body; try { - await getSupabaseServerClient({ req, res }); + await getSupabaseServerClientForAPI({ req, res }); const url = await createSignedStreamingUrl(workflowId); diff --git a/apps/web/pages/api/ai/suggest-title.ts b/apps/web/pages/api/ai/suggest-title.ts index 2919460..64ba3fd 100644 --- a/apps/web/pages/api/ai/suggest-title.ts +++ b/apps/web/pages/api/ai/suggest-title.ts @@ -1,7 +1,7 @@ import { IErrorResponse } from "@changes-page/supabase/types/api"; import type { NextApiRequest, NextApiResponse } from "next"; import { runWorkflow } from "../../../utils/manageprompt"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; const suggestTitle = async ( req: NextApiRequest, @@ -11,7 +11,7 @@ const suggestTitle = async ( const { content } = req.body; try { - await getSupabaseServerClient({ req, res }); + await getSupabaseServerClientForAPI({ req, res }); const result = await runWorkflow("wf_e1eb79b1dc017ca189506d799453caae", { content, diff --git a/apps/web/pages/api/auth/callback.ts b/apps/web/pages/api/auth/callback.ts index a8a5000..b73e49c 100644 --- a/apps/web/pages/api/auth/callback.ts +++ b/apps/web/pages/api/auth/callback.ts @@ -1,13 +1,13 @@ import { NextApiHandler } from "next"; import { ROUTES } from "../../../data/routes.data"; -import { createServerClientSSR } from "../../../utils/supabase/server"; +import { createServerClientForAPI } from "../../../utils/supabase/server"; const callback: NextApiHandler = async (req, res) => { const code = req.query.code; const redirectedFrom = req.query.redirectedFrom; if (typeof code === "string") { - const supabase = createServerClientSSR({ req, res }); + const supabase = createServerClientForAPI({ req, res }); try { const { data, error } = await supabase.auth.exchangeCodeForSession(code); diff --git a/apps/web/pages/api/billing/create-billing-portal.ts b/apps/web/pages/api/billing/create-billing-portal.ts index 5a4ab92..e52aec3 100644 --- a/apps/web/pages/api/billing/create-billing-portal.ts +++ b/apps/web/pages/api/billing/create-billing-portal.ts @@ -1,7 +1,7 @@ import { IErrorResponse } from "@changes-page/supabase/types/api"; import type { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createOrRetrieveCustomer } from "../../../utils/useDatabase"; import { getAppBaseURL } from "./../../../utils/helpers"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); @@ -15,7 +15,7 @@ const createBillingSession = async ( const { return_url } = req.body; try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const customer = await createOrRetrieveCustomer(user.id, user.email); console.log( diff --git a/apps/web/pages/api/billing/enable-email-notifications.ts b/apps/web/pages/api/billing/enable-email-notifications.ts index 73a3316..1e22171 100644 --- a/apps/web/pages/api/billing/enable-email-notifications.ts +++ b/apps/web/pages/api/billing/enable-email-notifications.ts @@ -1,7 +1,7 @@ +import { IErrorResponse } from "@changes-page/supabase/types/api"; import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; -import { IErrorResponse } from "@changes-page/supabase/types/api"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../utils/useDatabase"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); @@ -12,7 +12,7 @@ const enableEmailNotifications = async ( ) => { if (req.method === "PUT") { try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { stripe_subscription_id, stripe_subscription } = await getUserById( user.id diff --git a/apps/web/pages/api/billing/index.ts b/apps/web/pages/api/billing/index.ts index ad97cb1..2a9a2a3 100644 --- a/apps/web/pages/api/billing/index.ts +++ b/apps/web/pages/api/billing/index.ts @@ -2,7 +2,7 @@ import { IErrorResponse } from "@changes-page/supabase/types/api"; import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; import { IBillingInfo } from "../../../data/user.interface"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../utils/useDatabase"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); @@ -13,7 +13,7 @@ const getBillingStatus = async ( ) => { if (req.method === "GET") { try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { pro_gifted, diff --git a/apps/web/pages/api/billing/redirect-to-checkout.ts b/apps/web/pages/api/billing/redirect-to-checkout.ts index 5e63382..81651f9 100644 --- a/apps/web/pages/api/billing/redirect-to-checkout.ts +++ b/apps/web/pages/api/billing/redirect-to-checkout.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getAppBaseURL } from "../../../utils/helpers"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createOrRetrieveCustomer, getUserById, @@ -18,7 +18,7 @@ const redirectToCheckout = async ( const { return_url } = req.query; try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { stripe_customer_id, diff --git a/apps/web/pages/api/emails/subscribers/export-csv.ts b/apps/web/pages/api/emails/subscribers/export-csv.ts index e2db8d2..ee2d493 100644 --- a/apps/web/pages/api/emails/subscribers/export-csv.ts +++ b/apps/web/pages/api/emails/subscribers/export-csv.ts @@ -2,7 +2,7 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { Parser } from "@json2csv/plainjs"; import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; const getSubscribersExportCsv = async ( req: NextApiRequest, @@ -11,9 +11,9 @@ const getSubscribersExportCsv = async ( if (req.method === "GET") { try { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); - const { page_id } = req.query; + const page_id = String(req.query.page_id); await supabaseAdmin .from("pages") diff --git a/apps/web/pages/api/emails/subscribers/index.ts b/apps/web/pages/api/emails/subscribers/index.ts index 3fd9e52..0bbd4e7 100644 --- a/apps/web/pages/api/emails/subscribers/index.ts +++ b/apps/web/pages/api/emails/subscribers/index.ts @@ -1,6 +1,6 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; const getEmailSubscribers = async ( req: NextApiRequest, @@ -8,9 +8,9 @@ const getEmailSubscribers = async ( ) => { if (req.method === "GET") { try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); - const { page_id } = req.query; + const page_id = String(req.query.page_id); await supabaseAdmin .from("pages") diff --git a/apps/web/pages/api/pages/new.ts b/apps/web/pages/api/pages/new.ts index ee05531..7fbb8cd 100644 --- a/apps/web/pages/api/pages/new.ts +++ b/apps/web/pages/api/pages/new.ts @@ -3,7 +3,7 @@ import { IPage } from "@changes-page/supabase/types/page"; import type { NextApiRequest, NextApiResponse } from "next"; import { NewPageSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createPage, getUserById, @@ -20,7 +20,7 @@ const createNewPage = async ( const { url_slug, title, description, type } = req.body; try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { has_active_subscription } = await getUserById(user.id); if (!has_active_subscription) { diff --git a/apps/web/pages/api/pages/reactions.ts b/apps/web/pages/api/pages/reactions.ts index 6ec582d..8ea5b03 100644 --- a/apps/web/pages/api/pages/reactions.ts +++ b/apps/web/pages/api/pages/reactions.ts @@ -1,6 +1,6 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; export default async function getPostReactions( req: NextApiRequest, @@ -9,7 +9,7 @@ export default async function getPostReactions( let { post_id } = req.query; try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); if (!user) { return res.status(401).json({ ok: false, diff --git a/apps/web/pages/api/pages/settings/add-domain.ts b/apps/web/pages/api/pages/settings/add-domain.ts index c1b688c..fdd4934 100644 --- a/apps/web/pages/api/pages/settings/add-domain.ts +++ b/apps/web/pages/api/pages/settings/add-domain.ts @@ -1,10 +1,10 @@ import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; async function addDomain(req: NextApiRequest, res: NextApiResponse) { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { domain } = req.body; diff --git a/apps/web/pages/api/pages/settings/check-domain.ts b/apps/web/pages/api/pages/settings/check-domain.ts index 18db62f..e139a02 100644 --- a/apps/web/pages/api/pages/settings/check-domain.ts +++ b/apps/web/pages/api/pages/settings/check-domain.ts @@ -1,10 +1,10 @@ import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; async function checkDomain(req: NextApiRequest, res: NextApiResponse) { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { domain } = req.query; diff --git a/apps/web/pages/api/pages/settings/index.ts b/apps/web/pages/api/pages/settings/index.ts index 72ba978..a4e132a 100644 --- a/apps/web/pages/api/pages/settings/index.ts +++ b/apps/web/pages/api/pages/settings/index.ts @@ -1,7 +1,7 @@ import { IErrorResponse } from "@changes-page/supabase/types/api"; import { IPageSettings } from "@changes-page/supabase/types/page"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; const getPageSettings = async ( @@ -12,7 +12,7 @@ const getPageSettings = async ( const { page_id } = req.query; try { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); console.log("getPageSettings", user?.id); diff --git a/apps/web/pages/api/pages/settings/remove-domain.ts b/apps/web/pages/api/pages/settings/remove-domain.ts index 137d9cb..5a56d46 100644 --- a/apps/web/pages/api/pages/settings/remove-domain.ts +++ b/apps/web/pages/api/pages/settings/remove-domain.ts @@ -1,8 +1,8 @@ import { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; async function removeDomain(req: NextApiRequest, res: NextApiResponse) { - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { domain } = req.body; diff --git a/apps/web/pages/api/posts/index.ts b/apps/web/pages/api/posts/index.ts index 1c31ed0..2534f16 100644 --- a/apps/web/pages/api/posts/index.ts +++ b/apps/web/pages/api/posts/index.ts @@ -2,7 +2,7 @@ import { PostStatus } from "@changes-page/supabase/types/page"; import { NextApiRequest, NextApiResponse } from "next"; import { NewPostSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createPost } from "../../../utils/useDatabase"; const createNewPost = async (req: NextApiRequest, res: NextApiResponse) => { @@ -24,7 +24,10 @@ const createNewPost = async (req: NextApiRequest, res: NextApiResponse) => { try { await apiRateLimiter(req, res); - const { user, supabase } = await getSupabaseServerClient({ req, res }); + const { user, supabase } = await getSupabaseServerClientForAPI({ + req, + res, + }); const isValid = await NewPostSchema.isValid({ page_id, diff --git a/apps/web/pages/api/teams/invite/accept/index.ts b/apps/web/pages/api/teams/invite/accept/index.ts index 0b435c5..f872f37 100644 --- a/apps/web/pages/api/teams/invite/accept/index.ts +++ b/apps/web/pages/api/teams/invite/accept/index.ts @@ -1,7 +1,7 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../../utils/supabase/supabase-admin"; const acceptInvite = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "POST") { @@ -15,7 +15,10 @@ const acceptInvite = async (req: NextApiRequest, res: NextApiResponse) => { try { await apiRateLimiter(req, res); - const { user, supabase } = await getSupabaseServerClient({ req, res }); + const { user, supabase } = await getSupabaseServerClientForAPI({ + req, + res, + }); if (!user) { return res.status(401).json({ error: { statusCode: 401, message: "Unauthorized" }, diff --git a/apps/web/pages/api/teams/invite/index.ts b/apps/web/pages/api/teams/invite/index.ts index 1e4c52b..55bafb1 100644 --- a/apps/web/pages/api/teams/invite/index.ts +++ b/apps/web/pages/api/teams/invite/index.ts @@ -4,7 +4,7 @@ import { ROUTES } from "../../../../data/routes.data"; import { getAppBaseURL } from "../../../../utils/helpers"; import inngestClient from "../../../../utils/inngest"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../../utils/useDatabase"; const inviteUser = async (req: NextApiRequest, res: NextApiResponse) => { @@ -19,7 +19,10 @@ const inviteUser = async (req: NextApiRequest, res: NextApiResponse) => { try { await apiRateLimiter(req, res); - const { user, supabase } = await getSupabaseServerClient({ req, res }); + const { user, supabase } = await getSupabaseServerClientForAPI({ + req, + res, + }); if (!user) { return res.status(401).json({ error: { statusCode: 401, message: "Unauthorized" }, diff --git a/apps/web/pages/api/teams/member/[id]/index.ts b/apps/web/pages/api/teams/member/[id]/index.ts index 47091ac..74e6e16 100644 --- a/apps/web/pages/api/teams/member/[id]/index.ts +++ b/apps/web/pages/api/teams/member/[id]/index.ts @@ -1,7 +1,7 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../../utils/rate-limit"; -import { getSupabaseServerClient } from "../../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForAPI } from "../../../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../../../utils/useDatabase"; const getTeamMemberDetails = async ( @@ -19,7 +19,7 @@ const getTeamMemberDetails = async ( try { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClient({ req, res }); + const { user } = await getSupabaseServerClientForAPI({ req, res }); const { has_active_subscription } = await getUserById(user.id); if (!has_active_subscription) { @@ -31,7 +31,7 @@ const getTeamMemberDetails = async ( const { data: teamMember, error: teamMemberError } = await supabaseAdmin .from("team_members") .select("*") - .eq("id", id) + .eq("id", String(id)) .single(); if (teamMemberError) { diff --git a/apps/web/pages/integrations/zapier.tsx b/apps/web/pages/integrations/zapier.tsx index 6bb94f6..4cc0135 100644 --- a/apps/web/pages/integrations/zapier.tsx +++ b/apps/web/pages/integrations/zapier.tsx @@ -3,10 +3,10 @@ import Head from "next/head"; import FooterComponent from "../../components/layout/footer.component"; import HeaderComponent from "../../components/layout/header.component"; import Page from "../../components/layout/page.component"; -import { getSupabaseServerClient } from "../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../utils/supabase/supabase-admin"; export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { user } = await getSupabaseServerClient(ctx); + const { user } = await getSupabaseServerClientForSSR(ctx); return { props: { email: user?.email }, diff --git a/apps/web/pages/onboarding/open-page.tsx b/apps/web/pages/onboarding/open-page.tsx index dda1824..83b4b16 100644 --- a/apps/web/pages/onboarding/open-page.tsx +++ b/apps/web/pages/onboarding/open-page.tsx @@ -2,12 +2,12 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { SpinnerWithSpacing } from "@changes-page/ui"; import { GetServerSideProps, GetServerSidePropsContext } from "next"; import { ROUTES } from "../../data/routes.data"; -import { getSupabaseServerClient } from "../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../utils/supabase/supabase-admin"; export const getServerSideProps: GetServerSideProps = async ( ctx: GetServerSidePropsContext ) => { - const { user } = await getSupabaseServerClient(ctx); + const { user } = await getSupabaseServerClientForSSR(ctx); if (!user) { return { diff --git a/apps/web/pages/pages/[page_id]/[post_id].tsx b/apps/web/pages/pages/[page_id]/[post_id].tsx index 24891d6..9708aec 100644 --- a/apps/web/pages/pages/[page_id]/[post_id].tsx +++ b/apps/web/pages/pages/[page_id]/[post_id].tsx @@ -1,5 +1,5 @@ import { IPost, PostStatus } from "@changes-page/supabase/types/page"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useState, type JSX } from "react"; import { InferType } from "yup"; @@ -11,14 +11,14 @@ import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; import { NewPostSchema } from "../../../data/schema"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../utils/useDatabase"; import { useUserData } from "../../../utils/useUser"; -export async function getServerSideProps({ params, req, res }) { - const { page_id, post_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const { page_id, post_id } = ctx.params; - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const settings = await createOrRetrievePageSettings(String(page_id)); const { data: post } = await supabase .from("posts") diff --git a/apps/web/pages/pages/[page_id]/analytics.tsx b/apps/web/pages/pages/[page_id]/analytics.tsx index a3d40ff..785a9b2 100644 --- a/apps/web/pages/pages/[page_id]/analytics.tsx +++ b/apps/web/pages/pages/[page_id]/analytics.tsx @@ -1,11 +1,11 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { SpinnerWithSpacing } from "@changes-page/ui"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import Image from "next/image"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; import { getPageAnalytics } from "../../../utils/useDatabase"; import { getPage } from "../../../utils/useSSR"; @@ -183,10 +183,11 @@ const StatsTable = ({ data = [], title, total = 5 }) => { ); }; -export async function getServerSideProps({ req, res, query }) { - const { page_id, range } = query; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const { range } = ctx.query; + const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id); const rangeNum = Number(range) || 7; @@ -202,18 +203,21 @@ export async function getServerSideProps({ req, res, query }) { .eq("page_id", page_id) .gte("created_at", prevDate.toISOString()); - const currentPeriodViews = allPageViews?.filter( - (view) => new Date(view.created_at) >= date - ) || []; - - const previousPeriodViews = allPageViews?.filter( - (view) => new Date(view.created_at) >= prevDate && new Date(view.created_at) < date - ) || []; + const currentPeriodViews = + allPageViews?.filter((view) => new Date(view.created_at) >= date) || []; + + const previousPeriodViews = + allPageViews?.filter( + (view) => + new Date(view.created_at) >= prevDate && + new Date(view.created_at) < date + ) || []; const page_views = currentPeriodViews.length; - const visitors = new Set(currentPeriodViews.map(v => v.visitor_id)).size; + const visitors = new Set(currentPeriodViews.map((v) => v.visitor_id)).size; const prev_page_views = previousPeriodViews.length; - const prev_visitors = new Set(previousPeriodViews.map(v => v.visitor_id)).size; + const prev_visitors = new Set(previousPeriodViews.map((v) => v.visitor_id)) + .size; // Calculate growth rates const pageViewsGrowth = diff --git a/apps/web/pages/pages/[page_id]/audit-logs.tsx b/apps/web/pages/pages/[page_id]/audit-logs.tsx index 4473332..274f678 100644 --- a/apps/web/pages/pages/[page_id]/audit-logs.tsx +++ b/apps/web/pages/pages/[page_id]/audit-logs.tsx @@ -8,17 +8,17 @@ import { TrashIcon, UserIcon, } from "@heroicons/react/solid"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; import { getPage } from "../../../utils/useSSR"; -export async function getServerSideProps({ req, res, query }) { - const { page_id } = query; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id); const { data: audit_logs } = await supabaseAdmin .from("page_audit_logs") diff --git a/apps/web/pages/pages/[page_id]/edit.tsx b/apps/web/pages/pages/[page_id]/edit.tsx index ff8d7b2..8315504 100644 --- a/apps/web/pages/pages/[page_id]/edit.tsx +++ b/apps/web/pages/pages/[page_id]/edit.tsx @@ -1,5 +1,5 @@ import { IPage } from "@changes-page/supabase/types/page"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useState } from "react"; import { InferType } from "yup"; @@ -12,15 +12,15 @@ import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; import { NewPageSchema } from "../../../data/schema"; import { httpPost } from "../../../utils/http"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; import { getPage } from "../../../utils/useSSR"; import { useUserData } from "../../../utils/useUser"; -export async function getServerSideProps({ req, res, params }) { - const { page_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const { page_id } = ctx.params; - const { supabase } = await getSupabaseServerClient({ req, res }); - const page = await getPage(supabase, params.page_id as string); + const { supabase } = await getSupabaseServerClientForSSR(ctx); + const page = await getPage(supabase, page_id as string); return { props: { page_id, page }, diff --git a/apps/web/pages/pages/[page_id]/index.tsx b/apps/web/pages/pages/[page_id]/index.tsx index 11c35b1..c81d869 100644 --- a/apps/web/pages/pages/[page_id]/index.tsx +++ b/apps/web/pages/pages/[page_id]/index.tsx @@ -15,7 +15,7 @@ import { XCircleIcon, } from "@heroicons/react/solid"; import classNames from "classnames"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import Script from "next/script"; import { useMemo, useState, type JSX } from "react"; @@ -41,15 +41,15 @@ import { ROUTES } from "../../../data/routes.data"; import usePageSettings from "../../../utils/hooks/usePageSettings"; import usePageUrl from "../../../utils/hooks/usePageUrl"; import usePosts from "../../../utils/hooks/usePosts"; -import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../utils/useDatabase"; import { getPage } from "../../../utils/useSSR"; import { useUserData } from "../../../utils/useUser"; -export async function getServerSideProps({ req, res, params }) { - const { page_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; diff --git a/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx b/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx index 8fa097b..952f81a 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx @@ -1,21 +1,19 @@ -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { type JSX } from "react"; import { SecondaryButton } from "../../../../components/core/buttons.component"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; import RoadmapBoard from "../../../../components/roadmap/RoadmapBoard"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; import { getPage } from "../../../../utils/useSSR"; -export async function getServerSideProps({ req, res, params }) { - const { page_id, board_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); + const board_id = String(ctx.params?.board_id); - const { supabase } = await getSupabaseServerClient({ - req, - res, - }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; diff --git a/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx b/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx index 0de6d04..a0c8c83 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx @@ -5,23 +5,24 @@ import { } from "@changes-page/utils"; import { MenuIcon } from "@heroicons/react/outline"; import { PencilIcon, PlusIcon, TrashIcon } from "@heroicons/react/solid"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useEffect, useMemo, useState, type JSX } from "react"; import AuthLayout from "../../../../../components/layout/auth-layout.component"; import Page from "../../../../../components/layout/page.component"; import usePageSettings from "../../../../../utils/hooks/usePageSettings"; import { getPageUrl } from "../../../../../utils/hooks/usePageUrl"; -import { getSupabaseServerClient } from "../../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../../../utils/useDatabase"; import { getPage } from "../../../../../utils/useSSR"; import { useUserData } from "../../../../../utils/useUser"; -export async function getServerSideProps({ req, res, params, query }) { - const { page_id, board_id } = params; - const { tab = "board" } = query; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); + const board_id = String(ctx.params?.board_id); + const { tab = "board" } = ctx.query; - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; diff --git a/apps/web/pages/pages/[page_id]/roadmap/index.tsx b/apps/web/pages/pages/[page_id]/roadmap/index.tsx index 1855749..6be9eb4 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/index.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/index.tsx @@ -1,18 +1,18 @@ import { PlusIcon } from "@heroicons/react/solid"; -import { InferGetServerSidePropsType } from "next"; -import { useMemo, type JSX } from "react"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import Link from "next/link"; +import { useMemo, type JSX } from "react"; import { PrimaryRouterButton } from "../../../../components/core/buttons.component"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; import { ROUTES } from "../../../../data/routes.data"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; import { getPage } from "../../../../utils/useSSR"; -export async function getServerSideProps({ req, res, params }) { - const { page_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -132,7 +132,7 @@ export default function RoadmapPage({ {board.is_public ? "Public" : "Private"} - + {board.description && (

{board.description} @@ -142,17 +142,30 @@ export default function RoadmapPage({

-

Created

+

+ Created +

- {new Date(board.created_at).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - })} + {new Date(board.created_at).toLocaleDateString( + "en-US", + { + year: "numeric", + month: "short", + day: "numeric", + } + )}

- - + +
diff --git a/apps/web/pages/pages/[page_id]/roadmap/new.tsx b/apps/web/pages/pages/[page_id]/roadmap/new.tsx index f9a5e79..d49c8e1 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/new.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/new.tsx @@ -1,18 +1,16 @@ -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; -import { useMemo, useState, type JSX } from "react"; +import { useState, type JSX } from "react"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; -import usePageSettings from "../../../../utils/hooks/usePageSettings"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; -import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; +import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; import { getPage } from "../../../../utils/useSSR"; import { useUserData } from "../../../../utils/useUser"; -export async function getServerSideProps({ req, res, params }) { - const { page_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -24,30 +22,19 @@ export async function getServerSideProps({ req, res, params }) { }; } - const settings = await createOrRetrievePageSettings(String(page_id)); - return { props: { page_id, page, - settings, }, }; } export default function NewRoadmapBoard({ - page, page_id, - settings: serverSettings, }: InferGetServerSidePropsType) { const router = useRouter(); - const { supabase, user } = useUserData(); - const { settings: clientSettings } = usePageSettings(page_id, false); - - const settings = useMemo( - () => clientSettings ?? serverSettings, - [serverSettings, clientSettings] - ); + const { supabase } = useUserData(); const [formData, setFormData] = useState({ title: "", diff --git a/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx b/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx index 0e4cb0a..511bb7e 100644 --- a/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx +++ b/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx @@ -1,5 +1,5 @@ import { SpinnerWithSpacing } from "@changes-page/ui"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import dynamic from "next/dynamic"; import { useMemo } from "react"; import AuthLayout from "../../../../components/layout/auth-layout.component"; @@ -10,7 +10,7 @@ import SocialLinksSettings from "../../../../components/page-settings/social-lin import StyleSettings from "../../../../components/page-settings/style"; import { ROUTES } from "../../../../data/routes.data"; import usePageSettings from "../../../../utils/hooks/usePageSettings"; -import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; import { getPage } from "../../../../utils/useSSR"; @@ -21,10 +21,10 @@ const IntegrationsSettings = dynamic( } ); -export async function getServerSideProps({ req, res, params }) { - const { page_id } = params; +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClient({ req, res }); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id); const settings = await createOrRetrievePageSettings(String(page_id)); @@ -33,7 +33,7 @@ export async function getServerSideProps({ req, res, params }) { page, settings, page_id, - activeTab: params.activeTab, + activeTab: ctx.params?.activeTab, }, }; } diff --git a/apps/web/pages/pages/index.tsx b/apps/web/pages/pages/index.tsx index 286274c..7825a2e 100644 --- a/apps/web/pages/pages/index.tsx +++ b/apps/web/pages/pages/index.tsx @@ -14,11 +14,11 @@ import Page from "../../components/layout/page.component"; import Changelog from "../../components/marketing/changelog"; import { ROUTES } from "../../data/routes.data"; import { getAppBaseURL } from "../../utils/helpers"; -import { getSupabaseServerClient } from "../../utils/supabase/supabase-admin"; +import { getSupabaseServerClientForSSR } from "../../utils/supabase/supabase-admin"; import { useUserData } from "../../utils/useUser"; export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { supabase } = await getSupabaseServerClient(ctx); + const { supabase } = await getSupabaseServerClientForSSR(ctx); const { data: pages, error } = await supabase .from("pages") diff --git a/apps/web/utils/supabase/server.ts b/apps/web/utils/supabase/server.ts index f707f90..1596bb4 100644 --- a/apps/web/utils/supabase/server.ts +++ b/apps/web/utils/supabase/server.ts @@ -1,49 +1,64 @@ import { Database } from "@changes-page/supabase/types"; -import { createServerClient } from "@supabase/ssr"; +import { createServerClient, serializeCookieHeader } from "@supabase/ssr"; +import { + GetServerSidePropsContext, + NextApiRequest, + NextApiResponse, +} from "next/types"; -export function createServerClientSSR(context: { req: any; res: any }) { +export function createServerClientSSR({ req, res }: GetServerSidePropsContext) { return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { - return Object.keys(context.req.cookies).map((name) => ({ + return Object.keys(req.cookies).map((name) => ({ name, - value: context.req.cookies[name], + value: req.cookies[name] || "", })); }, setAll(cookiesToSet) { - const cookieStrings = cookiesToSet.map(({ name, value, options }) => { - const cookieOptions = { - path: "/", - httpOnly: false, // Allow client-side access for supabase-js - secure: process.env.NODE_ENV === "production", - sameSite: "lax" as const, - ...options, - }; - - return `${name}=${value}; Path=${cookieOptions.path}; ${ - cookieOptions.httpOnly ? "HttpOnly;" : "" - } ${cookieOptions.secure ? "Secure;" : ""} SameSite=${ - cookieOptions.sameSite - };${ - cookieOptions.maxAge ? ` Max-Age=${cookieOptions.maxAge};` : "" - }`; - }); - - // Get existing cookies and add the new ones - const existingCookies = context.res.getHeader("Set-Cookie") || []; - const existingCookieArray = Array.isArray(existingCookies) - ? existingCookies - : [existingCookies].filter(Boolean); + res.setHeader( + "Set-Cookie", + cookiesToSet.map(({ name, value, options }) => + serializeCookieHeader(name, value, options) + ) + ); + }, + }, + } + ); +} - context.res.setHeader("Set-Cookie", [ - ...existingCookieArray, - ...cookieStrings, - ]); +export function createServerClientForAPI({ + req, + res, +}: { + req: NextApiRequest; + res: NextApiResponse; +}) { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, + { + cookies: { + getAll() { + return Object.keys(req.cookies).map((name) => ({ + name, + value: req.cookies[name] || "", + })); + }, + setAll(cookiesToSet) { + res.setHeader( + "Set-Cookie", + cookiesToSet.map(({ name, value, options }) => + serializeCookieHeader(name, value, options) + ) + ); }, }, } ); + return supabase; } diff --git a/apps/web/utils/supabase/static.ts b/apps/web/utils/supabase/static.ts new file mode 100644 index 0000000..d162f56 --- /dev/null +++ b/apps/web/utils/supabase/static.ts @@ -0,0 +1,10 @@ +import { createClient as createClientPrimitive } from "@supabase/supabase-js"; + +export function createClient() { + const supabase = createClientPrimitive( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! + ); + + return supabase; +} diff --git a/apps/web/utils/supabase/supabase-admin.ts b/apps/web/utils/supabase/supabase-admin.ts index a63f3d5..ce9e710 100644 --- a/apps/web/utils/supabase/supabase-admin.ts +++ b/apps/web/utils/supabase/supabase-admin.ts @@ -5,15 +5,10 @@ import { NextApiRequest, NextApiResponse, } from "next"; -import { createServerClientSSR } from "./server"; - -export const getSupabaseServerClient = async ( - context: - | GetServerSidePropsContext - | { - req: NextApiRequest; - res: NextApiResponse; - } +import { createServerClientForAPI, createServerClientSSR } from "./server"; + +export const getSupabaseServerClientForSSR = async ( + context: GetServerSidePropsContext ): Promise<{ supabase: SupabaseClient; session: Session | null; @@ -40,3 +35,33 @@ export const getSupabaseServerClient = async ( user, }; }; + +export const getSupabaseServerClientForAPI = async (context: { + req: NextApiRequest; + res: NextApiResponse; +}): Promise<{ + supabase: SupabaseClient; + session: Session | null; + user: User | null; +}> => { + const supabase = createServerClientForAPI(context); + + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (error) { + console.error("Error getting user:", error); + } + + const { + data: { session }, + } = await supabase.auth.getSession(); + + return { + supabase, + session, + user, + }; +}; From 513474b4ed44823bc03483e3c5e949658c8810cc Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 7 Sep 2025 17:14:23 +1000 Subject: [PATCH 04/10] Update callback --- apps/web/pages/api/auth/callback.ts | 46 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/web/pages/api/auth/callback.ts b/apps/web/pages/api/auth/callback.ts index b73e49c..70164a9 100644 --- a/apps/web/pages/api/auth/callback.ts +++ b/apps/web/pages/api/auth/callback.ts @@ -6,31 +6,33 @@ const callback: NextApiHandler = async (req, res) => { const code = req.query.code; const redirectedFrom = req.query.redirectedFrom; - if (typeof code === "string") { - const supabase = createServerClientForAPI({ req, res }); - - try { - const { data, error } = await supabase.auth.exchangeCodeForSession(code); - - if (error) { - console.error("Auth callback error:", error); - return res.redirect( - `/login?error=${encodeURIComponent(error.message)}` - ); - } - - if (!data.session) { - console.error("Auth callback: No session created"); - return res.redirect( - `/login?error=${encodeURIComponent("No session created")}` - ); - } - } catch (err) { - console.error("Auth callback exception:", err); + if (typeof code !== "string") { + return res.redirect( + `/login?error=${encodeURIComponent("Missing or invalid code")}` + ); + } + + const supabase = createServerClientForAPI({ req, res }); + + try { + const { data, error } = await supabase.auth.exchangeCodeForSession(code); + + if (error) { + console.error("Auth callback error:", error); + return res.redirect(`/login?error=${encodeURIComponent(error.message)}`); + } + + if (!data.session) { + console.error("Auth callback: No session created"); return res.redirect( - `/login?error=${encodeURIComponent("Authentication failed")}` + `/login?error=${encodeURIComponent("No session created")}` ); } + } catch (err) { + console.error("Auth callback exception:", err); + return res.redirect( + `/login?error=${encodeURIComponent("Authentication failed")}` + ); } let redirectTo = ROUTES.PAGES; From b62275ed24bfa4601d38e1779df9d8716748765f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 7 Sep 2025 22:28:55 +1000 Subject: [PATCH 05/10] Update Supabase clients --- apps/web/pages/api/ai/get-streaming-url.ts | 14 +- apps/web/pages/api/ai/suggest-title.ts | 13 +- apps/web/pages/api/auth/callback.ts | 12 +- .../api/billing/create-billing-portal.ts | 60 ++++--- .../api/billing/enable-email-notifications.ts | 104 ++++++------ apps/web/pages/api/billing/index.ts | 13 +- .../pages/api/billing/redirect-to-checkout.ts | 18 +-- .../api/emails/subscribers/export-csv.ts | 11 +- .../web/pages/api/emails/subscribers/index.ts | 64 ++++---- apps/web/pages/api/pages/new.ts | 13 +- apps/web/pages/api/pages/reactions.ts | 76 ++++----- .../pages/api/pages/settings/add-domain.ts | 8 +- .../pages/api/pages/settings/check-domain.ts | 8 +- apps/web/pages/api/pages/settings/index.ts | 13 +- .../pages/api/pages/settings/remove-domain.ts | 41 +++-- apps/web/pages/api/posts/index.ts | 12 +- .../pages/api/teams/invite/accept/index.ts | 17 +- apps/web/pages/api/teams/invite/index.ts | 149 ++++++++---------- apps/web/pages/api/teams/member/[id]/index.ts | 12 +- apps/web/pages/integrations/zapier.tsx | 17 +- apps/web/pages/onboarding/open-page.tsx | 16 +- apps/web/pages/pages/[page_id]/[post_id].tsx | 9 +- apps/web/pages/pages/[page_id]/analytics.tsx | 11 +- apps/web/pages/pages/[page_id]/audit-logs.tsx | 9 +- apps/web/pages/pages/[page_id]/edit.tsx | 9 +- apps/web/pages/pages/[page_id]/index.tsx | 9 +- .../pages/[page_id]/roadmap/[board_id].tsx | 9 +- .../[page_id]/roadmap/[board_id]/settings.tsx | 9 +- .../pages/pages/[page_id]/roadmap/index.tsx | 9 +- .../web/pages/pages/[page_id]/roadmap/new.tsx | 9 +- .../pages/[page_id]/settings/[activeTab].tsx | 9 +- apps/web/pages/pages/index.tsx | 10 +- apps/web/utils/supabase/server.ts | 4 +- apps/web/utils/supabase/static.ts | 2 +- apps/web/utils/supabase/supabase-admin.ts | 67 -------- apps/web/utils/supabase/withSupabase.ts | 26 +++ apps/web/utils/withAuth.ts | 40 +++++ 37 files changed, 404 insertions(+), 528 deletions(-) delete mode 100644 apps/web/utils/supabase/supabase-admin.ts create mode 100644 apps/web/utils/supabase/withSupabase.ts create mode 100644 apps/web/utils/withAuth.ts diff --git a/apps/web/pages/api/ai/get-streaming-url.ts b/apps/web/pages/api/ai/get-streaming-url.ts index 25e9d85..63b6948 100644 --- a/apps/web/pages/api/ai/get-streaming-url.ts +++ b/apps/web/pages/api/ai/get-streaming-url.ts @@ -1,17 +1,11 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; -import type { NextApiRequest, NextApiResponse } from "next"; import { createSignedStreamingUrl } from "../../../utils/manageprompt"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../utils/withAuth"; -const expandConcept = async ( - req: NextApiRequest, - res: NextApiResponse<{ url: string } | IErrorResponse> -) => { +const expandConcept = withAuth<{ url: string }>(async (req, res) => { if (req.method === "POST") { const { workflowId } = req.body; - try { - await getSupabaseServerClientForAPI({ req, res }); + try { const url = await createSignedStreamingUrl(workflowId); return res.status(200).json({ url }); @@ -25,6 +19,6 @@ const expandConcept = async ( res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } -}; +}); export default expandConcept; diff --git a/apps/web/pages/api/ai/suggest-title.ts b/apps/web/pages/api/ai/suggest-title.ts index 64ba3fd..109132c 100644 --- a/apps/web/pages/api/ai/suggest-title.ts +++ b/apps/web/pages/api/ai/suggest-title.ts @@ -1,18 +1,11 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; -import type { NextApiRequest, NextApiResponse } from "next"; import { runWorkflow } from "../../../utils/manageprompt"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../utils/withAuth"; -const suggestTitle = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const suggestTitle = withAuth(async (req, res) => { if (req.method === "POST") { const { content } = req.body; try { - await getSupabaseServerClientForAPI({ req, res }); - const result = await runWorkflow("wf_e1eb79b1dc017ca189506d799453caae", { content, }); @@ -30,6 +23,6 @@ const suggestTitle = async ( res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } -}; +}); export default suggestTitle; diff --git a/apps/web/pages/api/auth/callback.ts b/apps/web/pages/api/auth/callback.ts index 70164a9..ebb4091 100644 --- a/apps/web/pages/api/auth/callback.ts +++ b/apps/web/pages/api/auth/callback.ts @@ -35,15 +35,11 @@ const callback: NextApiHandler = async (req, res) => { ); } - let redirectTo = ROUTES.PAGES; - if ( + const redirectTo = typeof redirectedFrom === "string" && - redirectedFrom.startsWith("/") && - !redirectedFrom.includes("://") && - !redirectedFrom.includes("\\") - ) { - redirectTo = redirectedFrom; - } + Object.values(ROUTES).includes(redirectedFrom) + ? redirectedFrom + : ROUTES.PAGES; res.redirect(redirectTo); }; diff --git a/apps/web/pages/api/billing/create-billing-portal.ts b/apps/web/pages/api/billing/create-billing-portal.ts index e52aec3..2fdb8a1 100644 --- a/apps/web/pages/api/billing/create-billing-portal.ts +++ b/apps/web/pages/api/billing/create-billing-portal.ts @@ -1,45 +1,41 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; -import type { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createOrRetrieveCustomer } from "../../../utils/useDatabase"; +import { withAuth } from "../../../utils/withAuth"; import { getAppBaseURL } from "./../../../utils/helpers"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); -const createBillingSession = async ( - req: NextApiRequest, - res: NextApiResponse<{ url: string } | IErrorResponse> -) => { - if (req.method === "POST") { - await apiRateLimiter(req, res); - const { return_url } = req.body; +const createBillingSession = withAuth<{ url: string }>( + async (req, res, { user }) => { + if (req.method === "POST") { + await apiRateLimiter(req, res); + const { return_url } = req.body; - try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - const customer = await createOrRetrieveCustomer(user.id, user.email); + try { + const customer = await createOrRetrieveCustomer(user.id, user.email); - console.log( - "createBillingSession", - user?.id, - "create billing session for existing user" - ); + console.log( + "createBillingSession", + user?.id, + "create billing session for existing user" + ); - const { url } = await stripe.billingPortal.sessions.create({ - customer, - return_url: return_url || `${getAppBaseURL()}/account/billing`, - }); + const { url } = await stripe.billingPortal.sessions.create({ + customer, + return_url: return_url || `${getAppBaseURL()}/account/billing`, + }); - return res.status(201).json({ url }); - } catch (err) { - console.log("createBillingSession", err); - res - .status(500) - .json({ error: { statusCode: 500, message: err.message } }); + return res.status(201).json({ url }); + } catch (err) { + console.log("createBillingSession", err); + res + .status(500) + .json({ error: { statusCode: 500, message: err.message } }); + } + } else { + res.setHeader("Allow", "POST"); + res.status(405).end("Method Not Allowed"); } - } else { - res.setHeader("Allow", "POST"); - res.status(405).end("Method Not Allowed"); } -}; +); export default createBillingSession; diff --git a/apps/web/pages/api/billing/enable-email-notifications.ts b/apps/web/pages/api/billing/enable-email-notifications.ts index 1e22171..4504ddb 100644 --- a/apps/web/pages/api/billing/enable-email-notifications.ts +++ b/apps/web/pages/api/billing/enable-email-notifications.ts @@ -1,67 +1,61 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; -import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../utils/useDatabase"; +import { withAuth } from "../../../utils/withAuth"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); -const enableEmailNotifications = async ( - req: NextApiRequest, - res: NextApiResponse<{ status: string } | IErrorResponse> -) => { - if (req.method === "PUT") { - try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - - const { stripe_subscription_id, stripe_subscription } = await getUserById( - user.id - ); - - if ( - !stripe_subscription || - (stripe_subscription as unknown as Stripe.Subscription)?.status === - "canceled" - ) { - return res.status(400).json({ - error: { - statusCode: 400, - message: - "You have canceled your subscription. Please reactivate it to continue using this feature.", - }, +const enableEmailNotifications = withAuth<{ status: string }>( + async (req, res, { user }) => { + if (req.method === "PUT") { + try { + const { stripe_subscription_id, stripe_subscription } = + await getUserById(user.id); + + if ( + !stripe_subscription || + (stripe_subscription as unknown as Stripe.Subscription)?.status === + "canceled" + ) { + return res.status(400).json({ + error: { + statusCode: 400, + message: + "You have canceled your subscription. Please reactivate it to continue using this feature.", + }, + }); + } + + const subscription = await stripe.subscriptions.retrieve( + stripe_subscription_id + ); + + // Ignore if already added to subscription + if ( + subscription.items.data.find( + (item) => + item.price.id === process.env.EMAIL_NOTIFICATION_STRIPE_PRICE_ID + ) + ) { + return res.status(200).json({ status: "ok" }); + } + + await stripe.subscriptionItems.create({ + subscription: stripe_subscription_id, + price: process.env.EMAIL_NOTIFICATION_STRIPE_PRICE_ID, }); - } - const subscription = await stripe.subscriptions.retrieve( - stripe_subscription_id - ); - - // Ignore if already added to subscription - if ( - subscription.items.data.find( - (item) => - item.price.id === process.env.EMAIL_NOTIFICATION_STRIPE_PRICE_ID - ) - ) { - return res.status(200).json({ status: "ok" }); + return res.status(201).json({ status: "ok" }); + } catch (err) { + console.log("createBillingSession", err); + res + .status(500) + .json({ error: { statusCode: 500, message: err.message } }); } - - await stripe.subscriptionItems.create({ - subscription: stripe_subscription_id, - price: process.env.EMAIL_NOTIFICATION_STRIPE_PRICE_ID, - }); - - return res.status(201).json({ status: "ok" }); - } catch (err) { - console.log("createBillingSession", err); - res - .status(500) - .json({ error: { statusCode: 500, message: err.message } }); + } else { + res.setHeader("Allow", "PUT"); + res.status(405).end("Method Not Allowed"); } - } else { - res.setHeader("Allow", "PUT"); - res.status(405).end("Method Not Allowed"); } -}; +); export default enableEmailNotifications; diff --git a/apps/web/pages/api/billing/index.ts b/apps/web/pages/api/billing/index.ts index 2a9a2a3..353d5ba 100644 --- a/apps/web/pages/api/billing/index.ts +++ b/apps/web/pages/api/billing/index.ts @@ -1,20 +1,13 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; -import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; import { IBillingInfo } from "../../../data/user.interface"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../utils/useDatabase"; +import { withAuth } from "../../../utils/withAuth"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); -const getBillingStatus = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const getBillingStatus = withAuth(async (req, res, { user }) => { if (req.method === "GET") { try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - const { pro_gifted, stripe_customer_id, @@ -143,6 +136,6 @@ const getBillingStatus = async ( res.setHeader("Allow", "GET"); res.status(405).end("Method Not Allowed"); } -}; +}); export default getBillingStatus; diff --git a/apps/web/pages/api/billing/redirect-to-checkout.ts b/apps/web/pages/api/billing/redirect-to-checkout.ts index 81651f9..5fac6ae 100644 --- a/apps/web/pages/api/billing/redirect-to-checkout.ts +++ b/apps/web/pages/api/billing/redirect-to-checkout.ts @@ -1,25 +1,19 @@ -import type { NextApiRequest, NextApiResponse } from "next"; import { getAppBaseURL } from "../../../utils/helpers"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createOrRetrieveCustomer, getUserById, } from "../../../utils/useDatabase"; +import { withAuth } from "../../../utils/withAuth"; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); -const redirectToCheckout = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const redirectToCheckout = withAuth(async (req, res, { user }) => { if (req.method === "GET") { await apiRateLimiter(req, res); const { return_url } = req.query; try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - const { stripe_customer_id, stripe_subscription, @@ -44,7 +38,8 @@ const redirectToCheckout = async ( return_url: return_url || `${getAppBaseURL()}/pages`, }); - return res.redirect(307, url); + res.redirect(307, url); + return; } console.log( @@ -84,7 +79,8 @@ const redirectToCheckout = async ( cancel_url: return_url || getAppBaseURL(), }); - return res.redirect(307, url); + res.redirect(307, url); + return; } catch (err) { console.log("createCheckout", err); res @@ -95,6 +91,6 @@ const redirectToCheckout = async ( res.setHeader("Allow", "GET"); res.status(405).end("Method Not Allowed"); } -}; +}); export default redirectToCheckout; diff --git a/apps/web/pages/api/emails/subscribers/export-csv.ts b/apps/web/pages/api/emails/subscribers/export-csv.ts index ee2d493..b265b7f 100644 --- a/apps/web/pages/api/emails/subscribers/export-csv.ts +++ b/apps/web/pages/api/emails/subscribers/export-csv.ts @@ -1,17 +1,12 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { Parser } from "@json2csv/plainjs"; -import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../../utils/withAuth"; -const getSubscribersExportCsv = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const getSubscribersExportCsv = withAuth(async (req, res, { user }) => { if (req.method === "GET") { try { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClientForAPI({ req, res }); const page_id = String(req.query.page_id); @@ -54,6 +49,6 @@ const getSubscribersExportCsv = async ( res.setHeader("Allow", "POST,PUT"); res.status(405).end("Method Not Allowed"); } -}; +}); export default getSubscribersExportCsv; diff --git a/apps/web/pages/api/emails/subscribers/index.ts b/apps/web/pages/api/emails/subscribers/index.ts index 0bbd4e7..499775b 100644 --- a/apps/web/pages/api/emails/subscribers/index.ts +++ b/apps/web/pages/api/emails/subscribers/index.ts @@ -1,45 +1,41 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; -import { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../../utils/withAuth"; -const getEmailSubscribers = async ( - req: NextApiRequest, - res: NextApiResponse -) => { - if (req.method === "GET") { - try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); +const getEmailSubscribers = withAuth<{ count: number }>( + async (req, res, { user }) => { + if (req.method === "GET") { + try { + const page_id = String(req.query.page_id); - const page_id = String(req.query.page_id); + await supabaseAdmin + .from("pages") + .select("id") + .eq("id", page_id) + .eq("user_id", user.id) + .single(); - await supabaseAdmin - .from("pages") - .select("id") - .eq("id", page_id) - .eq("user_id", user.id) - .single(); + const { count, error } = await supabaseAdmin + .from("page_email_subscribers") + .select("page_id", { count: "exact" }) + .eq("page_id", page_id); - const { count, error } = await supabaseAdmin - .from("page_email_subscribers") - .select("page_id", { count: "exact" }) - .eq("page_id", page_id); + if (error) { + console.error(error); + throw new Error("Failed to get email subscribers"); + } - if (error) { - console.error(error); - throw new Error("Failed to get email subscribers"); + return res.status(200).json({ count }); + } catch (err) { + console.log("getEmailSubscribers: Error:", err); + res + .status(500) + .json({ error: { statusCode: 500, message: err.message } }); } - - return res.status(200).json({ count }); - } catch (err) { - console.log("getEmailSubscribers: Error:", err); - res - .status(500) - .json({ error: { statusCode: 500, message: err.message } }); + } else { + res.setHeader("Allow", "POST,PUT"); + res.status(405).end("Method Not Allowed"); } - } else { - res.setHeader("Allow", "POST,PUT"); - res.status(405).end("Method Not Allowed"); } -}; +); export default getEmailSubscribers; diff --git a/apps/web/pages/api/pages/new.ts b/apps/web/pages/api/pages/new.ts index 7fbb8cd..7e47cc7 100644 --- a/apps/web/pages/api/pages/new.ts +++ b/apps/web/pages/api/pages/new.ts @@ -1,27 +1,20 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; import { IPage } from "@changes-page/supabase/types/page"; -import type { NextApiRequest, NextApiResponse } from "next"; import { NewPageSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createPage, getUserById, updateSubscriptionUsage, } from "../../../utils/useDatabase"; +import { withAuth } from "../../../utils/withAuth"; -const createNewPage = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const createNewPage = withAuth(async (req, res, { user }) => { if (req.method === "POST") { await apiRateLimiter(req, res); const { url_slug, title, description, type } = req.body; try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - const { has_active_subscription } = await getUserById(user.id); if (!has_active_subscription) { return res.status(403).json({ @@ -65,6 +58,6 @@ const createNewPage = async ( res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } -}; +}); export default createNewPage; diff --git a/apps/web/pages/api/pages/reactions.ts b/apps/web/pages/api/pages/reactions.ts index 8ea5b03..ad1b5e0 100644 --- a/apps/web/pages/api/pages/reactions.ts +++ b/apps/web/pages/api/pages/reactions.ts @@ -1,58 +1,46 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../utils/withAuth"; -export default async function getPostReactions( - req: NextApiRequest, - res: NextApiResponse<{ ok: boolean; aggregate: any }> -) { - let { post_id } = req.query; +export default withAuth<{ ok: boolean; aggregate: any }>( + async function getPostReactions(req, res) { + let { post_id } = req.query; - try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - if (!user) { - return res.status(401).json({ - ok: false, - aggregate: null, - }); - } + try { + const { data: aggregate, error: aggregateError } = + await supabaseAdmin.rpc("post_reactions_aggregate", { + postid: String(post_id), + }); - const { data: aggregate, error: aggregateError } = await supabaseAdmin.rpc( - "post_reactions_aggregate", - { - postid: String(post_id), + if (aggregateError) { + console.error("getPostReactions [Error]", aggregateError); } - ); - if (aggregateError) { - console.error("getPostReactions [Error]", aggregateError); - } + if (!aggregate?.length) { + res.status(200).json({ + ok: true, + aggregate: { + thumbs_up: 0, + thumbs_down: 0, + heart: 0, + sad: 0, + rocket: 0, + }, + }); + } - if (!aggregate?.length) { res.status(200).json({ ok: true, aggregate: { - thumbs_up: 0, - thumbs_down: 0, - heart: 0, - sad: 0, - rocket: 0, + thumbs_up: aggregate[0].thumbs_up_count, + thumbs_down: aggregate[0].thumbs_down_count, + heart: aggregate[0].heart_count, + sad: aggregate[0].sad_count, + rocket: aggregate[0].rocket_count, }, }); + } catch (e: Error | any) { + console.log("getPostReactions [Error]", e); + res.status(500).json({ ok: false, aggregate: null }); } - - res.status(200).json({ - ok: true, - aggregate: { - thumbs_up: aggregate[0].thumbs_up_count, - thumbs_down: aggregate[0].thumbs_down_count, - heart: aggregate[0].heart_count, - sad: aggregate[0].sad_count, - rocket: aggregate[0].rocket_count, - }, - }); - } catch (e: Error | any) { - console.log("getPostReactions [Error]", e); - res.status(500).json({ ok: false, aggregate: null }); } -} +); diff --git a/apps/web/pages/api/pages/settings/add-domain.ts b/apps/web/pages/api/pages/settings/add-domain.ts index fdd4934..a16bf5c 100644 --- a/apps/web/pages/api/pages/settings/add-domain.ts +++ b/apps/web/pages/api/pages/settings/add-domain.ts @@ -1,10 +1,8 @@ -import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../../utils/withAuth"; -async function addDomain(req: NextApiRequest, res: NextApiResponse) { +const addDomain = withAuth(async (req, res, { user }) => { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClientForAPI({ req, res }); const { domain } = req.body; @@ -49,6 +47,6 @@ async function addDomain(req: NextApiRequest, res: NextApiResponse) { success: true, }); } -} +}); export default addDomain; diff --git a/apps/web/pages/api/pages/settings/check-domain.ts b/apps/web/pages/api/pages/settings/check-domain.ts index e139a02..d0951dd 100644 --- a/apps/web/pages/api/pages/settings/check-domain.ts +++ b/apps/web/pages/api/pages/settings/check-domain.ts @@ -1,10 +1,8 @@ -import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../../utils/withAuth"; -async function checkDomain(req: NextApiRequest, res: NextApiResponse) { +const checkDomain = withAuth<{ valid: boolean }>(async (req, res, { user }) => { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClientForAPI({ req, res }); const { domain } = req.query; @@ -34,6 +32,6 @@ async function checkDomain(req: NextApiRequest, res: NextApiResponse) { res.status(200).json({ valid, }); -} +}); export default checkDomain; diff --git a/apps/web/pages/api/pages/settings/index.ts b/apps/web/pages/api/pages/settings/index.ts index a4e132a..27e30b2 100644 --- a/apps/web/pages/api/pages/settings/index.ts +++ b/apps/web/pages/api/pages/settings/index.ts @@ -1,19 +1,12 @@ -import { IErrorResponse } from "@changes-page/supabase/types/api"; import { IPageSettings } from "@changes-page/supabase/types/page"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; +import { withAuth } from "../../../../utils/withAuth"; -const getPageSettings = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const getPageSettings = withAuth(async (req, res, { user }) => { if (req.method === "GET") { const { page_id } = req.query; try { - const { user } = await getSupabaseServerClientForAPI({ req, res }); - console.log("getPageSettings", user?.id); const data = await createOrRetrievePageSettings(String(page_id)); @@ -29,6 +22,6 @@ const getPageSettings = async ( res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } -}; +}); export default getPageSettings; diff --git a/apps/web/pages/api/pages/settings/remove-domain.ts b/apps/web/pages/api/pages/settings/remove-domain.ts index 5a56d46..1101d5b 100644 --- a/apps/web/pages/api/pages/settings/remove-domain.ts +++ b/apps/web/pages/api/pages/settings/remove-domain.ts @@ -1,28 +1,27 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../../utils/withAuth"; -async function removeDomain(req: NextApiRequest, res: NextApiResponse) { - const { user } = await getSupabaseServerClientForAPI({ req, res }); +const removeDomain = withAuth<{ success: boolean }>( + async (req, res, { user }) => { + const { domain } = req.body; - const { domain } = req.body; + console.log("removeDomain", user?.id, `domain: ${domain}`); - console.log("removeDomain", user?.id, `domain: ${domain}`); + const response = await fetch( + `https://api.vercel.com/v8/projects/${process.env.VERCEL_PAGES_PROJECT_ID}/domains/${domain}?teamId=${process.env.VERCEL_TEAM_ID}`, + { + headers: { + Authorization: `Bearer ${process.env.VERCEL_AUTH_TOKEN}`, + }, + method: "DELETE", + } + ); - const response = await fetch( - `https://api.vercel.com/v8/projects/${process.env.VERCEL_PAGES_PROJECT_ID}/domains/${domain}?teamId=${process.env.VERCEL_TEAM_ID}`, - { - headers: { - Authorization: `Bearer ${process.env.VERCEL_AUTH_TOKEN}`, - }, - method: "DELETE", - } - ); + await response.json(); - await response.json(); - - res.status(200).json({ - success: true, - }); -} + res.status(200).json({ + success: true, + }); + } +); export default removeDomain; diff --git a/apps/web/pages/api/posts/index.ts b/apps/web/pages/api/posts/index.ts index 2534f16..c8e9f1c 100644 --- a/apps/web/pages/api/posts/index.ts +++ b/apps/web/pages/api/posts/index.ts @@ -1,11 +1,10 @@ import { PostStatus } from "@changes-page/supabase/types/page"; -import { NextApiRequest, NextApiResponse } from "next"; import { NewPostSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../utils/supabase/supabase-admin"; import { createPost } from "../../../utils/useDatabase"; +import { withAuth } from "../../../utils/withAuth"; -const createNewPost = async (req: NextApiRequest, res: NextApiResponse) => { +const createNewPost = withAuth(async (req, res, { user, supabase }) => { if (req.method === "POST") { const { page_id, @@ -24,11 +23,6 @@ const createNewPost = async (req: NextApiRequest, res: NextApiResponse) => { try { await apiRateLimiter(req, res); - const { user, supabase } = await getSupabaseServerClientForAPI({ - req, - res, - }); - const isValid = await NewPostSchema.isValid({ page_id, title: title.trim(), @@ -97,6 +91,6 @@ const createNewPost = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader("Allow", "POST,PUT"); res.status(405).end("Method Not Allowed"); } -}; +}); export default createNewPost; diff --git a/apps/web/pages/api/teams/invite/accept/index.ts b/apps/web/pages/api/teams/invite/accept/index.ts index f872f37..b8941cd 100644 --- a/apps/web/pages/api/teams/invite/accept/index.ts +++ b/apps/web/pages/api/teams/invite/accept/index.ts @@ -1,9 +1,8 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; -import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../../../utils/supabase/supabase-admin"; +import { withAuth } from "../../../../../utils/withAuth"; -const acceptInvite = async (req: NextApiRequest, res: NextApiResponse) => { +const acceptInvite = withAuth(async (req, res, { user, supabase }) => { if (req.method === "POST") { const { invite_id } = req.body; if (!invite_id) { @@ -15,16 +14,6 @@ const acceptInvite = async (req: NextApiRequest, res: NextApiResponse) => { try { await apiRateLimiter(req, res); - const { user, supabase } = await getSupabaseServerClientForAPI({ - req, - res, - }); - if (!user) { - return res.status(401).json({ - error: { statusCode: 401, message: "Unauthorized" }, - }); - } - const { data: invite } = await supabase .from("team_invitations") .select("*") @@ -67,6 +56,6 @@ const acceptInvite = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader("Allow", "POST,PUT"); res.status(405).end("Method Not Allowed"); } -}; +}); export default acceptInvite; diff --git a/apps/web/pages/api/teams/invite/index.ts b/apps/web/pages/api/teams/invite/index.ts index 55bafb1..e66b923 100644 --- a/apps/web/pages/api/teams/invite/index.ts +++ b/apps/web/pages/api/teams/invite/index.ts @@ -1,101 +1,92 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; -import { NextApiRequest, NextApiResponse } from "next"; import { ROUTES } from "../../../../data/routes.data"; import { getAppBaseURL } from "../../../../utils/helpers"; import inngestClient from "../../../../utils/inngest"; import { apiRateLimiter } from "../../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../../utils/useDatabase"; +import { withAuth } from "../../../../utils/withAuth"; -const inviteUser = async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === "POST") { - const { team_id, email } = req.body; - if (!team_id || !email) { - return res.status(400).json({ - error: { statusCode: 400, message: "Invalid request" }, - }); - } - - try { - await apiRateLimiter(req, res); - - const { user, supabase } = await getSupabaseServerClientForAPI({ - req, - res, - }); - if (!user) { - return res.status(401).json({ - error: { statusCode: 401, message: "Unauthorized" }, +const inviteUser = withAuth<{ ok: boolean }>( + async (req, res, { user, supabase }) => { + if (req.method === "POST") { + const { team_id, email } = req.body; + if (!team_id || !email) { + return res.status(400).json({ + error: { statusCode: 400, message: "Invalid request" }, }); } - const { has_active_subscription } = await getUserById(user.id); - if (!has_active_subscription) { - return res.status(403).json({ - error: { statusCode: 403, message: "Missing subscription" }, - }); - } + try { + await apiRateLimiter(req, res); - if (user.email === email) { - return res.status(400).json({ - error: { statusCode: 400, message: "You cannot invite yourself" }, - }); - } + const { has_active_subscription } = await getUserById(user.id); + if (!has_active_subscription) { + return res.status(403).json({ + error: { statusCode: 403, message: "Missing subscription" }, + }); + } - const { data: team } = await supabase - .from("teams") - .select("*") - .eq("id", team_id) - .eq("owner_id", user.id) - .single(); + if (user.email === email) { + return res.status(400).json({ + error: { statusCode: 400, message: "You cannot invite yourself" }, + }); + } - if (!team) { - return res.status(403).json({ - error: { statusCode: 403, message: "Team not found" }, - }); - } + const { data: team } = await supabase + .from("teams") + .select("*") + .eq("id", team_id) + .eq("owner_id", user.id) + .single(); - const { data: invitation, error: invitationError } = await supabaseAdmin - .from("team_invitations") - .insert({ - team_id, - inviter_id: user.id, - email, - role: "editor", - status: "pending", - }) - .select() - .single(); + if (!team) { + return res.status(403).json({ + error: { statusCode: 403, message: "Team not found" }, + }); + } - if (!invitation) { - return res.status(500).json({ - error: { statusCode: 500, message: "Failed to create invitation" }, - }); - } + const { data: invitation, error: invitationError } = await supabaseAdmin + .from("team_invitations") + .insert({ + team_id, + inviter_id: user.id, + email, + role: "editor", + status: "pending", + }) + .select() + .single(); + + if (!invitation || invitationError) { + return res.status(500).json({ + error: { statusCode: 500, message: "Failed to create invitation" }, + }); + } - await inngestClient.send({ - name: "email/team.invite", - data: { - email, - payload: { - owner_name: user.user_metadata?.name ?? user.email, - team_name: team.name, - confirm_link: `${getAppBaseURL()}${ROUTES.TEAMS}`, + await inngestClient.send({ + name: "email/team.invite", + data: { + email, + payload: { + owner_name: user.user_metadata?.name ?? user.email, + team_name: team.name, + confirm_link: `${getAppBaseURL()}${ROUTES.TEAMS}`, + }, }, - }, - }); + }); - return res.status(201).json({ ok: true }); - } catch (err) { - console.log("inviteUser", err); - res - .status(500) - .json({ error: { statusCode: 500, message: err.message } }); + return res.status(201).json({ ok: true }); + } catch (err) { + console.log("inviteUser", err); + res + .status(500) + .json({ error: { statusCode: 500, message: err.message } }); + } + } else { + res.setHeader("Allow", "POST,PUT"); + res.status(405).end("Method Not Allowed"); } - } else { - res.setHeader("Allow", "POST,PUT"); - res.status(405).end("Method Not Allowed"); } -}; +); export default inviteUser; diff --git a/apps/web/pages/api/teams/member/[id]/index.ts b/apps/web/pages/api/teams/member/[id]/index.ts index 74e6e16..7d04744 100644 --- a/apps/web/pages/api/teams/member/[id]/index.ts +++ b/apps/web/pages/api/teams/member/[id]/index.ts @@ -1,13 +1,9 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; -import { NextApiRequest, NextApiResponse } from "next"; import { apiRateLimiter } from "../../../../../utils/rate-limit"; -import { getSupabaseServerClientForAPI } from "../../../../../utils/supabase/supabase-admin"; import { getUserById } from "../../../../../utils/useDatabase"; +import { withAuth } from "../../../../../utils/withAuth"; -const getTeamMemberDetails = async ( - req: NextApiRequest, - res: NextApiResponse -) => { +const getTeamMemberDetails = withAuth(async (req, res, { user }) => { if (req.method === "GET") { const { id } = req.query; if (!id) { @@ -19,8 +15,6 @@ const getTeamMemberDetails = async ( try { await apiRateLimiter(req, res); - const { user } = await getSupabaseServerClientForAPI({ req, res }); - const { has_active_subscription } = await getUserById(user.id); if (!has_active_subscription) { return res.status(403).json({ @@ -62,6 +56,6 @@ const getTeamMemberDetails = async ( res.setHeader("Allow", "GET"); res.status(405).end("Method Not Allowed"); } -}; +}); export default getTeamMemberDetails; diff --git a/apps/web/pages/integrations/zapier.tsx b/apps/web/pages/integrations/zapier.tsx index 4cc0135..4a247d0 100644 --- a/apps/web/pages/integrations/zapier.tsx +++ b/apps/web/pages/integrations/zapier.tsx @@ -1,17 +1,16 @@ -import { GetServerSidePropsContext } from "next"; import Head from "next/head"; import FooterComponent from "../../components/layout/footer.component"; import HeaderComponent from "../../components/layout/header.component"; import Page from "../../components/layout/page.component"; -import { getSupabaseServerClientForSSR } from "../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../utils/supabase/withSupabase"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { user } = await getSupabaseServerClientForSSR(ctx); - - return { - props: { email: user?.email }, - }; -} +export const getServerSideProps = withSupabase<{ email: string }>( + async (_, { user }) => { + return { + props: { email: user?.email }, + }; + } +); export default function Zapier({ email }: { email?: string }) { return ( diff --git a/apps/web/pages/onboarding/open-page.tsx b/apps/web/pages/onboarding/open-page.tsx index 83b4b16..b43b3b8 100644 --- a/apps/web/pages/onboarding/open-page.tsx +++ b/apps/web/pages/onboarding/open-page.tsx @@ -1,14 +1,14 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { SpinnerWithSpacing } from "@changes-page/ui"; -import { GetServerSideProps, GetServerSidePropsContext } from "next"; import { ROUTES } from "../../data/routes.data"; -import { getSupabaseServerClientForSSR } from "../../utils/supabase/supabase-admin"; - -export const getServerSideProps: GetServerSideProps = async ( - ctx: GetServerSidePropsContext -) => { - const { user } = await getSupabaseServerClientForSSR(ctx); +import { withSupabase } from "../../utils/supabase/withSupabase"; +export const getServerSideProps = withSupabase<{ + redirect: { + permanent: boolean; + destination: string; + }; +}>(async (ctx, { user }) => { if (!user) { return { redirect: { @@ -40,7 +40,7 @@ export const getServerSideProps: GetServerSideProps = async ( destination: `${ROUTES.PAGES}/${pages[0].id}/${path ?? ""}`, }, }; -}; +}); function LoadingPage() { return ; diff --git a/apps/web/pages/pages/[page_id]/[post_id].tsx b/apps/web/pages/pages/[page_id]/[post_id].tsx index 9708aec..b044a43 100644 --- a/apps/web/pages/pages/[page_id]/[post_id].tsx +++ b/apps/web/pages/pages/[page_id]/[post_id].tsx @@ -1,5 +1,5 @@ import { IPost, PostStatus } from "@changes-page/supabase/types/page"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useState, type JSX } from "react"; import { InferType } from "yup"; @@ -11,14 +11,13 @@ import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; import { NewPostSchema } from "../../../data/schema"; -import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../utils/useDatabase"; import { useUserData } from "../../../utils/useUser"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const { page_id, post_id } = ctx.params; - const { supabase } = await getSupabaseServerClientForSSR(ctx); const settings = await createOrRetrievePageSettings(String(page_id)); const { data: post } = await supabase .from("posts") @@ -34,7 +33,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { settings, }, }; -} +}); export default function EditPost({ page_id, diff --git a/apps/web/pages/pages/[page_id]/analytics.tsx b/apps/web/pages/pages/[page_id]/analytics.tsx index 785a9b2..4cdf5a1 100644 --- a/apps/web/pages/pages/[page_id]/analytics.tsx +++ b/apps/web/pages/pages/[page_id]/analytics.tsx @@ -1,11 +1,11 @@ import { supabaseAdmin } from "@changes-page/supabase/admin"; import { SpinnerWithSpacing } from "@changes-page/ui"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import Image from "next/image"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; -import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../utils/supabase/withSupabase"; import { getPageAnalytics } from "../../../utils/useDatabase"; import { getPage } from "../../../utils/useSSR"; @@ -183,11 +183,10 @@ const StatsTable = ({ data = [], title, total = 5 }) => { ); }; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { range } = ctx.query; +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { + const range = String(ctx.query.range); const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id); const rangeNum = Number(range) || 7; @@ -281,7 +280,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { peakHoursData, }, }; -} +}); export default function PageAnalytics({ page, diff --git a/apps/web/pages/pages/[page_id]/audit-logs.tsx b/apps/web/pages/pages/[page_id]/audit-logs.tsx index 274f678..1c1880c 100644 --- a/apps/web/pages/pages/[page_id]/audit-logs.tsx +++ b/apps/web/pages/pages/[page_id]/audit-logs.tsx @@ -8,17 +8,16 @@ import { TrashIcon, UserIcon, } from "@heroicons/react/solid"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import AuthLayout from "../../../components/layout/auth-layout.component"; import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; -import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../utils/supabase/withSupabase"; import { getPage } from "../../../utils/useSSR"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id); const { data: audit_logs } = await supabaseAdmin .from("page_audit_logs") @@ -34,7 +33,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { audit_logs: audit_logs ?? [], }, }; -} +}); export default function PageAnalytics({ page, diff --git a/apps/web/pages/pages/[page_id]/edit.tsx b/apps/web/pages/pages/[page_id]/edit.tsx index 8315504..014f5f9 100644 --- a/apps/web/pages/pages/[page_id]/edit.tsx +++ b/apps/web/pages/pages/[page_id]/edit.tsx @@ -1,5 +1,5 @@ import { IPage } from "@changes-page/supabase/types/page"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useState } from "react"; import { InferType } from "yup"; @@ -12,20 +12,19 @@ import Page from "../../../components/layout/page.component"; import { ROUTES } from "../../../data/routes.data"; import { NewPageSchema } from "../../../data/schema"; import { httpPost } from "../../../utils/http"; -import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../utils/supabase/withSupabase"; import { getPage } from "../../../utils/useSSR"; import { useUserData } from "../../../utils/useUser"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const { page_id } = ctx.params; - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id as string); return { props: { page_id, page }, }; -} +}); export default function EditPage({ page_id, diff --git a/apps/web/pages/pages/[page_id]/index.tsx b/apps/web/pages/pages/[page_id]/index.tsx index c81d869..4459191 100644 --- a/apps/web/pages/pages/[page_id]/index.tsx +++ b/apps/web/pages/pages/[page_id]/index.tsx @@ -15,7 +15,7 @@ import { XCircleIcon, } from "@heroicons/react/solid"; import classNames from "classnames"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import Script from "next/script"; import { useMemo, useState, type JSX } from "react"; @@ -41,15 +41,14 @@ import { ROUTES } from "../../../data/routes.data"; import usePageSettings from "../../../utils/hooks/usePageSettings"; import usePageUrl from "../../../utils/hooks/usePageUrl"; import usePosts from "../../../utils/hooks/usePosts"; -import { getSupabaseServerClientForSSR } from "../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../utils/useDatabase"; import { getPage } from "../../../utils/useSSR"; import { useUserData } from "../../../utils/useUser"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -70,7 +69,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { settings, }, }; -} +}); export default function PageDetail({ page, diff --git a/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx b/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx index 952f81a..b07fc25 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/[board_id].tsx @@ -1,19 +1,18 @@ -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { type JSX } from "react"; import { SecondaryButton } from "../../../../components/core/buttons.component"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; import RoadmapBoard from "../../../../components/roadmap/RoadmapBoard"; -import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; import { getPage } from "../../../../utils/useSSR"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); const board_id = String(ctx.params?.board_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -84,7 +83,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { categories: categories || [], }, }; -} +}); export default function RoadmapBoardDetails({ page_id, diff --git a/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx b/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx index a0c8c83..d3977b0 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx @@ -5,24 +5,23 @@ import { } from "@changes-page/utils"; import { MenuIcon } from "@heroicons/react/outline"; import { PencilIcon, PlusIcon, TrashIcon } from "@heroicons/react/solid"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useEffect, useMemo, useState, type JSX } from "react"; import AuthLayout from "../../../../../components/layout/auth-layout.component"; import Page from "../../../../../components/layout/page.component"; import usePageSettings from "../../../../../utils/hooks/usePageSettings"; import { getPageUrl } from "../../../../../utils/hooks/usePageUrl"; -import { getSupabaseServerClientForSSR } from "../../../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../../../utils/useDatabase"; import { getPage } from "../../../../../utils/useSSR"; import { useUserData } from "../../../../../utils/useUser"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); const board_id = String(ctx.params?.board_id); const { tab = "board" } = ctx.query; - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -84,7 +83,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { initialTab: tab, }, }; -} +}); export default function BoardSettings({ page, diff --git a/apps/web/pages/pages/[page_id]/roadmap/index.tsx b/apps/web/pages/pages/[page_id]/roadmap/index.tsx index 6be9eb4..c10e041 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/index.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/index.tsx @@ -1,18 +1,17 @@ import { PlusIcon } from "@heroicons/react/solid"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import Link from "next/link"; import { useMemo, type JSX } from "react"; import { PrimaryRouterButton } from "../../../../components/core/buttons.component"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; import { ROUTES } from "../../../../data/routes.data"; -import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../../utils/supabase/withSupabase"; import { getPage } from "../../../../utils/useSSR"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -41,7 +40,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { boards: boards || [], }, }; -} +}); export default function RoadmapPage({ page, diff --git a/apps/web/pages/pages/[page_id]/roadmap/new.tsx b/apps/web/pages/pages/[page_id]/roadmap/new.tsx index d49c8e1..43250f0 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/new.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/new.tsx @@ -1,16 +1,15 @@ -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import { useState, type JSX } from "react"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; -import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../../utils/supabase/withSupabase"; import { getPage } from "../../../../utils/useSSR"; import { useUserData } from "../../../../utils/useUser"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id).catch((e) => { console.error("Failed to get page", e); return null; @@ -28,7 +27,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { page, }, }; -} +}); export default function NewRoadmapBoard({ page_id, diff --git a/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx b/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx index 511bb7e..b3c670a 100644 --- a/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx +++ b/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx @@ -1,5 +1,5 @@ import { SpinnerWithSpacing } from "@changes-page/ui"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import dynamic from "next/dynamic"; import { useMemo } from "react"; import AuthLayout from "../../../../components/layout/auth-layout.component"; @@ -10,7 +10,7 @@ import SocialLinksSettings from "../../../../components/page-settings/social-lin import StyleSettings from "../../../../components/page-settings/style"; import { ROUTES } from "../../../../data/routes.data"; import usePageSettings from "../../../../utils/hooks/usePageSettings"; -import { getSupabaseServerClientForSSR } from "../../../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../../utils/useDatabase"; import { getPage } from "../../../../utils/useSSR"; @@ -21,10 +21,9 @@ const IntegrationsSettings = dynamic( } ); -export async function getServerSideProps(ctx: GetServerSidePropsContext) { +export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const page_id = String(ctx.params?.page_id); - const { supabase } = await getSupabaseServerClientForSSR(ctx); const page = await getPage(supabase, page_id); const settings = await createOrRetrievePageSettings(String(page_id)); @@ -36,7 +35,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { activeTab: ctx.params?.activeTab, }, }; -} +}); export default function PageSettings({ page, diff --git a/apps/web/pages/pages/index.tsx b/apps/web/pages/pages/index.tsx index 7825a2e..0a3ee2c 100644 --- a/apps/web/pages/pages/index.tsx +++ b/apps/web/pages/pages/index.tsx @@ -1,7 +1,7 @@ import { PageType, PageTypeToLabel } from "@changes-page/supabase/types/page"; import { PlusIcon, UserGroupIcon } from "@heroicons/react/solid"; import classNames from "classnames"; -import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; +import { InferGetServerSidePropsType } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, type JSX } from "react"; @@ -14,12 +14,10 @@ import Page from "../../components/layout/page.component"; import Changelog from "../../components/marketing/changelog"; import { ROUTES } from "../../data/routes.data"; import { getAppBaseURL } from "../../utils/helpers"; -import { getSupabaseServerClientForSSR } from "../../utils/supabase/supabase-admin"; +import { withSupabase } from "../../utils/supabase/withSupabase"; import { useUserData } from "../../utils/useUser"; -export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { supabase } = await getSupabaseServerClientForSSR(ctx); - +export const getServerSideProps = withSupabase(async (_, { supabase }) => { const { data: pages, error } = await supabase .from("pages") .select( @@ -41,7 +39,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { error, }, }; -} +}); export default function Pages({ pages, diff --git a/apps/web/utils/supabase/server.ts b/apps/web/utils/supabase/server.ts index 1596bb4..1049c04 100644 --- a/apps/web/utils/supabase/server.ts +++ b/apps/web/utils/supabase/server.ts @@ -38,9 +38,9 @@ export function createServerClientForAPI({ req: NextApiRequest; res: NextApiResponse; }) { - const supabase = createServerClient( + const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { diff --git a/apps/web/utils/supabase/static.ts b/apps/web/utils/supabase/static.ts index d162f56..15a8ffe 100644 --- a/apps/web/utils/supabase/static.ts +++ b/apps/web/utils/supabase/static.ts @@ -3,7 +3,7 @@ import { createClient as createClientPrimitive } from "@supabase/supabase-js"; export function createClient() { const supabase = createClientPrimitive( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); return supabase; diff --git a/apps/web/utils/supabase/supabase-admin.ts b/apps/web/utils/supabase/supabase-admin.ts deleted file mode 100644 index ce9e710..0000000 --- a/apps/web/utils/supabase/supabase-admin.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Database } from "@changes-page/supabase/types"; -import { Session, SupabaseClient, User } from "@supabase/supabase-js"; -import { - GetServerSidePropsContext, - NextApiRequest, - NextApiResponse, -} from "next"; -import { createServerClientForAPI, createServerClientSSR } from "./server"; - -export const getSupabaseServerClientForSSR = async ( - context: GetServerSidePropsContext -): Promise<{ - supabase: SupabaseClient; - session: Session | null; - user: User | null; -}> => { - const supabase = createServerClientSSR(context); - - const { - data: { user }, - error, - } = await supabase.auth.getUser(); - - if (error) { - console.error("Error getting user:", error); - } - - const { - data: { session }, - } = await supabase.auth.getSession(); - - return { - supabase, - session, - user, - }; -}; - -export const getSupabaseServerClientForAPI = async (context: { - req: NextApiRequest; - res: NextApiResponse; -}): Promise<{ - supabase: SupabaseClient; - session: Session | null; - user: User | null; -}> => { - const supabase = createServerClientForAPI(context); - - const { - data: { user }, - error, - } = await supabase.auth.getUser(); - - if (error) { - console.error("Error getting user:", error); - } - - const { - data: { session }, - } = await supabase.auth.getSession(); - - return { - supabase, - session, - user, - }; -}; diff --git a/apps/web/utils/supabase/withSupabase.ts b/apps/web/utils/supabase/withSupabase.ts new file mode 100644 index 0000000..12eaa0c --- /dev/null +++ b/apps/web/utils/supabase/withSupabase.ts @@ -0,0 +1,26 @@ +import { Database } from "@changes-page/supabase/types"; +import { SupabaseClient, User } from "@supabase/supabase-js"; +import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; +import { createServerClientSSR } from "./server"; + +type SupabaseHandler

= ( + context: GetServerSidePropsContext, + { supabase, user }: { supabase: SupabaseClient; user: User } +) => Promise>; + +export function withSupabase

(handler: SupabaseHandler

) { + return async (context: GetServerSidePropsContext) => { + const supabase = createServerClientSSR(context); + + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (!user || error) { + throw new Error("User not authenticated"); + } + + return handler(context, { supabase, user }); + }; +} diff --git a/apps/web/utils/withAuth.ts b/apps/web/utils/withAuth.ts new file mode 100644 index 0000000..f3d86d1 --- /dev/null +++ b/apps/web/utils/withAuth.ts @@ -0,0 +1,40 @@ +import { Database } from "@changes-page/supabase/types"; +import { IErrorResponse } from "@changes-page/supabase/types/api"; +import { SupabaseClient, User } from "@supabase/supabase-js"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createServerClientForAPI } from "./supabase/server"; + +type AuthenticatedHandler = ( + req: NextApiRequest, + res: NextApiResponse, + { supabase, user }: { supabase: SupabaseClient; user: User } +) => Promise | void; + +export function withAuth(handler: AuthenticatedHandler) { + return async ( + req: NextApiRequest, + res: NextApiResponse + ) => { + try { + const supabase = createServerClientForAPI({ req, res }); + + const { + data: { user }, + error, + } = await supabase.auth.getUser(); + + if (!user || error) { + return res.status(401).json({ + error: { statusCode: 401, message: "Unauthorized" }, + }); + } + + return handler(req, res, { supabase, user }); + } catch (error) { + console.error("Auth wrapper error:", error); + return res.status(500).json({ + error: { statusCode: 500, message: "Internal server error" }, + }); + } + }; +} From 6378408a115c894dcf0763aa5cb9877e02984e7e Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 7 Sep 2025 23:54:42 +1000 Subject: [PATCH 06/10] Upgrade typescript --- ...expand-concept-prompt-dialog.component.tsx | 3 + .../ai-prood-read-dialog.component.tsx | 3 + ...-suggest-title-prompt-dialog.component.tsx | 11 +- .../confirm-delete-dialog.component.tsx | 6 +- .../date-time-prompt-dialog.component.tsx | 3 + .../dialogs/manage-team-dialog.component.tsx | 3 + .../dialogs/warning-dialog.component.tsx | 5 +- .../components/dialogs/widget-code-dialog.tsx | 3 + .../components/forms/post-form.component.tsx | 3 + .../layout/blog-layout.component.tsx | 1 + .../components/layout/header.component.tsx | 1 + apps/web/components/layout/page.component.tsx | 2 + apps/web/components/marketing/changelog.tsx | 5 +- apps/web/components/post/post-options.tsx | 11 +- apps/web/components/post/post.tsx | 1 + .../components/roadmap/RoadmapItemModal.tsx | 3 + apps/web/next.config.js | 3 - apps/web/package.json | 6 +- .../pages/api/billing/jobs/report-usage.ts | 15 +- .../integrations/zapier/trigger-new-post.tsx | 4 +- .../pages/api/pages/settings/remove-domain.ts | 19 + .../web/pages/free-tools/release-calendar.tsx | 815 ++++++++++++------ apps/web/pages/pages/[page_id]/[post_id].tsx | 2 +- apps/web/utils/hooks/usePosts.ts | 4 +- apps/web/utils/useUser.tsx | 1 + pnpm-lock.yaml | 164 +--- 26 files changed, 651 insertions(+), 446 deletions(-) diff --git a/apps/web/components/dialogs/ai-expand-concept-prompt-dialog.component.tsx b/apps/web/components/dialogs/ai-expand-concept-prompt-dialog.component.tsx index 55bbed4..4637c9c 100644 --- a/apps/web/components/dialogs/ai-expand-concept-prompt-dialog.component.tsx +++ b/apps/web/components/dialogs/ai-expand-concept-prompt-dialog.component.tsx @@ -72,6 +72,7 @@ export default function AiExpandConceptPromptDialogComponent({ }, [open, content]); return ( + // @ts-ignore

)} - + {/* @ts-ignore */}
{!editNotes ? ( formik.values.notes ? ( + // @ts-ignore {formik.values.notes} diff --git a/apps/web/components/layout/blog-layout.component.tsx b/apps/web/components/layout/blog-layout.component.tsx index 66a7cfb..dc95904 100644 --- a/apps/web/components/layout/blog-layout.component.tsx +++ b/apps/web/components/layout/blog-layout.component.tsx @@ -201,6 +201,7 @@ export default function BlogLayout({ ) : null}
+ {/* @ts-ignore */}