From 8bfc69d777dda4bae334c25c9d6022289a5e8dae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:54:57 +0000 Subject: [PATCH 1/6] feat(auth-provider): add DB connection mode switch with Cloud SQL connector support Co-authored-by: evanpetzoldt <48631145+evanpetzoldt@users.noreply.github.com> Agent-Logs-Url: https://github.com/F3-Nation/f3-nation-auth/sessions/9c96ab8b-9202-488c-b587-e75804d4b908 --- auth-provider/.env.example | 8 + auth-provider/README.md | 8 + auth-provider/db/index.ts | 55 ++++- auth-provider/package-lock.json | 364 ++++++++++++++++++++++++++++++-- auth-provider/package.json | 1 + 5 files changed, 414 insertions(+), 22 deletions(-) diff --git a/auth-provider/.env.example b/auth-provider/.env.example index 0d652b8..387b8f9 100644 --- a/auth-provider/.env.example +++ b/auth-provider/.env.example @@ -1,4 +1,12 @@ DATABASE_URL=postgresql://[user]:[password]@[host]:[port]/[database] +DB_CONNECTION_MODE=direct + +# Required when DB_CONNECTION_MODE=connector +# CLOUD_SQL_CONNECTION_NAME=[project-id]:[region]:[instance-id] +# DB_USER=[database-user] +# DB_PASSWORD=[database-password] +# DB_NAME=[database-name] +# CLOUD_SQL_IP_TYPE=PUBLIC # `openssl rand -hex 32` NEXTAUTH_SECRET=**** diff --git a/auth-provider/README.md b/auth-provider/README.md index 238ab2c..2583ac7 100644 --- a/auth-provider/README.md +++ b/auth-provider/README.md @@ -69,6 +69,14 @@ NEXTAUTH_URL="http://localhost:3000" # Database DATABASE_URL="your-postgresql-connection-string" +DB_CONNECTION_MODE="direct" # "direct" (default) or "connector" + +# Required when DB_CONNECTION_MODE=connector +CLOUD_SQL_CONNECTION_NAME="project:region:instance" +DB_USER="your-db-user" +DB_PASSWORD="your-db-password" +DB_NAME="your-db-name" +CLOUD_SQL_IP_TYPE="PUBLIC" # PUBLIC or PRIVATE # Email Verification TWILIO_SENDGRID_API_KEY="your-sendgrid-api-key" diff --git a/auth-provider/db/index.ts b/auth-provider/db/index.ts index 4d2e13f..6dcb65c 100644 --- a/auth-provider/db/index.ts +++ b/auth-provider/db/index.ts @@ -1,9 +1,60 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; +import { Connector, IpAddressTypes } from '@google-cloud/cloud-sql-connector'; import * as schema from './schema'; -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, +let connector: Connector | null = null; + +async function createCloudSqlPool(): Promise { + const instanceConnectionName = process.env.CLOUD_SQL_CONNECTION_NAME; + const dbUser = process.env.DB_USER; + const dbPassword = process.env.DB_PASSWORD; + const dbName = process.env.DB_NAME; + + if (!instanceConnectionName || !dbUser || !dbName) { + throw new Error( + 'Cloud SQL Connector requires CLOUD_SQL_CONNECTION_NAME, DB_USER, and DB_NAME.' + ); + } + + const validTypes = Object.values(IpAddressTypes) as string[]; + const ipAddressType = validTypes.includes(process.env.CLOUD_SQL_IP_TYPE ?? '') + ? (process.env.CLOUD_SQL_IP_TYPE as IpAddressTypes) + : IpAddressTypes.PUBLIC; + + connector = new Connector(); + const clientOpts = await connector.getOptions({ + instanceConnectionName, + ipType: ipAddressType, + }); + + return new Pool({ + ...clientOpts, + user: dbUser, + password: dbPassword, + database: dbName, + }); +} + +function createDirectPool(): Pool { + const connectionString = process.env.DATABASE_URL; + + if (!connectionString) { + throw new Error('DATABASE_URL is missing. Cannot connect to the database.'); + } + + return new Pool({ connectionString }); +} + +async function createPool(): Promise { + const mode = process.env.DB_CONNECTION_MODE ?? 'direct'; + return mode === 'connector' ? createCloudSqlPool() : createDirectPool(); +} + +const pool = await createPool(); + +pool.on('error', err => { + console.error('Unexpected error on idle PostgreSQL client:', err); }); // Use drizzle to wrap the PG pool with schema types diff --git a/auth-provider/package-lock.json b/auth-provider/package-lock.json index 2b35c4f..cfb8da3 100644 --- a/auth-provider/package-lock.json +++ b/auth-provider/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@auth/drizzle-adapter": "^0.8.2", + "@google-cloud/cloud-sql-connector": "^1.9.2", "@tailwindcss/postcss": "^4", "base64-url": "^2.3.3", "drizzle-orm": "^0.44.3", @@ -1167,6 +1168,33 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google-cloud/cloud-sql-connector": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.9.2.tgz", + "integrity": "sha512-o2LYUAKHvNtgevMCOaMH7bCDnRcyC9Z8UtEG6aPwjN+yIld1I6QPu+6Iq4n+/CHC7P4o+UlsNpHa9Hf/oM81qQ==", + "license": "Apache-2.0", + "dependencies": { + "@googleapis/sqladmin": "^35.2.0", + "gaxios": "^7.1.4", + "google-auth-library": "^10.6.1", + "p-throttle": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@googleapis/sqladmin": { + "version": "35.2.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-35.2.0.tgz", + "integrity": "sha512-ajR9EGLs1pCkKfsXxfbVRnQ7ZPyktKNAuahHoU06CVKguWwQo3b9aFmq06PYnGk1oXc0+tlW+XEamNa/HF4pbQ==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3015,6 +3043,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3285,6 +3322,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/base64-url": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-2.3.3.tgz", @@ -3294,6 +3351,15 @@ "node": ">=6" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3318,6 +3384,12 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3383,7 +3455,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3397,7 +3468,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3562,6 +3632,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3620,7 +3699,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3857,7 +3935,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3868,6 +3945,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3961,7 +4047,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3971,7 +4056,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4009,7 +4093,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4559,6 +4642,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4620,6 +4709,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4700,6 +4812,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4719,7 +4843,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4756,11 +4879,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4785,7 +4935,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4869,11 +5018,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4951,7 +5141,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4980,7 +5169,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4989,6 +5177,19 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5519,6 +5720,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5569,6 +5779,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5908,7 +6139,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6001,7 +6231,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -6185,6 +6414,44 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -6223,7 +6490,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6433,6 +6699,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-throttle": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-7.0.0.tgz", + "integrity": "sha512-aio0v+S0QVkH1O+9x4dHtD4dgCExACcL+3EtNaGqC01GBudS9ijMuUsmN8OVScyV4OOp0jqdLShZFuSlbL/AsA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6734,6 +7012,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6923,6 +7216,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -7096,7 +7409,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7116,7 +7428,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7133,7 +7444,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7152,7 +7462,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8225,6 +8534,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -8234,6 +8549,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/auth-provider/package.json b/auth-provider/package.json index 163d0d5..a8cd3ec 100644 --- a/auth-provider/package.json +++ b/auth-provider/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@auth/drizzle-adapter": "^0.8.2", + "@google-cloud/cloud-sql-connector": "^1.9.2", "@tailwindcss/postcss": "^4", "base64-url": "^2.3.3", "drizzle-orm": "^0.44.3", From 053fa2e389220f1832a887e6ff3117ae3ff511da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:56:03 +0000 Subject: [PATCH 2/6] chore(auth-provider): wire connector env vars in app hosting and secrets script Co-authored-by: evanpetzoldt <48631145+evanpetzoldt@users.noreply.github.com> Agent-Logs-Url: https://github.com/F3-Nation/f3-nation-auth/sessions/9c96ab8b-9202-488c-b587-e75804d4b908 --- auth-provider/apphosting.yaml | 30 +++++++++ auth-provider/scripts/firebase-secrets.sh | 76 ++++++++++++++++++++--- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/auth-provider/apphosting.yaml b/auth-provider/apphosting.yaml index 4e54487..44dc8d2 100644 --- a/auth-provider/apphosting.yaml +++ b/auth-provider/apphosting.yaml @@ -8,11 +8,41 @@ runConfig: # Environment variables and secrets. env: + - variable: DB_CONNECTION_MODE + secret: provider-db-connection-mode + availability: + - BUILD + - RUNTIME - variable: DATABASE_URL secret: provider-database-url availability: - BUILD - RUNTIME + - variable: CLOUD_SQL_CONNECTION_NAME + secret: provider-cloud-sql-connection-name + availability: + - BUILD + - RUNTIME + - variable: DB_USER + secret: provider-db-user + availability: + - BUILD + - RUNTIME + - variable: DB_PASSWORD + secret: provider-db-password + availability: + - BUILD + - RUNTIME + - variable: DB_NAME + secret: provider-db-name + availability: + - BUILD + - RUNTIME + - variable: CLOUD_SQL_IP_TYPE + secret: provider-cloud-sql-ip-type + availability: + - BUILD + - RUNTIME - variable: NEXTAUTH_SECRET secret: provider-nextauth-secret availability: diff --git a/auth-provider/scripts/firebase-secrets.sh b/auth-provider/scripts/firebase-secrets.sh index 6ecbffe..2e48d67 100755 --- a/auth-provider/scripts/firebase-secrets.sh +++ b/auth-provider/scripts/firebase-secrets.sh @@ -1,8 +1,38 @@ #!/usr/bin/env bash # Configuration constants -SECRET_VARS=("DATABASE_URL" "NEXTAUTH_SECRET" "NEXTAUTH_URL" "NEXT_PUBLIC_NEXTAUTH_URL" "TWILIO_SENDGRID_API_KEY" "TWILIO_SENDGRID_TEMPLATE_ID" "EMAIL_VERIFICATION_SENDER" "NODE_ENV") -SECRET_IDS=("provider-database-url" "provider-nextauth-secret" "provider-nextauth-url" "provider-next-public-nextauth-url" "provider-twilio-sendgrid-api-key" "provider-twilio-sendgrid-template-id" "provider-email-verification-sender" "provider-node-env") +SECRET_VARS=( + "DB_CONNECTION_MODE" + "DATABASE_URL" + "CLOUD_SQL_CONNECTION_NAME" + "DB_USER" + "DB_PASSWORD" + "DB_NAME" + "CLOUD_SQL_IP_TYPE" + "NEXTAUTH_SECRET" + "NEXTAUTH_URL" + "NEXT_PUBLIC_NEXTAUTH_URL" + "TWILIO_SENDGRID_API_KEY" + "TWILIO_SENDGRID_TEMPLATE_ID" + "EMAIL_VERIFICATION_SENDER" + "NODE_ENV" +) +SECRET_IDS=( + "provider-db-connection-mode" + "provider-database-url" + "provider-cloud-sql-connection-name" + "provider-db-user" + "provider-db-password" + "provider-db-name" + "provider-cloud-sql-ip-type" + "provider-nextauth-secret" + "provider-nextauth-url" + "provider-next-public-nextauth-url" + "provider-twilio-sendgrid-api-key" + "provider-twilio-sendgrid-template-id" + "provider-email-verification-sender" + "provider-node-env" +) ##################################### # MAIN EXECUTION FUNCTION @@ -88,7 +118,7 @@ validate_env_file() { if [[ ! -f "$env_file" ]]; then log_error ".env.prod file not found at $env_file" log_error "Please create this file with your environment variables." - log_error "Required variables: DATABASE_URL, NEXTAUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, OAUTH_CLIENT_SECRET_*, etc." + log_error "Required variables: DB_CONNECTION_MODE, NEXTAUTH_SECRET, NEXTAUTH_URL, and other runtime secrets." return 1 fi @@ -129,36 +159,62 @@ load_environment_variables() { validate_environment_variables() { log_step "Validating required environment variables..." - for i in "${!SECRET_VARS[@]}"; do - local envvar="${SECRET_VARS[$i]}" + local mode="${DB_CONNECTION_MODE:-direct}" + local required_vars=( + "DB_CONNECTION_MODE" + "NEXTAUTH_SECRET" + "NEXTAUTH_URL" + "NEXT_PUBLIC_NEXTAUTH_URL" + "TWILIO_SENDGRID_API_KEY" + "TWILIO_SENDGRID_TEMPLATE_ID" + "EMAIL_VERIFICATION_SENDER" + "NODE_ENV" + ) + + if [[ "$mode" == "connector" ]]; then + required_vars+=("CLOUD_SQL_CONNECTION_NAME" "DB_USER" "DB_NAME") + else + required_vars+=("DATABASE_URL") + fi + + for envvar in "${required_vars[@]}"; do local value="${!envvar:-}" - + if [[ -z "$value" ]]; then log_error "$envvar is not set in .env.prod" log_error "Please add $envvar=your_value to your .env.prod file" return 1 fi - + # Check if variable contains placeholder values if [[ "$value" == *"YOUR_"* ]] || [[ "$value" == *"your-"* ]]; then log_warning "$envvar appears to contain placeholder values." log_error "Please update it with your actual value in .env.prod" return 1 fi - + # Check for problematic whitespace or newlines if [[ "$value" =~ ^[[:space:]] ]] || [[ "$value" =~ [[:space:]]$ ]]; then log_warning "$envvar has leading or trailing whitespace - this will be automatically trimmed" fi - + if [[ "$value" =~ $'\n' ]] || [[ "$value" =~ $'\r' ]]; then log_warning "$envvar contains newlines or carriage returns - these will be automatically removed" fi - + # Show cleaned value length for debugging local cleaned_value=$(echo "$value" | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') log_success "Found: $envvar (${#cleaned_value} chars after cleaning)" done + + local optional_vars=("DATABASE_URL" "CLOUD_SQL_CONNECTION_NAME" "DB_USER" "DB_PASSWORD" "DB_NAME" "CLOUD_SQL_IP_TYPE") + for envvar in "${optional_vars[@]}"; do + if [[ -n "${!envvar:-}" ]]; then + local optional_cleaned + optional_cleaned=$(echo "${!envvar}" | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + log_success "Found optional: $envvar (${#optional_cleaned} chars after cleaning)" + fi + done } ##################################### From d790be93994fc0488f1353754524b092a8eb5f32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:58:52 +0000 Subject: [PATCH 3/6] fix(auth-provider): harden DB init failure handling and connector validation Co-authored-by: evanpetzoldt <48631145+evanpetzoldt@users.noreply.github.com> Agent-Logs-Url: https://github.com/F3-Nation/f3-nation-auth/sessions/9c96ab8b-9202-488c-b587-e75804d4b908 --- auth-provider/db/index.ts | 49 ++++++++++++++++++++--- auth-provider/scripts/firebase-secrets.sh | 2 +- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/auth-provider/db/index.ts b/auth-provider/db/index.ts index 6dcb65c..82b953e 100644 --- a/auth-provider/db/index.ts +++ b/auth-provider/db/index.ts @@ -11,9 +11,9 @@ async function createCloudSqlPool(): Promise { const dbPassword = process.env.DB_PASSWORD; const dbName = process.env.DB_NAME; - if (!instanceConnectionName || !dbUser || !dbName) { + if (!instanceConnectionName || !dbUser || !dbPassword || !dbName) { throw new Error( - 'Cloud SQL Connector requires CLOUD_SQL_CONNECTION_NAME, DB_USER, and DB_NAME.' + 'Cloud SQL Connector requires CLOUD_SQL_CONNECTION_NAME, DB_USER, DB_PASSWORD, and DB_NAME.' ); } @@ -51,14 +51,51 @@ async function createPool(): Promise { return mode === 'connector' ? createCloudSqlPool() : createDirectPool(); } -const pool = await createPool(); +const poolPromise = createPool(); -pool.on('error', err => { - console.error('Unexpected error on idle PostgreSQL client:', err); +const poolProxy = { + async query(...args: Parameters) { + const pool = await poolPromise; + return pool.query(...args); + }, + async connect(...args: Parameters) { + const pool = await poolPromise; + return pool.connect(...args); + }, + async end(...args: Parameters) { + const pool = await poolPromise; + return pool.end(...args); + }, +} as unknown as Pool; + +poolPromise + .then(pool => { + pool.on('error', err => { + console.error('Unexpected error on idle PostgreSQL client:', err); + }); + }) + .catch(err => { + console.error('Failed to initialize PostgreSQL pool:', err); + process.exit(1); + }); + +async function closeConnector() { + if (connector) { + await connector.close(); + connector = null; + } +} + +process.once('SIGTERM', () => { + void closeConnector(); +}); + +process.once('SIGINT', () => { + void closeConnector(); }); // Use drizzle to wrap the PG pool with schema types -export const db = drizzle(pool, { schema }); +export const db = drizzle(poolProxy, { schema }); // Export the database type for use in adapters export type DB = typeof db; diff --git a/auth-provider/scripts/firebase-secrets.sh b/auth-provider/scripts/firebase-secrets.sh index 2e48d67..2468aea 100755 --- a/auth-provider/scripts/firebase-secrets.sh +++ b/auth-provider/scripts/firebase-secrets.sh @@ -172,7 +172,7 @@ validate_environment_variables() { ) if [[ "$mode" == "connector" ]]; then - required_vars+=("CLOUD_SQL_CONNECTION_NAME" "DB_USER" "DB_NAME") + required_vars+=("CLOUD_SQL_CONNECTION_NAME" "DB_USER" "DB_PASSWORD" "DB_NAME") else required_vars+=("DATABASE_URL") fi From 0b18c9ae93654a8744d4cae693d26fa721e2e320 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:02:33 +0000 Subject: [PATCH 4/6] fix(auth-provider): add DB_SCHEMA search_path support and lazy DB pool init for CI Co-authored-by: evanpetzoldt <48631145+evanpetzoldt@users.noreply.github.com> Agent-Logs-Url: https://github.com/F3-Nation/f3-nation-auth/sessions/c31b6414-a65c-49ef-ab8e-531f93287061 --- auth-provider/.env.example | 1 + auth-provider/README.md | 1 + auth-provider/app/page.tsx | 2 + auth-provider/apphosting.yaml | 5 ++ auth-provider/db/index.ts | 63 +++++++++++++++++------ auth-provider/scripts/firebase-secrets.sh | 3 ++ 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/auth-provider/.env.example b/auth-provider/.env.example index 387b8f9..9166ee3 100644 --- a/auth-provider/.env.example +++ b/auth-provider/.env.example @@ -1,5 +1,6 @@ DATABASE_URL=postgresql://[user]:[password]@[host]:[port]/[database] DB_CONNECTION_MODE=direct +DB_SCHEMA=public # Required when DB_CONNECTION_MODE=connector # CLOUD_SQL_CONNECTION_NAME=[project-id]:[region]:[instance-id] diff --git a/auth-provider/README.md b/auth-provider/README.md index 2583ac7..72c7545 100644 --- a/auth-provider/README.md +++ b/auth-provider/README.md @@ -70,6 +70,7 @@ NEXTAUTH_URL="http://localhost:3000" # Database DATABASE_URL="your-postgresql-connection-string" DB_CONNECTION_MODE="direct" # "direct" (default) or "connector" +DB_SCHEMA="public" # PostgreSQL schema/search_path # Required when DB_CONNECTION_MODE=connector CLOUD_SQL_CONNECTION_NAME="project:region:instance" diff --git a/auth-provider/app/page.tsx b/auth-provider/app/page.tsx index a77f2e9..0f91c76 100644 --- a/auth-provider/app/page.tsx +++ b/auth-provider/app/page.tsx @@ -7,6 +7,8 @@ import { users } from '@/db/schema'; import { eq } from 'drizzle-orm'; import SignOutButton from './components/SignOutButton'; +export const dynamic = 'force-dynamic'; + interface User { name: string; email: string; diff --git a/auth-provider/apphosting.yaml b/auth-provider/apphosting.yaml index 44dc8d2..d364bec 100644 --- a/auth-provider/apphosting.yaml +++ b/auth-provider/apphosting.yaml @@ -13,6 +13,11 @@ env: availability: - BUILD - RUNTIME + - variable: DB_SCHEMA + secret: provider-db-schema + availability: + - BUILD + - RUNTIME - variable: DATABASE_URL secret: provider-database-url availability: diff --git a/auth-provider/db/index.ts b/auth-provider/db/index.ts index 82b953e..1d5cb7f 100644 --- a/auth-provider/db/index.ts +++ b/auth-provider/db/index.ts @@ -4,6 +4,30 @@ import { Connector, IpAddressTypes } from '@google-cloud/cloud-sql-connector'; import * as schema from './schema'; let connector: Connector | null = null; +let poolPromise: Promise | null = null; + +function getSearchPath(): string { + const raw = process.env.DB_SCHEMA ?? 'public'; + const schemas = raw + .split(',') + .map(schema => schema.trim()) + .filter(Boolean); + + if (schemas.length === 0) { + throw new Error('DB_SCHEMA must include at least one schema name.'); + } + + const validSchemaName = /^[A-Za-z_][A-Za-z0-9_]*$/; + for (const schema of schemas) { + if (!validSchemaName.test(schema)) { + throw new Error( + 'DB_SCHEMA contains invalid schema names. Use comma-separated schema names with letters, numbers, and underscores only.' + ); + } + } + + return schemas.map(schema => `"${schema}"`).join(','); +} async function createCloudSqlPool(): Promise { const instanceConnectionName = process.env.CLOUD_SQL_CONNECTION_NAME; @@ -27,12 +51,14 @@ async function createCloudSqlPool(): Promise { instanceConnectionName, ipType: ipAddressType, }); + const searchPath = getSearchPath(); return new Pool({ ...clientOpts, user: dbUser, password: dbPassword, database: dbName, + options: `-c search_path=${searchPath}`, }); } @@ -43,7 +69,8 @@ function createDirectPool(): Pool { throw new Error('DATABASE_URL is missing. Cannot connect to the database.'); } - return new Pool({ connectionString }); + const searchPath = getSearchPath(); + return new Pool({ connectionString, options: `-c search_path=${searchPath}` }); } async function createPool(): Promise { @@ -51,34 +78,38 @@ async function createPool(): Promise { return mode === 'connector' ? createCloudSqlPool() : createDirectPool(); } -const poolPromise = createPool(); +async function getPool(): Promise { + if (!poolPromise) { + poolPromise = createPool(); + poolPromise + .then(pool => { + pool.on('error', err => { + console.error('Unexpected error on idle PostgreSQL client:', err); + }); + }) + .catch(err => { + console.error('Failed to initialize PostgreSQL pool:', err); + }); + } + + return poolPromise; +} const poolProxy = { async query(...args: Parameters) { - const pool = await poolPromise; + const pool = await getPool(); return pool.query(...args); }, async connect(...args: Parameters) { - const pool = await poolPromise; + const pool = await getPool(); return pool.connect(...args); }, async end(...args: Parameters) { - const pool = await poolPromise; + const pool = await getPool(); return pool.end(...args); }, } as unknown as Pool; -poolPromise - .then(pool => { - pool.on('error', err => { - console.error('Unexpected error on idle PostgreSQL client:', err); - }); - }) - .catch(err => { - console.error('Failed to initialize PostgreSQL pool:', err); - process.exit(1); - }); - async function closeConnector() { if (connector) { await connector.close(); diff --git a/auth-provider/scripts/firebase-secrets.sh b/auth-provider/scripts/firebase-secrets.sh index 2468aea..0734b82 100755 --- a/auth-provider/scripts/firebase-secrets.sh +++ b/auth-provider/scripts/firebase-secrets.sh @@ -3,6 +3,7 @@ # Configuration constants SECRET_VARS=( "DB_CONNECTION_MODE" + "DB_SCHEMA" "DATABASE_URL" "CLOUD_SQL_CONNECTION_NAME" "DB_USER" @@ -19,6 +20,7 @@ SECRET_VARS=( ) SECRET_IDS=( "provider-db-connection-mode" + "provider-db-schema" "provider-database-url" "provider-cloud-sql-connection-name" "provider-db-user" @@ -162,6 +164,7 @@ validate_environment_variables() { local mode="${DB_CONNECTION_MODE:-direct}" local required_vars=( "DB_CONNECTION_MODE" + "DB_SCHEMA" "NEXTAUTH_SECRET" "NEXTAUTH_URL" "NEXT_PUBLIC_NEXTAUTH_URL" From bd3ae1071a99fdaa455b25af42b046c2850feca0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:03:24 +0000 Subject: [PATCH 5/6] chore(auth-provider): clean up DB_SCHEMA variable naming Co-authored-by: evanpetzoldt <48631145+evanpetzoldt@users.noreply.github.com> Agent-Logs-Url: https://github.com/F3-Nation/f3-nation-auth/sessions/c31b6414-a65c-49ef-ab8e-531f93287061 --- auth-provider/db/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/auth-provider/db/index.ts b/auth-provider/db/index.ts index 1d5cb7f..10a5bfa 100644 --- a/auth-provider/db/index.ts +++ b/auth-provider/db/index.ts @@ -8,25 +8,25 @@ let poolPromise: Promise | null = null; function getSearchPath(): string { const raw = process.env.DB_SCHEMA ?? 'public'; - const schemas = raw + const schemaNames = raw .split(',') - .map(schema => schema.trim()) + .map(schemaName => schemaName.trim()) .filter(Boolean); - if (schemas.length === 0) { + if (schemaNames.length === 0) { throw new Error('DB_SCHEMA must include at least one schema name.'); } const validSchemaName = /^[A-Za-z_][A-Za-z0-9_]*$/; - for (const schema of schemas) { - if (!validSchemaName.test(schema)) { + for (const schemaName of schemaNames) { + if (!validSchemaName.test(schemaName)) { throw new Error( 'DB_SCHEMA contains invalid schema names. Use comma-separated schema names with letters, numbers, and underscores only.' ); } } - return schemas.map(schema => `"${schema}"`).join(','); + return schemaNames.map(schemaName => `"${schemaName}"`).join(','); } async function createCloudSqlPool(): Promise { From 8334ae882e9a133c894ab545820749b894259514 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:13:37 +0000 Subject: [PATCH 6/6] fix(auth-provider): avoid duplicate search_path override in direct DB URL mode Co-authored-by: evanpetzoldt <48631145+evanpetzoldt@users.noreply.github.com> Agent-Logs-Url: https://github.com/F3-Nation/f3-nation-auth/sessions/273603c9-4156-45e4-8bc3-bb8611cb4052 --- auth-provider/db/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/auth-provider/db/index.ts b/auth-provider/db/index.ts index 10a5bfa..99f14a3 100644 --- a/auth-provider/db/index.ts +++ b/auth-provider/db/index.ts @@ -29,6 +29,20 @@ function getSearchPath(): string { return schemaNames.map(schemaName => `"${schemaName}"`).join(','); } +function hasSearchPathInConnectionString(connectionString: string): boolean { + try { + const parsed = new URL(connectionString); + if (parsed.searchParams.has('search_path') || parsed.searchParams.has('currentSchema')) { + return true; + } + + const options = parsed.searchParams.get('options'); + return options ? /\bsearch_path\s*=/.test(decodeURIComponent(options)) : false; + } catch { + return false; + } +} + async function createCloudSqlPool(): Promise { const instanceConnectionName = process.env.CLOUD_SQL_CONNECTION_NAME; const dbUser = process.env.DB_USER; @@ -70,6 +84,10 @@ function createDirectPool(): Pool { } const searchPath = getSearchPath(); + if (hasSearchPathInConnectionString(connectionString)) { + return new Pool({ connectionString }); + } + return new Pool({ connectionString, options: `-c search_path=${searchPath}` }); }