Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist Account keys on login #375

Merged
merged 11 commits into from
Oct 8, 2024
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default [
"coverage/**/*",
"fsl/**/*",
"test/**/*",
".history",
],
},
...compat.extends("oclif", "plugin:prettier/recommended"),
Expand Down
8 changes: 5 additions & 3 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import chalk from "chalk";
import evalCommand from "./yargs-commands/eval.mjs";
import loginCommand from "./yargs-commands/login.mjs";
import schemaCommand from "./yargs-commands/schema/schema.mjs";
import databaseCommand from "./yargs-commands/database.mjs";
import { logArgv } from "./lib/middleware.mjs";

/** @typedef {import('awilix').AwilixContainer<import('./config/setup-container.mjs').modifiedInjectables>} cliContainer */

/** @type {cliContainer} */
export let container;
/** @type {yargs.Argv} */
/** @type {import('yargs').Argv} */
export let builtYargs;

/**
Expand Down Expand Up @@ -48,12 +49,12 @@ export async function parseYargs(builtYargs) {
/**
* @function buildYargs
* @param {string} argvInput
* @returns {yargs.Argv<any>}
* @returns {import('yargs').Argv<any>}
*/
function buildYargs(argvInput) {
// have to build a yargsInstance _before_ chaining off it
// https://github.com/yargs/yargs/blob/main/docs/typescript.md?plain=1#L124
const yargsInstance = yargs(argvInput)
const yargsInstance = yargs(argvInput);

return (
yargsInstance
Expand All @@ -62,6 +63,7 @@ function buildYargs(argvInput) {
.command("eval", "evaluate a query", evalCommand)
.command("login", "login via website", loginCommand)
.command(schemaCommand)
.command(databaseCommand)
.command("throw", false, {
handler: () => {
throw new Error("this is a test error");
Expand Down
2 changes: 2 additions & 0 deletions src/config/setup-container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import open from "open";
import OAuthClient from "../lib/auth/oauth-client.mjs";
import { Lifetime } from "awilix";
import fs from "node:fs";
import { AccountKey } from "../lib/file-util.mjs";
import { parseYargs } from "../cli.mjs";

// import { findUpSync } from 'find-up'
Expand Down Expand Up @@ -66,6 +67,7 @@ export const injectables = {
}),
oauthClient: awilix.asClass(OAuthClient, { lifetime: Lifetime.SCOPED }),
makeFaunaRequest: awilix.asValue(makeFaunaRequest),
accountCreds: awilix.asClass(AccountKey, { lifetime: Lifetime.SCOPED }),
errorHandler: awilix.asValue((error, exitCode) => exit(exitCode)),

// feature-specific lib (homemade utilities)
Expand Down
1 change: 1 addition & 0 deletions src/config/setup-test-container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function setupTestContainer() {
),
accountClient: awilix.asFunction(stub()),
oauthClient: awilix.asFunction(stub()),
accountCreds: awilix.asFunction(stub()),
// in tests, let's exit by throwing
errorHandler: awilix.asValue((error, exitCode) => {
error.code = exitCode;
Expand Down
20 changes: 4 additions & 16 deletions src/lib/auth/oauth-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ const clientSecret =
const REDIRECT_URI = `http://127.0.0.1`;

class OAuthClient {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will jsdoc this in another pr, this one is getting big

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the JSDoc PR can you add the annotation //@ts-check to the top of the file? it'll turn on in-IDE static analysis and should help you generate good types.

server; //: http.Server;

port; //: number;

code_verifier; //: string;

code_challenge; //: string;

auth_code; //: string;

state; //: string;

constructor() {
this.server = http.createServer(this._handleRequest.bind(this));
this.code_verifier = Buffer.from(randomBytes(20)).toString("base64url");
Expand Down Expand Up @@ -127,11 +115,11 @@ class OAuthClient {

async start() {
try {
this.server.on("listening", () => {
this.port = this.server.address().port;
this.server.emit("ready");
});
if (!this.server.listening) {
this.server.on("listening", () => {
this.port = this.server.address().port;
this.server.emit("ready");
});
this.server.listen(0);
Comment on lines +119 to 123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this.server.listening set when you emit ready?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check was a workaround for that builtYargs.argv weirdness, so not strictly necessary but yes by the time i send "ready" it's already true

}
} catch (e) {
Expand Down
20 changes: 10 additions & 10 deletions src/lib/db.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import { container } from "../cli.mjs";

/**
* @function makeFaunaRequest
* @param {object} args
* @param {string} args.secret - The secret to include in the AUTHORIZATION header of the request.
* @param {string} args.baseUrl - The base URL from the scheme up through the top level domain and optional port; defaults to "https://db.fauna.com:443".
* @param {string} args.path - The path part of the URL. Added to the baseUrl and params to build the full URL.
* @param {Record<string, string>} [args.params] - The parameters (and their values) to set in the query string.
* @param {('GET'|'HEAD'|'OPTIONS'|'PATCH'|'PUT'|'POST'|'DELETE'|'PATCH')} args.method - The HTTP method to use when making the request.
* @param {object} [args.body] - The body to include in the request.
* @param {boolean} [args.shouldThrow] - Whether or not to throw if the network request succeeds but is not a 2XX. If this is set to false, makeFaunaRequest will return the error instead of throwing.
*/
* @function makeFaunaRequest
* @param {object} args
* @param {string} args.secret - The secret to include in the AUTHORIZATION header of the request.
* @param {string} args.baseUrl - The base URL from the scheme up through the top level domain and optional port; defaults to "https://db.fauna.com:443".
* @param {string} args.path - The path part of the URL. Added to the baseUrl and params to build the full URL.
* @param {Record<string, string>} [args.params] - The parameters (and their values) to set in the query string.
* @param {('GET'|'HEAD'|'OPTIONS'|'PATCH'|'PUT'|'POST'|'DELETE'|'PATCH')} args.method - The HTTP method to use when making the request.
* @param {object} [args.body] - The body to include in the request.
* @param {boolean} [args.shouldThrow] - Whether or not to throw if the network request succeeds but is not a 2XX. If this is set to false, makeFaunaRequest will return the error instead of throwing.
*/
export async function makeFaunaRequest({
secret,
baseUrl,
Expand Down
73 changes: 58 additions & 15 deletions src/lib/fauna-account-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,35 @@

import { container } from "../cli.mjs";

/**
* Class representing a client for interacting with the Fauna account API.
*/
export class FaunaAccountClient {
/**
* Creates an instance of FaunaAccountClient.
*/
constructor() {
/**
* The base URL for the Fauna account API.
* @type {string}
*/
this.url =
process.env.FAUNA_ACCOUNT_URL ?? "https://account.fauna.com/api/v1";

/**
* The fetch function for making HTTP requests.
* @type {Function}
*/
this.fetch = container.resolve("fetch");
}

/**
* Starts an OAuth request to the Fauna account API.
*
* @param {Object} authCodeParams - The parameters for the OAuth authorization code request.
* @returns {Promise<string>} - The URL to the Fauna dashboard for OAuth authorization.
* @throws {Error} - Throws an error if there is an issue during login.
*/
async startOAuthRequest(authCodeParams) {
const OAuthUrl = `${this.url}/api/v1/oauth/authorize?${new URLSearchParams(
authCodeParams
Expand All @@ -21,6 +43,18 @@ export class FaunaAccountClient {
return dashboardOAuthURL;
}

/**
* Retrieves an access token from the Fauna account API.
*
* @param {Object} opts - The options for the token request.
* @param {string} opts.clientId - The client ID for the OAuth application.
* @param {string} opts.clientSecret - The client secret for the OAuth application.
* @param {string} opts.authCode - The authorization code received from the OAuth authorization.
* @param {string} opts.redirectURI - The redirect URI for the OAuth application.
* @param {string} opts.codeVerifier - The code verifier for the OAuth PKCE flow.
mwilde345 marked this conversation as resolved.
Show resolved Hide resolved
* @returns {Promise<string>} - The access token.
* @throws {Error} - Throws an error if there is an issue during token retrieval.
*/
async getToken(opts) {
const params = {
grant_type: "authorization_code",
Expand All @@ -43,14 +77,20 @@ export class FaunaAccountClient {
`Failure to authorize with Fauna (${response.status}): ${response.statusText}`
);
}
const { /*state,*/ access_token } = await response.json();
const { access_token } = await response.json();
return access_token;
} catch (err) {
throw new Error("Failure to authorize with Fauna: " + err.message);
}
}

// TODO: remove access_token param and use credential manager helper
/**
* Retrieves the session information from the Fauna account API.
*
* @param {string} accessToken - The access token for the session.
* @returns {Promise<{account_key: string, refresh_token: string}>} - The session information.
* @throws {Error} - Throws an error if there is an issue during session retrieval.
*/
async getSession(accessToken) {
const headers = new Headers();
headers.append("Authorization", `Bearer ${accessToken}`);
Expand All @@ -66,22 +106,26 @@ export class FaunaAccountClient {
);
if (response.status >= 400) {
throw new Error(
`Error creating session (${response.status}): ${response.statusText}`
`Failure to get session with Fauna (${response.status}): ${response.statusText}`
);
}
const session = await response.json();
return session;
return await response.json();
} catch (err) {
throw new Error(
"Failure to create session with Fauna: " + JSON.stringify(err)
);
throw new Error("Failure to get session with Fauna: " + err.message);
}
}

// TODO: remove account_key param and use credential manager helper
async listDatabases(account_key) {
/**
* Lists databases associated with the given account key.
*
* @param {string} accountKey - The account key to list databases for.
* @returns {Promise<Object[]>} - The list of databases.
* @throws {Error} - Throws an error if there is an issue during the request.
*/
async listDatabases(accountKey) {
const headers = new Headers();
headers.append("Authorization", `Bearer ${account_key}`);
headers.append("Authorization", `Bearer ${accountKey}`);

Comment on lines +127 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on abstracting out something with an API like makeFaunaRequest on the next draft of this PR? that'll let you centralize error handling and make these methods really short & similar.

const requestOptions = {
method: "GET",
headers,
Expand All @@ -93,13 +137,12 @@ export class FaunaAccountClient {
);
if (response.status >= 400) {
throw new Error(
`Error listing databases (${response.status}): ${response.statusText}`
`Failure to list databases. (${response.status}): ${response.statusText}`
);
}
const databases = await response.json();
return databases;
return await response.json();
} catch (err) {
throw new Error("Failure to list databases: ", err.message);
throw new Error("Failure to list databases with Fauna: " + err.message);
}
}
}
Loading