Skip to content

Commit

Permalink
fix(feedback): updated response feedback quality (closes #30)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasfroeller committed Apr 5, 2024
1 parent 22a77de commit 8c4404c
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 45 deletions.
34 changes: 34 additions & 0 deletions src/v1/adapters/github/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export interface MicroserviceErrorOptions {
error: string;
code?: number;
success?: boolean;
info?: object | null;
data?: object | null;
}

export class MicroserviceError extends Error {
json: MicroserviceErrorOptions;

constructor(options: MicroserviceErrorOptions) {
super(options.error);
this.name = 'MicroserviceError';
this.json = options;
this.json.code = typeof options.code === 'number' ? options.code : 500; // Default to Internal Server Error if no status code is provided
this.json.success = options?.success ? options.success : false; // so that you can always check the response status, even if it's an error
this.json.info = options?.info ? options.info : null;
this.json.data = options?.data ? options.data : null; // so that you can always check the response data, even if it's an error

// Capturing stack trace, excluding constructor call from it.
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor);
}
}

toJSON(): MicroserviceErrorOptions {
return JSON.parse(JSON.stringify(this.json)); // so that functions and so on are being converted to json
}

toString(): string {
return JSON.stringify(this.json);
}
}
30 changes: 14 additions & 16 deletions src/v1/adapters/github/functions/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { RateLimit } from "@octokit/graphql-schema";
import { GITHUB_QUOTA } from "../graphql";
import { maybeStringToNumber } from "./parse";
import { DEV_MODE } from "../../../../config";
import { MicroserviceError } from "../error";

/* JWT */

Expand Down Expand Up @@ -47,7 +48,7 @@ export function checkForTokenPresence(
'WWW-Authenticate'
] = `Bearer realm='${GITHUB_JWT_REALM}', error="bearer_token_missing"`;

throw Error(errorMessage);
throw new MicroserviceError({ error: errorMessage, code: 400 });
}

return token as string;
Expand All @@ -73,7 +74,7 @@ export async function checkIfTokenIsValid(token: string | number, set: Context["
'WWW-Authenticate'
] = `Bearer realm='${GITHUB_JWT_REALM}', error="invalid_bearer_token"`;

throw Error(`The provided token is invalid or has expired. Please try another token. Perhaps you chose the wrong provider? [${response?.error}]` ?? '');
throw new MicroserviceError({ error: "The provided token is invalid or has expired. Please try another token. Perhaps you chose the wrong provider?", code: 401, info: response?.error });
}

return true;
Expand All @@ -95,10 +96,10 @@ export const RESOLVE_JWT = new Elysia()
'WWW-Authenticate'
] = `Bearer realm='${GITHUB_JWT_REALM}', error="bearer_token_invalid"`;

throw Error('Unauthorized. Authentication token is missing or invalid. Please provide a valid token. Tokens can be obtained from the `/auth/app|token` endpoints.');
throw new MicroserviceError({ error: 'Unauthorized. Authentication token is missing or invalid. Please provide a valid token. Tokens can be obtained from the `/auth/app|token` endpoints.', code: 401 });
}

const valid = await checkIfTokenIsValid(jwt.auth, set);
const valid = await checkIfTokenIsValid(jwt.auth, set); // check again, just to be sure (guardEndpoints-plugin)
if (DEV_MODE) console.log('JWT:', jwt, 'Valid:', valid);

return {
Expand Down Expand Up @@ -134,11 +135,11 @@ export const GITHUB_APP_AUTHENTICATION = new Elysia({ prefix: '/auth' })
'WWW-Authenticate'
] = `Bearer realm='${GITHUB_JWT_REALM}', error="invalid_request"`;

throw Error('App installation is missing. Install it at https://github.com/apps/propromo-software/installations/new.');
throw new MicroserviceError({ error: 'App installation is missing. Install it at https://github.com/apps/propromo-software/installations/new.', code: 400 });
}

