Skip to content

Commit

Permalink
Merge pull request #4 from WunderbarNetwork/jwt-api-key-support
Browse files Browse the repository at this point in the history
JWT and API Key Handling
  • Loading branch information
MilosRandelovic committed May 8, 2023
2 parents e6bf489 + dee51b1 commit 4c373d2
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 5 deletions.
103 changes: 98 additions & 5 deletions src/services/EventTrackingService.ts
Expand Up @@ -3,6 +3,17 @@ import type { ServiceResponse } from "../types/ServiceResponse.js";

import { config } from "../util/Config.js";

/** JWT Auth token obtained from Mini Digital */
let jwtAuthorizationToken: string | undefined;

/** Determine if the SDK is running inside a browser (uses JWT authentication) or Node.js (uses API Key authentication). */
const isBrowser: boolean = typeof window !== "undefined" && typeof window.document !== "undefined";

/** Check if the `test` string has a value defined */
const isStringNullOrEmpty = (test: string | null | undefined): boolean => {
return test === null || test === undefined || test.trim().length === 0;
};

/**
* Provides an interface towards the **Mini Digital Event API**, to submit `AnalyticsEvent` objects.
*
Expand All @@ -12,7 +23,7 @@ import { config } from "../util/Config.js";
export async function postEvent(event: AnalyticsEvent, logResponse: boolean = false): Promise<void> {
let miniDigitalUrl: string = config.miniDigitalUrl;

if (miniDigitalUrl === null || miniDigitalUrl === undefined || miniDigitalUrl.trim() === "") {
if (isStringNullOrEmpty(miniDigitalUrl)) {
throw new Error("The Mini Digital URL has not been defined in the config.");
}

Expand All @@ -21,13 +32,93 @@ export async function postEvent(event: AnalyticsEvent, logResponse: boolean = fa
miniDigitalUrl = miniDigitalUrl.substring(0, miniDigitalUrl.length - 1);
}

if (isBrowser) {
// Running from within a browser, use JWT
const response = await postEventJwt(event, false, miniDigitalUrl, logResponse);

// If all good, we're done here
if (response.statusCode === 200) return;

// If we get a 401, the JWT token is either missing or has expired, so try again
if (response.statusCode === 401 && !isStringNullOrEmpty(response.authorizationToken)) {
jwtAuthorizationToken = response.authorizationToken;
await postEventJwt(event, true, miniDigitalUrl, logResponse);
}
} else {
// Running from within Node.js, use API Keys
await postEventApiKey(event, miniDigitalUrl, logResponse);
}
}

/**
* Posts an event using the JWT Authentication
*/
async function postEventJwt(
event: AnalyticsEvent,
forceErrorOn401: boolean,
miniDigitalUrl: string,
logResponse: boolean
): Promise<ServiceResponse> {
let response: Response;
try {
const headers: HeadersInit = {
Accept: "application/json",
"Content-Type": "application/json",
};
if (!isStringNullOrEmpty(jwtAuthorizationToken)) {
headers.Authorization = jwtAuthorizationToken ?? "";
}
response = await fetch(`${miniDigitalUrl}/events/jwt/${event.eventId}`, {
method: "POST",
headers,
body: JSON.stringify(event),
});
} catch (error: any) {
throw new Error("Error interfacing with Mini Digital.");
}

if (!response.ok) {
if (response.status !== 401 || (response.status === 401 && forceErrorOn401)) {
throw new Error(`Mini Digital POST error! Status: ${response.status}`);
}
}

try {
const responseBody = await response.json();
const serviceResponse: ServiceResponse = {
message: responseBody.message,
statusCode: response.status,
authorizationToken: response.headers.get("Authorization") ?? undefined,
};

if (logResponse) {
console.log(`Posted event: ${event.eventId}, outcome: ${serviceResponse.message} (${serviceResponse.statusCode})`);
}

return serviceResponse;
} catch (error: any) {
throw new Error("Could not parse the Mini Digital response.");
}
}

/**
* Posts an event using the API Key Authentication
*/
async function postEventApiKey(event: AnalyticsEvent, miniDigitalUrl: string, logResponse: boolean): Promise<ServiceResponse> {
const apiKey = config.apiKey;

if (isStringNullOrEmpty(apiKey)) {
throw new Error("The API Key is not set in config.");
}

let response: Response;
try {
response = await fetch(`${miniDigitalUrl}/events/${event.eventId}`, {
response = await fetch(`${miniDigitalUrl}/events/key/${event.eventId}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Api-Key": apiKey,
},
body: JSON.stringify(event),
});
Expand All @@ -39,16 +130,18 @@ export async function postEvent(event: AnalyticsEvent, logResponse: boolean = fa
throw new Error(`Mini Digital POST error! Status: ${response.status}`);
}

const responseBody = await response.json();

try {
const responseBody = await response.json();
const serviceResponse: ServiceResponse = {
message: responseBody.message,
statusCode: response.status,
};

if (logResponse) {
console.log(`Posted event: ${event.eventId}, outcome: ${serviceResponse.message} (${response.status})`);
console.log(`Posted event: ${event.eventId}, outcome: ${serviceResponse.message} (${serviceResponse.statusCode})`);
}

return serviceResponse;
} catch (error: any) {
throw new Error("Could not parse the Mini Digital response.");
}
Expand Down
2 changes: 2 additions & 0 deletions src/types/ServiceResponse.ts
@@ -1,3 +1,5 @@
export interface ServiceResponse {
message: string;
statusCode: number;
authorizationToken?: string;
}
2 changes: 2 additions & 0 deletions src/util/Config.ts
@@ -1,4 +1,6 @@
export const config = {
/** The API Key to send to Mini Digital, if using API Key Authentication */
apiKey: "",
/** Default value for the Mini Digital Event Ingress URL, can be customized if needed */
miniDigitalUrl: "https://api.mini.digital",
};

0 comments on commit 4c373d2

Please sign in to comment.