diff --git a/packages/cli/src/cli/commands/login.ts b/packages/cli/src/cli/commands/login.ts index a239fa88..d09f1974 100644 --- a/packages/cli/src/cli/commands/login.ts +++ b/packages/cli/src/cli/commands/login.ts @@ -2,16 +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 { 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 { getLocalAuthDetails, + JwtCredentials, persistAuthDetails, } from "../../lib/localStorage"; import { errorHandler } from "../exceptions"; @@ -20,69 +17,61 @@ nunjucks.configure({ autoescape: true }); const OAUTH_SCOPES = ["https://www.googleapis.com/auth/userinfo.email"]; -function login(extraScopes: string[]): Promise { +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(extraScopes); + const authData = (await getLocalAuthDetails()) as JwtCredentials | 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(); - } + console.log("already exists", JSON.stringify({ authData }, null, 4)); + // spinner.succeed( + // `You are already logged in as ${authData.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) { + if (req.url.indexOf("/auth-success") !== -1) { const qs = new url.URL(req.url, "http://localhost:3030") .searchParams; - const code = qs.get("code"); + 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)); 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); + + await persistAuthDetails( + { + idToken, + oauthToken, + email, + expiration, + } + ); res.end( nunjucks.renderString(content.toString(), { - email: jwtPayload.email, + email: email, }), ); server.destroy(); - spinner.succeed( - `You are successfully logged in as ${jwtPayload.email}`, - ); + 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(); @@ -93,7 +82,10 @@ function login(extraScopes: string[]): Promise { destroyer(server); server.listen(3030, () => { - open(authorizeUrl, { wait: true }).then((cp) => cp.kill()); + // const apiConfig = await getApiConfig(); + open("http://localhost:3000/auth/cli", { wait: true }).then((cp) => + cp.kill(), + ); }); } catch (e) { spinner.fail(); @@ -102,7 +94,7 @@ function login(extraScopes: string[]): Promise { }, ); } -export default errorHandler(login); +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..70eace22 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, + JwtCredentials, +} 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 JwtCredentials | 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 21368711..738cd552 100644 --- a/packages/cli/src/lib/addonApiHelper.ts +++ b/packages/cli/src/lib/addonApiHelper.ts @@ -3,10 +3,13 @@ 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 { + getLocalAuthDetails, + JwtCredentials, +} from "./localStorage"; import { toKebabCase } from "./utils"; class AddOnApiHelper { @@ -41,26 +44,20 @@ class AddOnApiHelper { } } - static async getIdToken( - requiredScopes?: string[], - ): Promise<{ idToken: string; oauthToken: string }>; - static async getIdToken(requiredScopes?: string[]) { - let authDetails = await getLocalAuthDetails(requiredScopes); + static async getAuthTokens(requiredScopes?: string[]): Promise { + let authDetails = (await getLocalAuthDetails()) as JwtCredentials | null; // If auth details not found, try user logging in if (!authDetails) { - // Clears older spinner if any - ora().clear(); - - await login(requiredScopes || []); - authDetails = await getLocalAuthDetails(requiredScopes); + const prevOra = ora().stopAndPersist(); + await login(); + prevOra.start(); + authDetails = (await getLocalAuthDetails( + )) as JwtCredentials | null; if (!authDetails) throw new UserNotLoggedIn(); } - return { - idToken: authDetails.id_token, - oauthToken: authDetails.access_token, - }; + return authDetails; } static async getDocument( @@ -68,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}`, @@ -95,7 +92,7 @@ class AddOnApiHelper { fieldTitle: string, fieldType: string, ): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); await axios.post( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/metadata`, @@ -108,7 +105,7 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, "Content-Type": "application/json", }, }, @@ -126,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", { @@ -161,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", ]); @@ -202,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", ]); @@ -232,7 +229,7 @@ class AddOnApiHelper { static async createApiKey({ siteId, }: { siteId?: string } = {}): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.post( (await getApiConfig()).API_KEY_ENDPOINT, @@ -241,7 +238,7 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -249,11 +246,11 @@ class AddOnApiHelper { } static async listApiKeys(): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get((await getApiConfig()).API_KEY_ENDPOINT, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }); @@ -261,12 +258,12 @@ class AddOnApiHelper { } static async revokeApiKey(id: string): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); try { await axios.delete(`${(await getApiConfig()).API_KEY_ENDPOINT}/${id}`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }); } catch (err) { @@ -279,14 +276,14 @@ class AddOnApiHelper { } static async createSite(url: string): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.post( (await getApiConfig()).SITE_ENDPOINT, { name: "", url, emailList: "" }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -298,7 +295,7 @@ class AddOnApiHelper { transferToSiteId: string | null | undefined, force: boolean, ): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.delete( queryString.stringifyUrl({ @@ -310,7 +307,7 @@ class AddOnApiHelper { }), { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -322,11 +319,11 @@ class AddOnApiHelper { }: { withConnectionStatus?: boolean; }): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get((await getApiConfig()).SITE_ENDPOINT, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, params: { withConnectionStatus, @@ -337,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}`, @@ -352,27 +349,27 @@ class AddOnApiHelper { } static async updateSite(id: string, url: string): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); 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.getAuthTokens(); await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -382,7 +379,7 @@ class AddOnApiHelper { id: string, componentSchema: typeof SmartComponentMapZod, ): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); await axios.post( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/components`, @@ -391,39 +388,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.getAuthTokens(); 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.getAuthTokens(); 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.getAuthTokens(); await axios.patch( `${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, @@ -432,18 +429,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.getAuthTokens(); await axios.delete(`${(await getApiConfig()).SITE_ENDPOINT}/${id}/admins`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, data: { email, @@ -452,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( @@ -467,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`, @@ -483,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`, @@ -512,7 +509,7 @@ class AddOnApiHelper { preferredEvents?: string[]; }, ): Promise { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const configuredWebhook = webhookUrl || webhookSecret || preferredEvents; @@ -530,7 +527,7 @@ class AddOnApiHelper { }, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, }, ); @@ -546,13 +543,13 @@ class AddOnApiHelper { offset?: number; }, ) { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); const resp = await axios.get( `${(await getApiConfig()).SITE_ENDPOINT}/${siteId}/webhookLogs`, { headers: { - Authorization: `Bearer ${idToken}`, + Authorization: `Bearer ${jwtToken}`, }, params: { limit, @@ -565,13 +562,13 @@ class AddOnApiHelper { } static async fetchAvailableWebhookEvents(siteId: string) { - const { idToken } = await this.getIdToken(); + const jwtToken = await this.getAuthTokens(); 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..e3c9bce5 100644 --- a/packages/cli/src/lib/localStorage.ts +++ b/packages/cli/src/lib/localStorage.ts @@ -4,51 +4,43 @@ 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_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() { + return path.join(AUTH_FOLDER_PATH, "auth.json"); +} + +export interface JwtCredentials { + idToken: string; + oauthToken: string; + email: string; + expiration: string; +} + export const getLocalAuthDetails = async ( - 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()).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; - } - - // Check if token is expired - if (credentials.expiry_date) { - const currentTime = await AddOnApiHelper.getCurrentTime(); + const credentials: JwtCredentials = storedJSON as JwtCredentials; - if (currentTime < credentials.expiry_date) { - return credentials; + // Check if token is expired + if (credentials.expiration) { + if (Date.now() >= Date.parse(credentials.expiration)) { + return null; + } } - } - try { - const newCred = await AddOnApiHelper.refreshToken( - credentials.refresh_token as string, - ); - persistAuthDetails(newCred); - return newCred; - } catch (_err) { - return null; - } + return credentials; }; export const getLocalConfigDetails = async (): Promise => { @@ -60,9 +52,9 @@ export const getLocalConfigDetails = async (): Promise => { }; export const persistAuthDetails = async ( - payload: Credentials, + payload: Credentials | JwtCredentials, ): Promise => { - await persistDetailsToFile(payload, AUTH_FILE_PATH); + await persistDetailsToFile(payload, getAuthFile()); }; export const persistConfigDetails = async (payload: Config): Promise => {