const valid = await checkIfTokenIsValid(body.installation_id, set);
if (DEV_MODE) console.log('JWT:', jwt, 'Valid:', valid);
if (DEV_MODE) console.log('JWT:', body.installation_id, 'Valid:', valid);
},
body: t.Object({
code: t.Optional(
Expand Down Expand Up @@ -173,7 +174,7 @@ export const GITHUB_APP_AUTHENTICATION = new Elysia({ prefix: '/auth' })
async beforeHandle({ bearer, set }) {
const token = checkForTokenPresence(bearer, set);
const valid = await checkIfTokenIsValid(token, set);
if (DEV_MODE) console.log('JWT:', jwt, 'Valid:', valid);
if (DEV_MODE) console.log('JWT:', token, 'Valid:', valid);
},
detail: {
description: "Authenticate using a GitHub PAT.",
Expand All @@ -187,16 +188,13 @@ export const GITHUB_APP_AUTHENTICATION = new Elysia({ prefix: '/auth' })
* Generates an Octokit object based on the provided authentication strategy and credentials.
* @documentation https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#using-octokitjs-to-authenticate-with-an-installation-id
*/
export async function getOctokitObject(authStrategy: GITHUB_AUTHENTICATION_STRATEGY_OPTIONS | null, auth: string | number | null): Promise<Octokit | null> {
let octokitObject = null;

if (typeof auth === "string" && (!authStrategy || authStrategy === GITHUB_AUTHENTICATION_STRATEGY_OPTIONS.TOKEN)) {
octokitObject = new Octokit({ auth });
export async function getOctokitObject(authStrategy: GITHUB_AUTHENTICATION_STRATEGY_OPTIONS | null, auth: string | number | null, set: Context["set"]) {
if (typeof auth === "string" && (!authStrategy || authStrategy === GITHUB_AUTHENTICATION_STRATEGY_OPTIONS.TOKEN)) {
return new Octokit({ auth });
} else if (authStrategy === GITHUB_AUTHENTICATION_STRATEGY_OPTIONS.APP) {
octokitObject = await octokitApp.getInstallationOctokit(auth as number); // get Installation by installationId
} else {
return null;
return await octokitApp.getInstallationOctokit(auth as number); // get Installation by installationId
}

return octokitObject;
set.status = 400;
throw new MicroserviceError({ error: "Invalid authentication strategy", code: 400 });
}
46 changes: 27 additions & 19 deletions src/v1/adapters/github/functions/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { GITHUB_API_HEADERS } from "../globals";
import { getOctokitObject } from "./authenticate";
import type { OctokitResponse } from "@octokit/types";
import { MicroserviceError } from "../error";

/**
* Fetches the rate limit using the provided authentication and context set.
Expand All @@ -21,12 +22,10 @@ import type { OctokitResponse } from "@octokit/types";
export async function fetchRateLimit(auth: string | number, set: Context["set"]): Promise<RestResponse<GetRateLimit>> {
const octokit = await getOctokitObject(
GITHUB_AUTHENTICATION_STRATEGY_OPTIONS.TOKEN,
auth
auth,
set
);

if (!octokit)
return { success: false, error: "Invalid authentication strategy" };

return tryFetch<GetRateLimit>(
() => octokit.rest.rateLimit.get(),
set
Expand All @@ -44,9 +43,16 @@ export async function fetchRateLimit(auth: string | number, set: Context["set"])
*/
// biome-ignore lint/suspicious/noExplicitAny:
export async function fetchGithubDataUsingRest(path: string, auth: string | number | undefined | null, set: Context["set"], authStrategy: GITHUB_AUTHENTICATION_STRATEGY_OPTIONS | null = null): Promise<RestResponse<OctokitResponse<any, number>>> {
if (auth === undefined) return { success: false, error: "No authentication token provided" };
const octokit = await getOctokitObject(authStrategy, auth);
if (!octokit) return { success: false, error: "Invalid authentication strategy" };
if (auth === undefined) {
set.status = 400;
throw new MicroserviceError({ error: "No authentication token provided", code: 400 });
}

const octokit = await getOctokitObject(authStrategy, auth, set);
if (!octokit) {
set.status = 400;
throw new MicroserviceError({ error: "Invalid authentication strategy", code: 400 });
}

// biome-ignore lint/suspicious/noExplicitAny:
return tryFetch<OctokitResponse<any, number>>(
Expand All @@ -65,9 +71,11 @@ export async function fetchGithubDataUsingRest(path: string, auth: string | numb
* @return {Promise<GraphqlResponse<T>>} a promise that resolves to a GraphQL response
*/
export async function fetchGithubDataUsingGraphql<T>(graphqlInput: string, auth: string | number | undefined | null, set: Context["set"], authStrategy: GITHUB_AUTHENTICATION_STRATEGY_OPTIONS | null = null): Promise<GraphqlResponse<T>> {
if (auth === undefined) return { success: false, error: "No authentication token provided" };
const octokit = await getOctokitObject(authStrategy, auth);
if (!octokit) return { success: false, error: "Invalid authentication strategy" };
if (auth === undefined) {
set.status = 400;
throw new MicroserviceError({ error: "No authentication token provided", code: 400 });
}
const octokit = await getOctokitObject(authStrategy, auth, set);

return tryFetch<T>(
() => octokit.graphql<T>(graphqlInput, {
Expand Down Expand Up @@ -102,23 +110,23 @@ const tryFetch = async <T>(
} catch (error: any) {
if (error instanceof GraphqlResponseError) {
set.status = error.errors?.[0].type === GraphqlResponseErrorCode.NOT_FOUND ? 404 : 500;
return {
success: false,
error: error.message,
cause: error.cause,
path: error.errors?.[0].path,
type: error.errors?.[0].type,
};

throw new MicroserviceError({
error: error.message, code: set.status, info: {
cause: error.cause,
path: error.errors?.[0].path,
type: error.errors?.[0].type
}});
} if (
error instanceof InternalServerError ||
error instanceof ParseError ||
error instanceof NotFoundError
) {
set.status = error.status;
return { success: false, error: error.status };
throw new MicroserviceError({ error: "Something went horrible wrong.", code: error.status });
}

set.status = error?.status ?? 500;
return { success: false, error: errorMessage };
throw new MicroserviceError({ error: errorMessage, code: error.status });
}
};
5 changes: 4 additions & 1 deletion src/v1/adapters/github/functions/parse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Context } from "elysia";
import { MicroserviceError } from "../error";

/**
* Parses the input scopes and returns an array of valid enum values.
Expand All @@ -17,7 +18,9 @@ export function parseScopes<T>(inputScopes: string | undefined, validationEnum:

if (!scope_values_are_of_valid_enum_type) {
set.status = 400;
throw Error(errorMessage ?? 'Invalid scope values');

const message = errorMessage ?? 'Invalid scope values';
throw new MicroserviceError({ error: message, code: 400 });
}

return scope_values as T[];
Expand Down
13 changes: 4 additions & 9 deletions src/v1/adapters/github/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Elysia } from "elysia";
import { GITHUB_JWT, GITHUB_JWT_REALM } from "./functions/authenticate";
import { GITHUB_JWT, checkForTokenPresence, checkIfTokenIsValid } from "./functions/authenticate";
import bearer from '@elysiajs/bearer';
import { DEV_MODE } from "../../../config";

/* GUARDED ENDPOINTS */

Expand All @@ -19,14 +20,8 @@ export const guardEndpoints = (endpoints: Elysia) => new Elysia({
.guard(
{
async beforeHandle({ bearer, set }) {
if (!bearer) {
set.status = 400
set.headers[
'WWW-Authenticate'
] = `Bearer realm='${GITHUB_JWT_REALM}', error="invalid_request"`

return 'Unauthorized'
}
const token = checkForTokenPresence(bearer, set);
if (DEV_MODE) console.log('JWT:', token);
}
},
(app) => app
Expand Down

0 comments on commit 8c4404c

Please sign in to comment.