From 93b7c942cc56f7c2234e4ac5f55de227a6b3157a Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Wed, 25 Jun 2025 17:46:57 -0400 Subject: [PATCH] ENG-520: break-out infrastructure from ENG-373 --- .github/workflows/roam-pr.yaml | 3 + apps/roam/package.json | 2 + apps/roam/scripts/compile.ts | 4 + apps/roam/tsconfig.json | 1 - apps/website/app/utils/supabase/client.ts | 14 ++ apps/website/app/utils/supabase/middleware.ts | 70 ++++++++++ apps/website/app/utils/supabase/server.ts | 2 + apps/website/tsconfig.json | 1 + package-lock.json | 126 ++++++++++++++---- packages/ui/package.json | 4 +- packages/ui/src/lib/supabase/client.ts | 14 ++ turbo.json | 16 ++- 12 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 apps/website/app/utils/supabase/client.ts create mode 100644 apps/website/app/utils/supabase/middleware.ts create mode 100644 packages/ui/src/lib/supabase/client.ts diff --git a/.github/workflows/roam-pr.yaml b/.github/workflows/roam-pr.yaml index a4a2b3d21..6c702a556 100644 --- a/.github/workflows/roam-pr.yaml +++ b/.github/workflows/roam-pr.yaml @@ -16,6 +16,9 @@ env: jobs: deploy: runs-on: ubuntu-latest + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} steps: - name: Checkout Code uses: actions/checkout@v4 diff --git a/apps/roam/package.json b/apps/roam/package.json index 3d7233eae..8fbced3f5 100644 --- a/apps/roam/package.json +++ b/apps/roam/package.json @@ -28,6 +28,8 @@ "@octokit/auth-app": "^7.1.4", "@octokit/core": "^6.1.3", "@repo/types": "*", + "@supabase/auth-js": "^2.70.0", + "@supabase/supabase-js": "^2.50.0", "@tldraw/tldraw": "^2.0.0-alpha.12", "@vercel/blob": "^0.27.0", "contrast-color": "^1.0.1", diff --git a/apps/roam/scripts/compile.ts b/apps/roam/scripts/compile.ts index aa57b71fd..8efb20a1e 100644 --- a/apps/roam/scripts/compile.ts +++ b/apps/roam/scripts/compile.ts @@ -129,6 +129,10 @@ export const compile = ({ outdir, bundle: true, format, + define: { + SUPABASE_URL: JSON.stringify(process.env.SUPABASE_URL!), + SUPABASE_ANON_KEY: JSON.stringify(process.env.SUPABASE_ANON_KEY!) + }, sourcemap: process.env.NODE_ENV === "production" ? undefined : "inline", minify: process.env.NODE_ENV === "production", entryNames: out, diff --git a/apps/roam/tsconfig.json b/apps/roam/tsconfig.json index e25fb5139..5945a30d7 100644 --- a/apps/roam/tsconfig.json +++ b/apps/roam/tsconfig.json @@ -12,7 +12,6 @@ "module": "ESNext", "moduleResolution": "Node", "forceConsistentCasingInFileNames": true, - "jsx": "react", "noUncheckedIndexedAccess": false } diff --git a/apps/website/app/utils/supabase/client.ts b/apps/website/app/utils/supabase/client.ts new file mode 100644 index 000000000..e37f94f93 --- /dev/null +++ b/apps/website/app/utils/supabase/client.ts @@ -0,0 +1,14 @@ +import { createClient as createSupabaseClient } from "@supabase/supabase-js"; +import { Database } from "@repo/database/types.gen.ts"; + +// Inspired by https://supabase.com/ui/docs/nextjs/password-based-auth + +export const createClient = () => { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!url || !key) { + throw new Error("Missing required Supabase environment variables"); + } + return createSupabaseClient(url, key); +}; diff --git a/apps/website/app/utils/supabase/middleware.ts b/apps/website/app/utils/supabase/middleware.ts new file mode 100644 index 000000000..24b3363d3 --- /dev/null +++ b/apps/website/app/utils/supabase/middleware.ts @@ -0,0 +1,70 @@ +import { createServerClient } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +// Inspired by https://supabase.com/ui/docs/nextjs/password-based-auth + +export const updateSession = async (request: NextRequest) => { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + throw new Error("Missing required Supabase environment variables"); + } + + let supabaseResponse = NextResponse.next({ request }); + + const supabase = createServerClient(supabaseUrl, supabaseKey, { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => + request.cookies.set(name, value), + ); + supabaseResponse = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options), + ); + }, + }, + }); + + // Do not run code between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + // IMPORTANT: DO NOT REMOVE auth.getUser() + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if ( + !user && + !request.nextUrl.pathname.startsWith("/login") && + !request.nextUrl.pathname.startsWith("/auth") + ) { + // no user, potentially respond by redirecting the user to the login page + const url = request.nextUrl.clone(); + url.pathname = "/auth/login"; + return NextResponse.redirect(url); + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. + // If you're creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse; +}; diff --git a/apps/website/app/utils/supabase/server.ts b/apps/website/app/utils/supabase/server.ts index 89eedba1a..40195d903 100644 --- a/apps/website/app/utils/supabase/server.ts +++ b/apps/website/app/utils/supabase/server.ts @@ -2,6 +2,8 @@ import { createServerClient, type CookieOptions } from "@supabase/ssr"; import { cookies } from "next/headers"; import { Database } from "@repo/database/types.gen.ts"; +// Inspired by https://supabase.com/ui/docs/nextjs/password-based-auth + export const createClient = async () => { const cookieStore = await cookies(); const supabaseUrl = process.env.SUPABASE_URL; diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index cb1aa3c02..8171b7a50 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@repo/typescript-config/nextjs.json", "compilerOptions": { + "baseUrl": ".", "paths": { "~/*": ["./app/*"] }, diff --git a/package-lock.json b/package-lock.json index 90fb5761d..956aaa4c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -728,6 +728,8 @@ "@octokit/auth-app": "^7.1.4", "@octokit/core": "^6.1.3", "@repo/types": "*", + "@supabase/auth-js": "^2.70.0", + "@supabase/supabase-js": "^2.50.0", "@tldraw/tldraw": "^2.0.0-alpha.12", "@vercel/blob": "^0.27.0", "contrast-color": "^1.0.1", @@ -9065,22 +9067,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@stitches/core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", + "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { - "version": "2.69.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", - "integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==", + "version": "2.70.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz", + "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, + "node_modules/@supabase/auth-ui-react": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-react/-/auth-ui-react-0.4.7.tgz", + "integrity": "sha512-Lp4FQGFh7BMX1Y/BFaUKidbryL7eskj1fl6Lby7BeHrTctbdvDbCMjVKS8wZ2rxuI8FtPS2iU900fSb70FHknQ==", + "dependencies": { + "@stitches/core": "^1.2.8", + "@supabase/auth-ui-shared": "0.1.8", + "prop-types": "^15.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.21.0" + } + }, + "node_modules/@supabase/auth-ui-react/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@supabase/auth-ui-react/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@supabase/auth-ui-react/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@supabase/auth-ui-shared": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-shared/-/auth-ui-shared-0.1.8.tgz", + "integrity": "sha512-ouQ0DjKcEFg+0gZigFIEgu01V3e6riGZPzgVD0MJsCBNsMsiDT74+GgCEIElMUpTGkwSja3xLwdFRFgMNFKcjg==", + "license": "MIT", + "peerDependencies": { + "@supabase/supabase-js": "^2.21.0" + } + }, "node_modules/@supabase/functions-js": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -9090,7 +9154,6 @@ "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", "license": "MIT", - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9103,22 +9166,21 @@ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "node_modules/@supabase/realtime-js": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", - "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.15.tgz", + "integrity": "sha512-HQKRnwAqdVqJW/P9TjKVK+/ETpW4yQ8tyDPPtRMKOH4Uh3vQD74vmj353CYs8+YwVBKubeUOOEpI9CT8mT4obw==", "license": "MIT", - "peer": true, "dependencies": { - "@supabase/node-fetch": "^2.6.14", - "@types/phoenix": "^1.5.4", - "@types/ws": "^8.5.10", - "ws": "^8.18.0" + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "isows": "^1.0.7", + "ws": "^8.18.2" } }, "node_modules/@supabase/ssr": { @@ -9138,23 +9200,21 @@ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "node_modules/@supabase/supabase-js": { - "version": "2.49.8", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.8.tgz", - "integrity": "sha512-zzBQLgS/jZs7btWcIAc7V5yfB+juG7h0AXxKowMJuySsO5vK+F7Vp+HCa07Z+tu9lZtr3sT9fofkc86bdylmtw==", + "version": "2.50.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.2.tgz", + "integrity": "sha512-+27xlGgw7VyfwXXe+OiDJQosJNS+PPtjj1EnLR4uk+PKKZ91RA0/8NbIQybe6AGPanAaPtgOFFMMCArC6fZ++Q==", "license": "MIT", - "peer": true, "dependencies": { - "@supabase/auth-js": "2.69.1", + "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", - "@supabase/realtime-js": "2.11.2", + "@supabase/realtime-js": "2.11.15", "@supabase/storage-js": "2.7.1" } }, @@ -9435,8 +9495,7 @@ "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.14", @@ -9509,7 +9568,6 @@ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -15088,6 +15146,21 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/iterator.prototype": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", @@ -20443,7 +20516,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -20575,6 +20647,8 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { + "@supabase/auth-ui-react": "0.4.7", + "@supabase/supabase-js": "^2.50.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.468.0", diff --git a/packages/ui/package.json b/packages/ui/package.json index c574a7ce1..0e3cf5643 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,8 +22,8 @@ "ui": "npx shadcn@latest" }, "devDependencies": { - "@repo/tailwind-config": "*", "@repo/eslint-config": "*", + "@repo/tailwind-config": "*", "@repo/typescript-config": "*", "@turbo/gen": "^1.12.4", "@types/eslint": "^8.56.5", @@ -34,6 +34,8 @@ "typescript": "5.5.4" }, "dependencies": { + "@supabase/auth-ui-react": "0.4.7", + "@supabase/supabase-js": "^2.50.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.468.0", diff --git a/packages/ui/src/lib/supabase/client.ts b/packages/ui/src/lib/supabase/client.ts new file mode 100644 index 000000000..a07491c13 --- /dev/null +++ b/packages/ui/src/lib/supabase/client.ts @@ -0,0 +1,14 @@ +import { createClient as createSupabaseClient } from "@supabase/supabase-js"; +import { Database } from "@repo/database/types.gen.ts"; + +declare const SUPABASE_URL: string; +declare const SUPABASE_ANON_KEY: string; + +// Inspired by https://supabase.com/ui/docs/react/password-based-auth + +export const createClient = () => { + return createSupabaseClient( + SUPABASE_URL, + SUPABASE_ANON_KEY, + ); +}; diff --git a/turbo.json b/turbo.json index c7a293272..d5aef7014 100644 --- a/turbo.json +++ b/turbo.json @@ -9,7 +9,9 @@ "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "NODE_ENV", - "BLOB_READ_WRITE_TOKEN" + "BLOB_READ_WRITE_TOKEN", + "SUPABASE_ANON_KEY", + "SUPABASE_URL" ], "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], @@ -31,7 +33,9 @@ "POSTGRES_URL", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", - "GEMINI_API_KEY" + "GEMINI_API_KEY", + "SUPABASE_ANON_KEY", + "SUPABASE_URL" ], "cache": false, "persistent": true, @@ -54,7 +58,9 @@ "POSTGRES_URL", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", - "GEMINI_API_KEY" + "GEMINI_API_KEY", + "SUPABASE_ANON_KEY", + "SUPABASE_URL" ] }, "publish": { @@ -64,7 +70,9 @@ "GITHUB_TOKEN", "APP_PRIVATE_KEY", "APP_ID", - "NODE_ENV" + "NODE_ENV", + "SUPABASE_ANON_KEY", + "SUPABASE_URL" ] } }