diff --git a/src/_utils.js b/src/_utils.js index 646eda8..231682e 100644 --- a/src/_utils.js +++ b/src/_utils.js @@ -4,7 +4,7 @@ import { dirname } from "node:path"; import { pathToFileURL } from "node:url"; import chalk from "chalk"; -import { GoogleAuth } from "google-auth-library"; +import { GoogleAuth, OAuth2Client } from "google-auth-library"; import { findUp } from "find-up"; const _is_js_config = (filename) => { @@ -65,10 +65,36 @@ export const get_auth = (path, scopes) => { const file = path.startsWith("~") ? path.replace("~", homedir()) : path; if (!existsSync(file)) { fatal_error(` - Could not open service account credentials at ${file}. - Reconfigure the "auth" properties in your configuration file or download the credentials file. + Could not open account credentials at ${file}. + Reconfigure the "auth" properties in your configuration file or create a credentials file. `); } - return new GoogleAuth({ keyFile: file, scopes }); + const auth = JSON.parse(readFileSync(file).toString()); + + if (auth.type === "service_account") { + return new GoogleAuth({ keyFile: file, scopes }); + } else if (auth.type === "oauth") { + if (!existsSync(auth.clientPath)) { + fatal_error(`Could not open OAuth client file at ${auth.clientPath}.`) + } else { + const { web: { client_id, client_secret, redirect_uris }} = JSON.parse(readFileSync(auth.clientPath).toString()); + + const client = new OAuth2Client( + client_id, + client_secret, + redirect_uris[0] + ); + + const token = structuredClone(auth); + delete token.type; + delete token.clientPath; + + client.setCredentials(token); + + return client; + } + } else { + fatal_error(`Could not parse authentication file type ${auth.type} at ${file}`); + } }; diff --git a/src/sink-auth.js b/src/sink-auth.js new file mode 100644 index 0000000..859685f --- /dev/null +++ b/src/sink-auth.js @@ -0,0 +1,85 @@ +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; +import { readFileSync, writeFileSync } from "node:fs"; +import { createServer } from "node:http"; + +import { program } from "commander"; +import { OAuth2Client } from "google-auth-library"; +import chalk from "chalk"; +import { success } from "./_utils.js"; + +const self = fileURLToPath(import.meta.url); + +const main = async () => { + const authPath = "~/.sink-google-auth-oauth-client.json"; + const absoluteAuthPath = authPath.startsWith("~") ? authPath.replace("~", homedir()) : authPath; + const auth = JSON.parse(readFileSync(absoluteAuthPath).toString()); + + const client = new OAuth2Client( + auth.web.client_id, + auth.web.client_secret, + auth.web.redirect_uris[0] + ); + + const authorizationUrl = client.generateAuthUrl({ + access_type: "offline", + scope: "https://www.googleapis.com/auth/drive.readonly" + }); + + console.log( + `Start the OAuth workflow at ${chalk.yellow(authorizationUrl)}.` + ); + + console.log( + "Note that you are expected to see an error screen after getting redirected to a localhost URL." + ) + + let server; + const sockets = new Set(); + + const token = await new Promise((resolve) => { + server = createServer(async (req) => { + const queryParams = new URL(req.url, "http://localhost:3000").searchParams; + const code = queryParams.get("code"); + const { tokens } = await client.getToken(code); + resolve(tokens); + }); + + server.listen(3000); + + server.on("connection", (socket) => { + sockets.add(socket); + + server.once("close", () => { + sockets.delete(socket); + }) + }) + }); + + for (const socket of sockets) { + socket.destroy(); + sockets.delete(socket); + } + + server.close(); + + const tokenPath = `${homedir()}/.sink-google-auth-oauth-token.json` + writeFileSync( + tokenPath, + JSON.stringify({ + type: "oauth", + clientPath: absoluteAuthPath, + ...token + }) + ); + + success(`A Google OAuth token has been generated at ${tokenPath}.`); +} + +if (process.argv[1] === self) { + program + .version("2.7.3") + .parse(); + + main(); +} \ No newline at end of file diff --git a/src/sink-text.js b/src/sink-text.js index d1a2ac2..d3efb2a 100644 --- a/src/sink-text.js +++ b/src/sink-text.js @@ -9,6 +9,7 @@ import { get_auth, write_file, has_filled_props, + fatal_error } from "./_utils.js"; export const fetchText = async ({ id, output, auth }) => { diff --git a/src/sink.js b/src/sink.js index 41207e7..d368e84 100755 --- a/src/sink.js +++ b/src/sink.js @@ -11,6 +11,7 @@ program .command("json", "fetch JSON files from Google Drive") .command("text", "fetch text files from Google Drive") .command("fetch", "fetch all Google Docs and Sheets") - .command("deploy", "deploy a build directory to AWS S3"); + .command("deploy", "deploy a build directory to AWS S3") + .command("auth", "authenticate with Google OAuth"); program.parse(process.argv);