From b1e8272d5ab384d539731a3410599bf4efc9fefe Mon Sep 17 00:00:00 2001 From: Kevin Stubbs Date: Mon, 19 Aug 2024 21:45:57 +0300 Subject: [PATCH 1/2] WIP using dashboard JWT tokens for tokens. --- packages/cli/src/cli/commands/login.ts | 95 +++++++++++++++++++-- packages/cli/src/cli/commands/logout.ts | 7 +- packages/cli/src/cli/commands/whoAmI.ts | 31 ++++++- packages/cli/src/lib/addonApiHelper.ts | 107 +++++++++++++++--------- packages/cli/src/lib/localStorage.ts | 95 ++++++++++++++------- 5 files changed, 254 insertions(+), 81 deletions(-) diff --git a/packages/cli/src/cli/commands/login.ts b/packages/cli/src/cli/commands/login.ts index a239fa88..f7bc16a8 100644 --- a/packages/cli/src/cli/commands/login.ts +++ b/packages/cli/src/cli/commands/login.ts @@ -3,7 +3,7 @@ import http from "http"; import { dirname, join } from "path"; import url, { fileURLToPath } from "url"; import { parseJwt } from "@pantheon-systems/pcc-sdk-core"; -import { OAuth2Client } from "google-auth-library"; +import { Credentials, OAuth2Client } from "google-auth-library"; import nunjucks from "nunjucks"; import open from "open"; import ora from "ora"; @@ -11,7 +11,9 @@ import destroyer from "server-destroy"; import AddOnApiHelper from "../../lib/addonApiHelper"; import { getApiConfig } from "../../lib/apiConfig"; import { + CREDENTIAL_TYPE, getLocalAuthDetails, + NextJwtCredentials, persistAuthDetails, } from "../../lib/localStorage"; import { errorHandler } from "../exceptions"; @@ -20,13 +22,17 @@ nunjucks.configure({ autoescape: true }); const OAUTH_SCOPES = ["https://www.googleapis.com/auth/userinfo.email"]; -function login(extraScopes: string[]): Promise { +export function loginOAuth(extraScopes: string[]): Promise { return new Promise( // eslint-disable-next-line no-async-promise-executor -- Handling promise rejection in the executor async (resolve, reject) => { const spinner = ora("Logging you in...").start(); try { - const authData = await getLocalAuthDetails(extraScopes); + const authData = (await getLocalAuthDetails( + CREDENTIAL_TYPE.OAUTH, + extraScopes, + )) as Credentials | null; + if (authData) { const scopes = authData.scope?.split(" "); @@ -70,7 +76,7 @@ function login(extraScopes: string[]): Promise { ); const credentials = await AddOnApiHelper.getToken(code as string); const jwtPayload = parseJwt(credentials.id_token as string); - await persistAuthDetails(credentials); + await persistAuthDetails(credentials, CREDENTIAL_TYPE.OAUTH); res.end( nunjucks.renderString(content.toString(), { @@ -102,7 +108,86 @@ function login(extraScopes: string[]): Promise { }, ); } -export default errorHandler(login); + +function login(): Promise { + return new Promise( + // eslint-disable-next-line no-async-promise-executor -- Handling promise rejection in the executor + async (resolve, reject) => { + const spinner = ora("Logging you in...").start(); + try { + const authData = (await getLocalAuthDetails( + CREDENTIAL_TYPE.NEXT_JWT, + )) as NextJwtCredentials | null; + if (authData) { + console.log("already exists", { authData }); + // spinner.succeed( + // `You are already logged in as ${authData.email}.`, + // ); + // return resolve(); + } + + const server = http.createServer(async (req, res) => { + try { + if (!req.url) { + throw new Error("No URL path provided"); + } + + if (req.url.indexOf("/auth-success") !== -1) { + const qs = new url.URL(req.url, "http://localhost:3030") + .searchParams; + const token = qs.get("code") as string; + const email = qs.get("email") as string; + const expiration = qs.get("expiration") as string; + const currDir = dirname(fileURLToPath(import.meta.url)); + const content = readFileSync( + join(currDir, "../templates/loginSuccess.html"), + ); + + await persistAuthDetails( + { + token, + email, + expiration, + }, + CREDENTIAL_TYPE.NEXT_JWT, + ); + + res.end( + nunjucks.renderString(content.toString(), { + email: email, + }), + ); + server.destroy(); + + spinner.succeed(`You are successfully logged in as ${email}`); + resolve(); + } else { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello World\n"); + return; + } + } catch (e) { + spinner.fail(); + reject(e); + } + }); + + destroyer(server); + + server.listen(3030, () => { + // const apiConfig = await getApiConfig(); + open("http://localhost:3000/auth/cli", { wait: true }).then((cp) => + cp.kill(), + ); + }); + } catch (e) { + spinner.fail(); + reject(e); + } + }, + ); +} +export default errorHandler(login); export const LOGIN_EXAMPLES = [ { description: "Login the user", command: "$0 login" }, ]; diff --git a/packages/cli/src/cli/commands/logout.ts b/packages/cli/src/cli/commands/logout.ts index 1a4feb97..4334ff69 100644 --- a/packages/cli/src/cli/commands/logout.ts +++ b/packages/cli/src/cli/commands/logout.ts @@ -1,12 +1,15 @@ import { existsSync, rmSync } from "fs"; import ora from "ora"; -import { AUTH_FILE_PATH } from "../../lib/localStorage"; +import { AUTH_FOLDER_PATH } from "../../lib/localStorage"; import { errorHandler } from "../exceptions"; const logout = async () => { const spinner = ora("Logging you out...").start(); try { - if (existsSync(AUTH_FILE_PATH)) rmSync(AUTH_FILE_PATH); + if (existsSync(AUTH_FOLDER_PATH)) + rmSync(AUTH_FOLDER_PATH, { + recursive: true, + }); spinner.succeed("Successfully logged you out from PPC client!"); } catch (e) { spinner.fail(); diff --git a/packages/cli/src/cli/commands/whoAmI.ts b/packages/cli/src/cli/commands/whoAmI.ts index 7bf6368d..9cb17198 100644 --- a/packages/cli/src/cli/commands/whoAmI.ts +++ b/packages/cli/src/cli/commands/whoAmI.ts @@ -1,16 +1,39 @@ import { parseJwt } from "@pantheon-systems/pcc-sdk-core"; import chalk from "chalk"; -import { getLocalAuthDetails } from "../../lib/localStorage"; +import { Credentials } from "google-auth-library"; +import { + CREDENTIAL_TYPE, + getLocalAuthDetails, + NextJwtCredentials, +} from "../../lib/localStorage"; import { errorHandler } from "../exceptions"; const printWhoAmI = async () => { try { - const authData = await getLocalAuthDetails(); - if (!authData) { + const nextJwt = (await getLocalAuthDetails( + CREDENTIAL_TYPE.NEXT_JWT, + )) as NextJwtCredentials | null; + if (!nextJwt) { console.log("You aren't logged in."); + } else { + console.log(`You're logged in as ${nextJwt.email}`); + } + } catch (e) { + chalk.red("Something went wrong - couldn't retrieve auth info."); + throw e; + } + + try { + const authData = (await getLocalAuthDetails( + CREDENTIAL_TYPE.OAUTH, + )) as Credentials | null; + if (!authData) { + console.log( + "You aren't logged into oauth. For some actions, the oauth connection isn't necessary. If you run a command that requires it, you will be only prompted to log in with oauth at that point.", + ); } else { const jwtPayload = parseJwt(authData.id_token as string); - console.log(`You're logged in as ${jwtPayload.email}`); + console.log(`Oauth: You're logged in as ${jwtPayload.email}`); } } catch (e) { chalk.red("Something went wrong - couldn't retrieve auth info."); diff --git a/packages/cli/src/lib/addonApiHelper.ts b/packages/cli/src/lib/addonApiHelper.ts index 968f2217..bf1f1d8f 100644 --- a/packages/cli/src/lib/addonApiHelper.ts +++ b/packages/cli/src/lib/addonApiHelper.ts @@ -3,10 +3,14 @@ import axios, { AxiosError, HttpStatusCode } from "axios"; import { Credentials } from "google-auth-library"; import ora from "ora"; import queryString from "query-string"; -import login from "../cli/commands/login"; +import login, { loginOAuth } from "../cli/commands/login"; import { HTTPNotFound, UserNotLoggedIn } from "../cli/exceptions"; import { getApiConfig } from "./apiConfig"; -import { getLocalAuthDetails } from "./localStorage"; +import { + CREDENTIAL_TYPE, + getLocalAuthDetails, + NextJwtCredentials, +} from "./localStorage"; import { toKebabCase } from "./utils"; class AddOnApiHelper { @@ -41,19 +45,44 @@ class AddOnApiHelper { } } + static async getNextJwt(): Promise { + let authDetails = (await getLocalAuthDetails( + CREDENTIAL_TYPE.NEXT_JWT, + )) as NextJwtCredentials | null; + + // If auth details not found, try user logging in + if (!authDetails) { + const prevOra = ora().stopAndPersist(); + await login(); + prevOra.start(); + authDetails = (await getLocalAuthDetails( + CREDENTIAL_TYPE.NEXT_JWT, + )) as NextJwtCredentials | null; + if (!authDetails) throw new UserNotLoggedIn(); + } + + return authDetails?.token; + } + static async getIdToken( requiredScopes?: string[], withAuthToken?: true, ): Promise<{ idToken: string; oauthToken: string }>; static async getIdToken(requiredScopes?: string[], withAuthToken?: boolean) { - let authDetails = await getLocalAuthDetails(requiredScopes); + let authDetails = (await getLocalAuthDetails( + CREDENTIAL_TYPE.OAUTH, + requiredScopes, + )) as Credentials | null; // If auth details not found, try user logging in if (!authDetails) { const prevOra = ora().stopAndPersist(); - await login(requiredScopes || []); + await loginOAuth(requiredScopes || []); prevOra.start(); - authDetails = await getLocalAuthDetails(requiredScopes); + authDetails = (await getLocalAuthDetails( + CREDENTIAL_TYPE.OAUTH, + requiredScopes, + )) as Credentials | null; if (!authDetails) throw new UserNotLoggedIn(); } @@ -92,7 +121,7 @@ class AddOnApiHelper { fieldTitle: string, fieldType: string, ): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.post( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/metadata`, @@ -105,7 +134,7 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, "Content-Type": "application/json", }, }, @@ -230,7 +259,7 @@ class AddOnApiHelper { static async createApiKey({ siteId, }: { siteId?: string } = {}): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.post( (await getApiConfig()).API_KEY_ENDPOINT, @@ -239,7 +268,7 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -247,11 +276,11 @@ class AddOnApiHelper { } static async listApiKeys(): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.get((await getApiConfig()).API_KEY_ENDPOINT, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }); @@ -259,12 +288,12 @@ class AddOnApiHelper { } static async revokeApiKey(id: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); try { await axios.delete(`${(await getApiConfig()).API_KEY_ENDPOINT}/${id}`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }); } catch (err) { @@ -277,14 +306,14 @@ class AddOnApiHelper { } static async createSite(url: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.post( (await getApiConfig()).SITE_ENDPOINT, { name: "", url, emailList: "" }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -296,7 +325,7 @@ class AddOnApiHelper { transferToSiteId: string | null | undefined, force: boolean, ): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.delete( queryString.stringifyUrl({ @@ -308,7 +337,7 @@ class AddOnApiHelper { }), { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -320,11 +349,11 @@ class AddOnApiHelper { }: { withConnectionStatus?: boolean; }): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.get((await getApiConfig()).SITE_ENDPOINT, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, params: { withConnectionStatus, @@ -350,27 +379,27 @@ class AddOnApiHelper { } static async updateSite(id: string, url: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.patch( `${(await getApiConfig()).SITE_ENDPOINT}/${id}`, { url }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); } static async getServersideComponentSchema(id: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -380,7 +409,7 @@ class AddOnApiHelper { id: string, componentSchema: typeof SmartComponentMapZod, ): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.post( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, @@ -389,39 +418,39 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); } static async removeComponentSchema(id: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.delete( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); } static async listAdmins(id: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); return ( await axios.get(`${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }) ).data; } static async addAdmin(id: string, email: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.patch( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, @@ -430,18 +459,18 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); } static async removeAdmin(id: string, email: string): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); await axios.delete(`${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, data: { email, @@ -463,7 +492,7 @@ class AddOnApiHelper { preferredEvents?: string[]; }, ): Promise { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const configuredWebhook = webhookUrl || webhookSecret || preferredEvents; @@ -481,7 +510,7 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -497,13 +526,13 @@ class AddOnApiHelper { offset?: number; }, ) { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/webhookLogs`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, params: { limit, @@ -516,13 +545,13 @@ class AddOnApiHelper { } static async fetchAvailableWebhookEvents(siteId: string) { - const idToken = await this.getIdToken(); + const jwtToken = await this.getNextJwt(); const resp = await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/availableWebhookEvents`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); diff --git a/packages/cli/src/lib/localStorage.ts b/packages/cli/src/lib/localStorage.ts index 80f3724e..9f08cca1 100644 --- a/packages/cli/src/lib/localStorage.ts +++ b/packages/cli/src/lib/localStorage.ts @@ -6,48 +6,80 @@ import { PCC_ROOT_DIR } from "../constants"; import { Config } from "../types/config"; import AddOnApiHelper from "./addonApiHelper"; -export const AUTH_FILE_PATH = path.join(PCC_ROOT_DIR, "auth.json"); +export const AUTH_FOLDER_PATH = path.join(PCC_ROOT_DIR, "auth"); export const CONFIG_FILE_PATH = path.join(PCC_ROOT_DIR, "config.json"); +function getAuthFile(credentialType: CREDENTIAL_TYPE) { + return path.join(AUTH_FOLDER_PATH, `${credentialType}.json`); +} + +export enum CREDENTIAL_TYPE { + OAUTH = "OAUTH", + NEXT_JWT = "NEXT_JWT", +} + +export interface NextJwtCredentials { + token: string; + email: string; + expiration: string; +} + export const getLocalAuthDetails = async ( + credentialType: CREDENTIAL_TYPE, requiredScopes?: string[], -): Promise => { - let credentials: Credentials; +): Promise => { + let storedJSON; + try { - credentials = JSON.parse( - readFileSync(AUTH_FILE_PATH).toString(), - ) as Credentials; + storedJSON = JSON.parse( + readFileSync(getAuthFile(credentialType)).toString(), + ); } catch (_err) { return null; } - // Return null if required scope is not present - const grantedScopes = new Set(credentials.scope?.split(" ") || []); - if ( - requiredScopes && - requiredScopes.length > 0 && - !requiredScopes.every((i) => grantedScopes.has(i)) - ) { - return null; - } + if (credentialType == CREDENTIAL_TYPE.OAUTH) { + const credentials: Credentials = storedJSON as Credentials; + + // Return null if required scope is not present + const grantedScopes = new Set(credentials.scope?.split(" ") || []); + if ( + requiredScopes && + requiredScopes.length > 0 && + !requiredScopes.every((i) => grantedScopes.has(i)) + ) { + return null; + } + + // Check if token is expired + if (credentials.expiry_date) { + const currentTime = await AddOnApiHelper.getCurrentTime(); - // Check if token is expired - if (credentials.expiry_date) { - const currentTime = await AddOnApiHelper.getCurrentTime(); + if (currentTime < credentials.expiry_date) { + return credentials; + } + } - if (currentTime < credentials.expiry_date) { - return credentials; + try { + const newCred = await AddOnApiHelper.refreshToken( + credentials.refresh_token as string, + ); + persistAuthDetails(newCred, CREDENTIAL_TYPE.OAUTH); + return newCred; + } catch (_err) { + return null; } - } + } else { + const credentials: NextJwtCredentials = storedJSON as NextJwtCredentials; - try { - const newCred = await AddOnApiHelper.refreshToken( - credentials.refresh_token as string, - ); - persistAuthDetails(newCred); - return newCred; - } catch (_err) { - return null; + // Check if token is expired + if (credentials.expiration) { + if (Date.now() >= Date.parse(credentials.expiration)) { + return null; + } + } + + return credentials; } }; @@ -60,9 +92,10 @@ export const getLocalConfigDetails = async (): Promise => { }; export const persistAuthDetails = async ( - payload: Credentials, + payload: Credentials | NextJwtCredentials, + credentialType: CREDENTIAL_TYPE, ): Promise => { - await persistDetailsToFile(payload, AUTH_FILE_PATH); + await persistDetailsToFile(payload, getAuthFile(credentialType)); }; export const persistConfigDetails = async (payload: Config): Promise => { From c717308a251d60dfa29432326c93de893dd69310 Mon Sep 17 00:00:00 2001 From: Kevin Stubbs Date: Sun, 15 Sep 2024 19:15:37 +0200 Subject: [PATCH 2/2] Use only JWT credentials. --- packages/cli/src/cli/commands/login.ts | 109 ++---------------------- packages/cli/src/cli/commands/whoAmI.ts | 4 +- packages/cli/src/lib/addonApiHelper.ts | 91 +++++++------------- packages/cli/src/lib/localStorage.ts | 61 +++---------- 4 files changed, 50 insertions(+), 215 deletions(-) diff --git a/packages/cli/src/cli/commands/login.ts b/packages/cli/src/cli/commands/login.ts index f7bc16a8..d09f1974 100644 --- a/packages/cli/src/cli/commands/login.ts +++ b/packages/cli/src/cli/commands/login.ts @@ -2,18 +2,13 @@ import { readFileSync } from "fs"; import http from "http"; import { dirname, join } from "path"; import url, { fileURLToPath } from "url"; -import { parseJwt } from "@pantheon-systems/pcc-sdk-core"; -import { Credentials, OAuth2Client } from "google-auth-library"; import nunjucks from "nunjucks"; import open from "open"; import ora from "ora"; import destroyer from "server-destroy"; -import AddOnApiHelper from "../../lib/addonApiHelper"; -import { getApiConfig } from "../../lib/apiConfig"; import { - CREDENTIAL_TYPE, getLocalAuthDetails, - NextJwtCredentials, + JwtCredentials, persistAuthDetails, } from "../../lib/localStorage"; import { errorHandler } from "../exceptions"; @@ -22,104 +17,15 @@ nunjucks.configure({ autoescape: true }); const OAUTH_SCOPES = ["https://www.googleapis.com/auth/userinfo.email"]; -export function loginOAuth(extraScopes: string[]): Promise { - return new Promise( - // eslint-disable-next-line no-async-promise-executor -- Handling promise rejection in the executor - async (resolve, reject) => { - const spinner = ora("Logging you in...").start(); - try { - const authData = (await getLocalAuthDetails( - CREDENTIAL_TYPE.OAUTH, - extraScopes, - )) as Credentials | null; - - if (authData) { - const scopes = authData.scope?.split(" "); - - if ( - !extraScopes?.length || - extraScopes.find((x) => scopes?.includes(x)) - ) { - const jwtPayload = parseJwt(authData.id_token as string); - spinner.succeed( - `You are already logged in as ${jwtPayload.email}.`, - ); - return resolve(); - } - } - - const apiConfig = await getApiConfig(); - const oAuth2Client = new OAuth2Client({ - clientId: apiConfig.googleClientId, - redirectUri: apiConfig.googleRedirectUri, - }); - - // Generate the url that will be used for the consent dialog. - const authorizeUrl = oAuth2Client.generateAuthUrl({ - access_type: "offline", - scope: [...OAUTH_SCOPES, ...extraScopes], - }); - - const server = http.createServer(async (req, res) => { - try { - if (!req.url) { - throw new Error("No URL path provided"); - } - - if (req.url.indexOf("/oauth-redirect") > -1) { - const qs = new url.URL(req.url, "http://localhost:3030") - .searchParams; - const code = qs.get("code"); - const currDir = dirname(fileURLToPath(import.meta.url)); - const content = readFileSync( - join(currDir, "../templates/loginSuccess.html"), - ); - const credentials = await AddOnApiHelper.getToken(code as string); - const jwtPayload = parseJwt(credentials.id_token as string); - await persistAuthDetails(credentials, CREDENTIAL_TYPE.OAUTH); - - res.end( - nunjucks.renderString(content.toString(), { - email: jwtPayload.email, - }), - ); - server.destroy(); - - spinner.succeed( - `You are successfully logged in as ${jwtPayload.email}`, - ); - resolve(); - } - } catch (e) { - spinner.fail(); - reject(e); - } - }); - - destroyer(server); - - server.listen(3030, () => { - open(authorizeUrl, { wait: true }).then((cp) => cp.kill()); - }); - } catch (e) { - spinner.fail(); - reject(e); - } - }, - ); -} - function login(): Promise { return new Promise( // eslint-disable-next-line no-async-promise-executor -- Handling promise rejection in the executor async (resolve, reject) => { const spinner = ora("Logging you in...").start(); try { - const authData = (await getLocalAuthDetails( - CREDENTIAL_TYPE.NEXT_JWT, - )) as NextJwtCredentials | null; + const authData = (await getLocalAuthDetails()) as JwtCredentials | null; if (authData) { - console.log("already exists", { authData }); + console.log("already exists", JSON.stringify({ authData }, null, 4)); // spinner.succeed( // `You are already logged in as ${authData.email}.`, // ); @@ -135,7 +41,8 @@ function login(): Promise { if (req.url.indexOf("/auth-success") !== -1) { const qs = new url.URL(req.url, "http://localhost:3030") .searchParams; - const token = qs.get("code") as string; + const idToken = qs.get("idToken") as string; + const oauthToken = qs.get("oauthToken") as string; const email = qs.get("email") as string; const expiration = qs.get("expiration") as string; const currDir = dirname(fileURLToPath(import.meta.url)); @@ -145,11 +52,11 @@ function login(): Promise { await persistAuthDetails( { - token, + idToken, + oauthToken, email, expiration, - }, - CREDENTIAL_TYPE.NEXT_JWT, + } ); res.end( diff --git a/packages/cli/src/cli/commands/whoAmI.ts b/packages/cli/src/cli/commands/whoAmI.ts index 9cb17198..70eace22 100644 --- a/packages/cli/src/cli/commands/whoAmI.ts +++ b/packages/cli/src/cli/commands/whoAmI.ts @@ -4,7 +4,7 @@ import { Credentials } from "google-auth-library"; import { CREDENTIAL_TYPE, getLocalAuthDetails, - NextJwtCredentials, + JwtCredentials, } from "../../lib/localStorage"; import { errorHandler } from "../exceptions"; @@ -12,7 +12,7 @@ const printWhoAmI = async () => { try { const nextJwt = (await getLocalAuthDetails( CREDENTIAL_TYPE.NEXT_JWT, - )) as NextJwtCredentials | null; + )) as JwtCredentials | null; if (!nextJwt) { console.log("You aren't logged in."); } else { diff --git a/packages/cli/src/lib/addonApiHelper.ts b/packages/cli/src/lib/addonApiHelper.ts index 779b74ad..738cd552 100644 --- a/packages/cli/src/lib/addonApiHelper.ts +++ b/packages/cli/src/lib/addonApiHelper.ts @@ -7,9 +7,8 @@ import login, { loginOAuth } from "../cli/commands/login"; import { HTTPNotFound, UserNotLoggedIn } from "../cli/exceptions"; import { getApiConfig } from "./apiConfig"; import { - CREDENTIAL_TYPE, getLocalAuthDetails, - NextJwtCredentials, + JwtCredentials, } from "./localStorage"; import { toKebabCase } from "./utils"; @@ -45,10 +44,8 @@ class AddOnApiHelper { } } - static async getNextJwt(): Promise { - let authDetails = (await getLocalAuthDetails( - CREDENTIAL_TYPE.NEXT_JWT, - )) as NextJwtCredentials | null; + static async getAuthTokens(requiredScopes?: string[]): Promise { + let authDetails = (await getLocalAuthDetails()) as JwtCredentials | null; // If auth details not found, try user logging in if (!authDetails) { @@ -56,39 +53,11 @@ class AddOnApiHelper { await login(); prevOra.start(); authDetails = (await getLocalAuthDetails( - CREDENTIAL_TYPE.NEXT_JWT, - )) as NextJwtCredentials | null; + )) as JwtCredentials | null; if (!authDetails) throw new UserNotLoggedIn(); } - return authDetails?.token; - } - - static async getIdToken( - requiredScopes?: string[], - ): Promise<{ idToken: string; oauthToken: string }>; - static async getIdToken(requiredScopes?: string[], withAuthToken?: boolean) { - let authDetails = (await getLocalAuthDetails( - CREDENTIAL_TYPE.OAUTH, - requiredScopes, - )) as Credentials | null; - - // If auth details not found, try user logging in - if (!authDetails) { - const prevOra = ora().stopAndPersist(); - await loginOAuth(requiredScopes || []); - prevOra.start(); - authDetails = (await getLocalAuthDetails( - CREDENTIAL_TYPE.OAUTH, - requiredScopes, - )) as Credentials | null; - if (!authDetails) throw new UserNotLoggedIn(); - } - - return { - idToken: authDetails.id_token, - oauthToken: authDetails.access_token, - }; + return authDetails; } static async getDocument( @@ -96,7 +65,7 @@ class AddOnApiHelper { insertIfMissing = false, title?: string, ): Promise
{ - const { idToken, oauthToken } = await this.getIdToken(); + const { idToken, oauthToken } = await this.getAuthTokens(); const resp = await axios.get( `${(await getApiConfig()).DOCUMENT_ENDPOINT}/${documentId}`, @@ -123,7 +92,7 @@ class AddOnApiHelper { fieldTitle: string, fieldType: string, ): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.post( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/metadata`, @@ -154,7 +123,7 @@ class AddOnApiHelper { verbose?: boolean, ): Promise
{ - const { idToken, oauthToken } = await this.getIdToken(); + const { idToken, oauthToken } = await this.getAuthTokens(); if (verbose) { console.log("update document", { @@ -189,7 +158,7 @@ class AddOnApiHelper { } static async publishDocument(documentId: string) { - const { idToken, oauthToken } = await this.getIdToken([ + const { idToken, oauthToken } = await this.getAuthTokens([ "https://www.googleapis.com/auth/drive", ]); @@ -230,7 +199,7 @@ class AddOnApiHelper { baseUrl?: string; }, ): Promise { - const { idToken, oauthToken } = await this.getIdToken([ + const { idToken, oauthToken } = await this.getAuthTokens([ "https://www.googleapis.com/auth/drive", ]); @@ -260,7 +229,7 @@ class AddOnApiHelper { static async createApiKey({ siteId, }: { siteId?: string } = {}): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.post( (await getApiConfig()).API_KEY_ENDPOINT, @@ -277,7 +246,7 @@ class AddOnApiHelper { } static async listApiKeys(): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get((await getApiConfig()).API_KEY_ENDPOINT, { headers: { @@ -289,7 +258,7 @@ class AddOnApiHelper { } static async revokeApiKey(id: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); try { await axios.delete(`${(await getApiConfig()).API_KEY_ENDPOINT}/${id}`, { @@ -307,7 +276,7 @@ class AddOnApiHelper { } static async createSite(url: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.post( (await getApiConfig()).SITE_ENDPOINT, @@ -326,7 +295,7 @@ class AddOnApiHelper { transferToSiteId: string | null | undefined, force: boolean, ): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.delete( queryString.stringifyUrl({ @@ -350,7 +319,7 @@ class AddOnApiHelper { }: { withConnectionStatus?: boolean; }): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get((await getApiConfig()).SITE_ENDPOINT, { headers: { @@ -365,7 +334,7 @@ class AddOnApiHelper { } static async getSite(siteId: string): Promise { - const { idToken } = await this.getIdToken(); + const idToken = await this.getAuthTokens(); const resp = await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}`, @@ -380,7 +349,7 @@ class AddOnApiHelper { } static async updateSite(id: string, url: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.patch( `${(await getApiConfig()).SITE_ENDPOINT}/${id}`, @@ -394,7 +363,7 @@ class AddOnApiHelper { } static async getServersideComponentSchema(id: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, @@ -410,7 +379,7 @@ class AddOnApiHelper { id: string, componentSchema: typeof SmartComponentMapZod, ): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.post( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, @@ -426,7 +395,7 @@ class AddOnApiHelper { } static async removeComponentSchema(id: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.delete( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, @@ -439,7 +408,7 @@ class AddOnApiHelper { } static async listAdmins(id: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); return ( await axios.get(`${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, { @@ -451,7 +420,7 @@ class AddOnApiHelper { } static async addAdmin(id: string, email: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.patch( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, @@ -467,7 +436,7 @@ class AddOnApiHelper { } static async removeAdmin(id: string, email: string): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); await axios.delete(`${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, { headers: { @@ -480,7 +449,7 @@ class AddOnApiHelper { } static async listCollaborators(id: string): Promise { - const idToken = await this.getIdToken(); + const idToken = await this.getAuthTokens(); return ( await axios.get( @@ -495,7 +464,7 @@ class AddOnApiHelper { } static async addCollaborator(id: string, email: string): Promise { - const idToken = await this.getIdToken(); + const idToken = await this.getAuthTokens(); await axios.patch( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/collaborators`, @@ -511,7 +480,7 @@ class AddOnApiHelper { } static async removeCollaborator(id: string, email: string): Promise { - const idToken = await this.getIdToken(); + const idToken = await this.getAuthTokens(); await axios.delete( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/collaborators`, @@ -540,7 +509,7 @@ class AddOnApiHelper { preferredEvents?: string[]; }, ): Promise { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const configuredWebhook = webhookUrl || webhookSecret || preferredEvents; @@ -574,7 +543,7 @@ class AddOnApiHelper { offset?: number; }, ) { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/webhookLogs`, @@ -593,7 +562,7 @@ class AddOnApiHelper { } static async fetchAvailableWebhookEvents(siteId: string) { - const jwtToken = await this.getNextJwt(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/availableWebhookEvents`, diff --git a/packages/cli/src/lib/localStorage.ts b/packages/cli/src/lib/localStorage.ts index 9f08cca1..e3c9bce5 100644 --- a/packages/cli/src/lib/localStorage.ts +++ b/packages/cli/src/lib/localStorage.ts @@ -4,73 +4,34 @@ import { ensureFile, remove } from "fs-extra"; import { Credentials } from "google-auth-library"; import { PCC_ROOT_DIR } from "../constants"; import { Config } from "../types/config"; -import AddOnApiHelper from "./addonApiHelper"; export const AUTH_FOLDER_PATH = path.join(PCC_ROOT_DIR, "auth"); export const CONFIG_FILE_PATH = path.join(PCC_ROOT_DIR, "config.json"); -function getAuthFile(credentialType: CREDENTIAL_TYPE) { - return path.join(AUTH_FOLDER_PATH, `${credentialType}.json`); +function getAuthFile() { + return path.join(AUTH_FOLDER_PATH, "auth.json"); } -export enum CREDENTIAL_TYPE { - OAUTH = "OAUTH", - NEXT_JWT = "NEXT_JWT", -} - -export interface NextJwtCredentials { - token: string; +export interface JwtCredentials { + idToken: string; + oauthToken: string; email: string; expiration: string; } export const getLocalAuthDetails = async ( - credentialType: CREDENTIAL_TYPE, - requiredScopes?: string[], -): Promise => { +): Promise => { let storedJSON; try { storedJSON = JSON.parse( - readFileSync(getAuthFile(credentialType)).toString(), + readFileSync(getAuthFile()).toString(), ); } catch (_err) { return null; } - if (credentialType == CREDENTIAL_TYPE.OAUTH) { - const credentials: Credentials = storedJSON as Credentials; - - // Return null if required scope is not present - const grantedScopes = new Set(credentials.scope?.split(" ") || []); - if ( - requiredScopes && - requiredScopes.length > 0 && - !requiredScopes.every((i) => grantedScopes.has(i)) - ) { - return null; - } - - // Check if token is expired - if (credentials.expiry_date) { - const currentTime = await AddOnApiHelper.getCurrentTime(); - - if (currentTime < credentials.expiry_date) { - return credentials; - } - } - - try { - const newCred = await AddOnApiHelper.refreshToken( - credentials.refresh_token as string, - ); - persistAuthDetails(newCred, CREDENTIAL_TYPE.OAUTH); - return newCred; - } catch (_err) { - return null; - } - } else { - const credentials: NextJwtCredentials = storedJSON as NextJwtCredentials; + const credentials: JwtCredentials = storedJSON as JwtCredentials; // Check if token is expired if (credentials.expiration) { @@ -80,7 +41,6 @@ export const getLocalAuthDetails = async ( } return credentials; - } }; export const getLocalConfigDetails = async (): Promise => { @@ -92,10 +52,9 @@ export const getLocalConfigDetails = async (): Promise => { }; export const persistAuthDetails = async ( - payload: Credentials | NextJwtCredentials, - credentialType: CREDENTIAL_TYPE, + payload: Credentials | JwtCredentials, ): Promise => { - await persistDetailsToFile(payload, getAuthFile(credentialType)); + await persistDetailsToFile(payload, getAuthFile()); }; export const persistConfigDetails = async (payload: Config): Promise => {