UNPKG

@dataroadinc/setup-auth

Version:

CLI tool and programmatic API for automated OAuth setup across cloud platforms

239 lines (238 loc) 12.2 kB
import { ServiceUsageClient } from "@google-cloud/service-usage"; import { SetupAuthError } from "../../../utils/error.js"; import { getVercelClient } from "../../../utils/vercel/index.js"; import { GcpCloudCliClient } from "../../gcp/cloud-cli-client.js"; import { GcpOAuthBrandClient, GcpProjectManager } from "../../index.js"; import { PUBLIC_SERVICES, REQUIRED_SERVICES } from "../iam/constants.js"; import { GcpOAuthWebClientManager } from "./client.js"; export class GcpProjectOAuthSetup { constructor(identity, organizationId, projectId, quotaProjectId, oauthBrandName, platform, clientId, vercelProjectName, allowedDomains) { console.log(`DEBUG: GcpProjectOAuthSetup constructor received oauthBrandName: '${oauthBrandName}'`); this.initialized = false; this.identity = identity; this.brandClient = new GcpOAuthBrandClient(projectId, identity); this.projectId = projectId; this.quotaProjectId = quotaProjectId; this.oauthBrandName = oauthBrandName; this.platform = platform; this.vercelProjectName = vercelProjectName; this.defaultRedirectUri = this.getDefaultRedirectUri(); this.clientId = clientId; this.allowedDomains = allowedDomains; this.oauthClient = new GcpOAuthWebClientManager(this.projectId); this.projectManager = new GcpProjectManager(identity, organizationId); } async initialize() { if (this.initialized) { return; } await this.projectManager.initialize(); await this.brandClient.initialize(); if (!this.serviceUsageClient) { const authInstance = await this.identity.getAuthClient(); this.serviceUsageClient = new ServiceUsageClient({ auth: authInstance, quotaProjectId: this.quotaProjectId, }); } this.initialized = true; } async setupForProject() { const cli = new GcpCloudCliClient(); try { await cli.checkInstalled(); await cli.checkAlphaComponent(); await cli.autoAuthenticate(); } catch (err) { throw new SetupAuthError("GCP CLI automation failed: " + (err instanceof Error ? err.message : String(err)), { cause: err }); } await this.initialize(); await this.step_0_checkProject(); await this.step_1_enableServices(); await this.step_2_updateOrCreateBrand(); await this.step_3_determineRedirectUri(); await this.setupForVercelProject(); await this.setupForOpenNextProject(); await this.setupForNetlifyProject(); } async setupForVercelProject() { if (this.platform !== "vercel") return; console.log("\n--- Updating Vercel specifics ---"); if (!this.clientId) { throw new SetupAuthError("Cannot update Vercel: Client ID is missing."); } if (!this.clientSecret) { console.warn("Client Secret is not available (client likely pre-existed). Cannot update Vercel secret variable."); } console.log("Adding all active Vercel deployment redirect URIs..."); const vercelClient = await getVercelClient(); const deploymentUrls = await vercelClient.getDeployments(); const baseOrigin = this.defaultRedirectUri.split("/api/auth")[0]; const callbackPath = "/callback/google"; const allRedirectUris = [ ...new Set([ `${baseOrigin}${callbackPath}`, ...deploymentUrls.map((url) => `${url}${callbackPath}`), ]), ]; await this.oauthClient.updateRedirectUris(this.clientId, allRedirectUris); console.log("✅ Successfully updated redirect URIs (Placeholder)"); } async setupForOpenNextProject() { if (this.platform !== "opennext") return; console.log("\n--- Updating OpenNext specifics ---"); } async setupForNetlifyProject() { if (this.platform !== "netlify") return; console.log("\n--- Updating Netlify specifics ---"); } async step_0_checkProject() { console.log("\n--- Step 0: Check if the project is valid and create if needed... ---"); if (await this.projectManager.projectExists(this.projectId)) { console.log(`✅ GCP project ${this.projectId} already exists`); return; } console.log(`Creating GCP project ${this.projectId}...`); await this.projectManager.createProject(this.projectId); console.log(`✅ Created GCP project ${this.projectId}`); } async step_1_enableServices() { await this.initialize(); if (!this.serviceUsageClient) { throw new SetupAuthError("ServiceUsageClient failed to initialize"); } console.log("\n--- Step 1: Ensuring required services are enabled... ---"); try { const [services] = await this.serviceUsageClient.listServices({ parent: `projects/${this.projectId}`, filter: "state:ENABLED", }); const enabledApis = new Set(services .map((service) => service.name?.split("/").pop() || "") .filter(Boolean)); for (const publicApi of PUBLIC_SERVICES) { enabledApis.add(publicApi); } const apisToEnable = Object.values(REQUIRED_SERVICES).filter((api) => !enabledApis.has(api) && !PUBLIC_SERVICES.includes(api)); if (apisToEnable.length > 0) { console.log("APIs to enable:", apisToEnable); for (const api of apisToEnable) { console.log(`Enabling API: ${api}...`); const [operation] = await this.serviceUsageClient.enableService({ name: `projects/${this.projectId}/services/${api}`, }); await operation.promise(); console.log(`✅ API ${api} enabled successfully.`); await new Promise(resolve => setTimeout(resolve, 500)); } } else { console.log("All required APIs are already enabled."); } } catch (error) { console.error("Failed to enable required APIs using service account:", error); let reason = "Unknown Reason"; let details = error instanceof Error ? error.message : String(error); if (error && typeof error === "object") { if ("reason" in error) reason = String(error.reason); if ("details" in error) details = String(error.details); } throw new SetupAuthError(`API enablement failed (Reason: ${reason}). Ensure the service account has necessary permissions (like serviceusage.serviceUsageAdmin) and check project/org policies. Details: ${details}`, { cause: error instanceof Error ? error : new Error(details) }); } } async step_2_updateOrCreateBrand() { console.log("\n--- Step 2: Assuming Brand (Consent Screen) exists... ---"); console.log(`Proceeding with OAuth client setup assuming brand '${this.oauthBrandName}' exists.`); } async step_3_determineRedirectUri() { console.log("\n--- Step 3: Determine the redirect URI based on the platform... ---"); const envClientId = process.env.GCP_OAUTH_CLIENT_ID; const envClientSecret = process.env.GCP_OAUTH_CLIENT_SECRET; const hasValidClientId = envClientId && envClientId.trim() !== "" && envClientId !== "PLACEHOLDER"; const hasValidClientSecret = envClientSecret && envClientSecret.trim() !== "" && envClientSecret !== "PLACEHOLDER"; if (!hasValidClientId || !hasValidClientSecret) { console.log("OAuth credentials not found in .env.local. Creating a new OAuth client..."); const displayName = `${this.platform.charAt(0).toUpperCase() + this.platform.slice(1)} OAuth Client`; const redirectUris = [this.defaultRedirectUri]; const origins = []; try { const { clientId, clientSecret } = await this.oauthClient.createClient(displayName, redirectUris, origins); this.clientId = clientId; this.clientSecret = clientSecret; console.log("\n✅ OAuth client created successfully!"); console.log(`Client ID: ${clientId}`); console.log(`Client Secret: ${clientSecret}`); console.log("\nSaving OAuth credentials to .env.local..."); const { updateOrAddEnvVariable } = await import("../../../utils/env-handler.js"); await updateOrAddEnvVariable("GCP_OAUTH_CLIENT_ID", clientId); await updateOrAddEnvVariable("GCP_OAUTH_CLIENT_SECRET", clientSecret); console.log("✅ OAuth credentials saved to .env.local"); return; } catch (error) { throw new SetupAuthError("Failed to create OAuth client automatically.", { cause: error }); } } this.clientId = envClientId; this.clientSecret = envClientSecret; console.log(`Using OAuth client from .env.local: ${this.clientId}`); try { console.log("Verifying OAuth client configuration..."); const details = await this.oauthClient.getClientDetails(this.clientId); console.log(`Current redirect URIs: ${details.redirectUris.join(", ")}`); if (!details.redirectUris.includes(this.defaultRedirectUri)) { console.log(`Adding redirect URI: ${this.defaultRedirectUri}`); const updatedUris = [...details.redirectUris, this.defaultRedirectUri]; await this.oauthClient.updateRedirectUris(this.clientId, updatedUris); console.log("✅ Redirect URI added successfully"); } else { console.log("✅ Redirect URI already configured"); } } catch (error) { console.error("Error verifying OAuth client:", error); if (this.clientId.endsWith(".apps.googleusercontent.com")) { throw new SetupAuthError(`The OAuth client ${this.clientId} appears to be created via Google Cloud Console.\n\n` + "Console-created OAuth clients cannot be managed via gcloud CLI.\n" + "You have two options:\n" + "1. Continue managing this client manually in the Google Cloud Console, OR\n" + "2. Create a new OAuth client using this tool:\n" + " - Remove GCP_OAUTH_CLIENT_ID and GCP_OAUTH_CLIENT_SECRET from .env.local\n" + " - Run this command again to create a new OAuth client automatically\n" + " - Follow the instructions to retrieve the new client secret\n\n" + "For more information, see: https://support.google.com/cloud/answer/15549257", { cause: error }); } throw new SetupAuthError(`Failed to verify OAuth client ${this.clientId}. ` + "Please ensure the client ID in .env.local is correct and the client exists in your GCP project.", { cause: error }); } } getDefaultRedirectUri() { switch (this.platform) { case "vercel": if (!this.vercelProjectName) { throw new SetupAuthError("Vercel project name is required for Vercel platform"); } return `https://${this.vercelProjectName}.vercel.app/api/auth`; case "opennext": return process.env.PRODUCTION_URL ? `${process.env.PRODUCTION_URL}/api/auth` : `http://localhost:3000/api/auth`; case "netlify": return `https://${this.projectId}.netlify.app/api/auth`; default: throw new SetupAuthError(`Unsupported platform: ${this.platform}`); } } }