Skip to content

Commit

Permalink
Persist Account keys on login (#375)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwilde345 authored Oct 8, 2024
1 parent f9a2bcd commit 70088ed
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 61 deletions.
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 {
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);
}
} 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.
* @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}`);

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

0 comments on commit 70088ed

Please sign in to comment.