Skip to content

Instantly share code, notes, and snippets.

@jtomchak
Created July 19, 2021 15:24
Show Gist options
  • Save jtomchak/34bc88749ce1682534a27f3cf53a46dc to your computer and use it in GitHub Desktop.
Save jtomchak/34bc88749ce1682534a27f3cf53a46dc to your computer and use it in GitHub Desktop.
import { RequestOptions, RESTDataSource } from 'apollo-datasource-rest';
import { ApolloError, AuthenticationError } from 'apollo-server';
import axios from 'axios';
import util from 'util';
import { Event, Speaker } from '../generated/graphql';
import {
KlikAttendee,
KlikCalendar,
KlikCalendarDefinition,
KlikCarousel,
KlikCustomResource,
KlikSession,
KlikSpeaker,
KlikSponsor,
KlikServiceAccount,
ServiceAccountTokens,
KlikSponsorRepresentative,
ConciergeCalendarSessionsResponse,
KlikTouchpoint,
KlikLoginResponse,
KlikCalendarUrl,
} from '../types';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || '');
if (process.env.SQREEN_TOKEN) {
var Sqreen = require('sqreen');
}
/**
* ENV Variables
*/
const serviceEmail = (): string => process.env.KLIK_EMAIL_ADDRESS || '';
const servicePassword = (): string => process.env.KLIK_PASSWORD || '';
const eventId = (): string => process.env.KLIK_EVENT_ID || 'signal-2020';
const klikBaseURL = (): string => `https://${process.env.KLIK_API_DOMAIN}`;
const clientId = (): string | undefined => process.env.KLIK_CLIENT_ID;
const publicCalendarId = (): string | undefined => process.env.KLIK_PUBLIC_CALENDAR_ID;
export class KlikAPI extends RESTDataSource {
// private serviceRefreshToken: string;
private bearerToken: string;
private superUserToken: string;
private eventId: string;
baseURL = klikBaseURL();
constructor(serviceAccount: KlikServiceAccount) {
super();
this.superUserToken = serviceAccount.getAccessToken();
this.bearerToken = serviceAccount.getAccessToken();
this.eventId = serviceAccount.getEventId();
}
private handleError(error): Error {
console.error(error);
this.resetBearerToken();
switch (error.extensions.response.status) {
case 401:
return error.extensions.response.body.description === 'Incorrect email address or password. Please try again.'
? new AuthenticationError('Incorrect email or password')
: error.extensions.response.body.description.includes(`HMAC in token`)
? new AuthenticationError('Access token is Invalid')
: new AuthenticationError('Access token is expired');
case 404:
return new ApolloError('Account not found', '404');
default:
return error;
}
}
setBearerToken(token: string): void {
this.bearerToken = token;
}
resetBearerToken(): void {
this.bearerToken = this.superUserToken;
}
async willSendRequest(request: RequestOptions): Promise<void> {
//get authToken Function
const isInvalidated = await redis.get(this.bearerToken);
if (!isInvalidated) {
request.headers.set('Authorization', `Bearer ${this.bearerToken}`);
} else {
console.error('USING INVALID ACCESS TOKEN');
}
}
async editAttendeeByEmail(email, body): Promise<KlikAttendee> {
try {
const result = await this.put(`events/${this.eventId}/attendees/email:${email}`, body);
return result;
} catch (error) {
throw this.handleError(error);
}
}
async editSessionById(id: string, body) {
try {
const result = await this.put(`events/${this.eventId}/sessions/${id}`, body);
return result;
} catch (error) {
throw this.handleError(error);
}
}
async editAttendeeById(id, body): Promise<KlikAttendee> {
try {
const result = await this.put(`events/${this.eventId}/attendees/${id}`, body);
return result;
} catch (error) {
throw this.handleError(error);
}
}
async confirmServiceToken(): Promise<boolean> {
try {
const result = await this.get('accounts/me');
return result.event_permissions &&
result.event_permissions[eventId()] &&
result.event_permissions[eventId()]['id'] === 'organizer'
? true
: false;
} catch (error) {
throw this.handleError(error);
}
}
async getAttendeeById(id: string): Promise<KlikAttendee> {
try {
return await this.get(`events/${this.eventId}/attendees/${id}`);
} catch (error) {
throw this.handleError(error);
}
}
async getSessionAttendees(id: string, calendarId: string): Promise<KlikAttendee[]> {
try {
const results = await this.get(`events/${this.eventId}/calendars/${calendarId}/sessions/${id}/attendees/going`);
return results;
} catch (error) {
throw this.handleError(error);
}
}
async editAttendeeByMe(body): Promise<KlikAttendee> {
try {
const result = await this.put(`events/${this.eventId}/attendees/me`, body);
return result;
} catch (error) {
throw this.handleError(error);
}
}
async getEvent(eventId = this.eventId): Promise<Event> {
try {
const eventDetails = await this.get(`events/${eventId}`);
return eventDetails;
} catch (error) {
throw this.handleError(error);
}
}
async getExhibitors(): Promise<KlikSponsor[]> {
try {
const exhibitorList = await this.get(`events/${this.eventId}/exhibitors`);
return exhibitorList;
} catch (error) {
throw this.handleError(error);
}
}
async getExhibitor(exhibitorId: string): Promise<KlikSponsor> {
try {
const exhibitor = await this.get(`events/${this.eventId}/exhibitors/${exhibitorId}`);
return exhibitor;
} catch (error) {
throw this.handleError(error);
}
}
async getExhibitorRepresentatives(exhibitorId: string): Promise<KlikSponsorRepresentative[]> {
try {
const exhibitorRepresentatives = await this.get(
`events/${this.eventId}/exhibitors/${exhibitorId}/representatives`
);
return exhibitorRepresentatives;
} catch (error) {
throw this.handleError(error);
}
}
async editExhibitor(exhibitorId: string, body: object): Promise<KlikSponsor> {
try {
const exhibitor = await this.put(`events/${this.eventId}/exhibitors/${exhibitorId}`, body);
return exhibitor;
} catch (error) {
throw this.handleError(error);
}
}
async getTouchpoints(): Promise<KlikTouchpoint[]> {
try {
const touchpoints = await this.get(`events/${this.eventId}/touchpoints`);
return touchpoints;
} catch (error) {
throw this.handleError(error);
}
}
async createExhibitorTouchpoint(name: string, exhibitor_id: string): Promise<KlikTouchpoint> {
try {
const touchpoint = await this.post(`events/${this.eventId}/touchpoints`, { name, exhibitor_id });
return touchpoint;
} catch (error) {
throw this.handleError(error);
}
}
async activateTouchpoint(attendee_id: string, bookmarkable_id: string): Promise<KlikTouchpoint> {
try {
const touchpoint = await this.post(`events/${this.eventId}/attendees/${attendee_id}/kliks`, {
bookmarkable_id,
bookmarked_at: Date.now(),
});
return touchpoint;
} catch (error) {
throw this.handleError(error);
}
}
async getSessions(): Promise<KlikSession[]> {
const calendarId = publicCalendarId();
if (calendarId) {
try {
const sessionList = await this.get(`events/${this.eventId}/calendars/${calendarId}/sessions`);
return sessionList;
} catch (error) {
console.error('Error requesting public sessions', error);
console.error(error.extensions.response.body);
throw this.handleError(error);
}
}
try {
const sessionList = await this.get(`events/${this.eventId}/sessions`);
return sessionList;
} catch (error) {
throw this.handleError(error);
}
}
async getSession(sessionId): Promise<KlikSession> {
try {
const session = await this.get(`events/${this.eventId}/sessions/${sessionId}`);
return session;
} catch (error) {
throw this.handleError(error);
}
}
async getSpeakers(eventId = this.eventId): Promise<KlikSpeaker[]> {
try {
const speakers = await this.get(`events/${eventId}/speakers`);
return speakers;
} catch (error) {
throw this.handleError(error);
}
}
async getSpeaker(speakerId): Promise<Speaker> {
try {
const speaker = await this.get(`events/${this.eventId}/speakers/${speakerId}`);
return speaker;
} catch (error) {
throw this.handleError(error);
}
}
async getAttendees(): Promise<[KlikAttendee]> {
try {
const attendees = await this.get(`events/${this.eventId}/attendees/`);
return attendees;
} catch (error) {
throw this.handleError(error);
}
}
async getAttendeesWhere(filter: string): Promise<[KlikAttendee]> {
try {
const attendees = await this.get(`events/${this.eventId}/attendees?filter=${filter}`);
return attendees;
} catch (error) {
throw this.handleError(error);
}
}
async getAttendeeByMe(): Promise<KlikAttendee> {
try {
const attendee = await this.get(`events/${this.eventId}/attendees/me`);
return attendee;
} catch (error) {
throw this.handleError(error);
}
}
async getAttendeeByMeForSessionList(): Promise<KlikAttendee> {
try {
const attendee = await this.get(`events/${this.eventId}/attendees/me`);
/* We need this section in place to deal with typescript errors, owing to
the conflict between the Klik type for this field ({ title: string }|undefined)
and the published GraphQL return type (string). Because we're implicitly
converting KlikAttendee into Attendee, and back, they can't both have properties
with the same name and different types :(
*/
if (attendee.attendee_type) {
attendee.attendee_type = attendee.attendee_type.title;
}
return attendee;
} catch (error) {
throw this.handleError(error);
}
}
// authenticate
async getAttendeeByEmail(email, eventId = this.eventId): Promise<KlikAttendee> {
try {
const attendee = await this.get(`events/${eventId}/attendees/email:${email}`);
return attendee;
} catch (error) {
throw this.handleError(error);
}
}
async login(email: string, password: string): Promise<KlikLoginResponse> {
try {
return await this.post(`login`, {
username: `email:${email}`,
password: password,
client_id: process.env.KLIK_CLIENT_ID,
grant_type: 'password',
});
} catch (error) {
if (Sqreen) {
Sqreen.auth_track(false, {
email,
});
}
throw this.handleError(error);
}
}
async getAccessTokenByEmail(email: string): Promise<KlikLoginResponse> {
try {
return await this.post(`/events/${this.eventId}/attendees/email:${email}/access_token`);
} catch (error) {
throw this.handleError(error);
}
}
async setAccountSmsPreferences(opt_out: boolean): Promise<KlikLoginResponse> {
const preferences = opt_out ? [] : ['sms'];
try {
return await this.put(`/accounts/me`, {
settings: {
notifications: {
unsolicited: preferences,
reminder: preferences,
invitation: preferences,
announcement_platform: preferences,
announcement_admin: preferences,
},
},
});
} catch (error) {
throw this.handleError(error);
}
}
async getItineraryByEmail(email: string): Promise<[KlikCalendar]> {
try {
return await this.get(`/events/${this.eventId}/attendees/email:${email}/calendar`);
} catch (error) {
throw this.handleError(error);
}
}
async getItineraryById(id: string): Promise<[KlikCalendar]> {
try {
return await this.get(`/events/${this.eventId}/attendees/${id}/calendar`);
} catch (error) {
throw this.handleError(error);
}
}
async getItineraryByMe(): Promise<[KlikCalendar]> {
try {
return await this.get(`/events/${this.eventId}/attendees/me/calendar`);
} catch (error) {
throw this.handleError(error);
}
}
async registerSessionByMe(sessionId: string): Promise<unknown> {
try {
const response = await this.put(`/events/${this.eventId}/attendees/me/sessions/${sessionId}/going`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async registerSessionByEmail(sessionId: string, email: string): Promise<KlikSession> {
try {
const response = await this.put(`/events/${this.eventId}/attendees/email:${email}/sessions/${sessionId}/going`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async registerSessionById(sessionId: string, id: string): Promise<KlikSession> {
try {
const response = await this.put(`/events/${this.eventId}/attendees/${id}/sessions/${sessionId}/going`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async deregisterSessionByEmail(email: string, sessionId: string): Promise<KlikSession> {
try {
return await this.delete(`/events/${this.eventId}/attendees/email:${email}/sessions/${sessionId}/going`);
} catch (error) {
throw this.handleError(error);
}
}
async deregisterSessionById(id: string, sessionId: string): Promise<KlikSession> {
try {
return await this.delete(`/events/${this.eventId}/attendees/${id}/sessions/${sessionId}/going`);
} catch (error) {
throw this.handleError(error);
}
}
async unregisterSession(sessionId: string): Promise<unknown> {
try {
const response = await this.delete(`/events/${this.eventId}/attendees/me/sessions/${sessionId}/going`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async attendSessionById(sessionId: string, id: string): Promise<unknown> {
try {
let timestamp = Math.floor(new Date().getTime() / 1000);
const response = await this.put(
`/events/${this.eventId}/attendees/${id}/sessions/${sessionId}/attend?attended_at=${timestamp}`
);
return response;
} catch (error) {
if (error.message === '409: Conflict') {
return true;
}
throw this.handleError(error);
}
}
async deleteSession(sessionId: string): Promise<void> {
try {
const response = await this.delete(`/events/${this.eventId}/sessions/${sessionId}`);
return;
} catch (error) {
throw this.handleError(error);
}
}
async getCarouselItems(): Promise<KlikCarousel> {
try {
const response = await this.get(`/events/${this.eventId}/conf/carousel`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async getCustomResources(): Promise<KlikCustomResource[]> {
try {
const response = await this.get(`/events/${this.eventId}/custom_resources?adjacents=true`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async provideFeedback(sessionId: string, rating: number, comment: string | null | undefined): Promise<unknown> {
try {
const response = await this.put(`/events/${this.eventId}/attendees/me/sessions/${sessionId}/feedback`, {
rating,
comment,
});
return response;
} catch (error) {
throw this.handleError(error);
}
}
async createPrivateCalendar(
name: string,
description: string,
color: string,
position: number
): Promise<KlikCalendarDefinition> {
try {
const response = await this.post(`/events/${this.eventId}/calendars`, {
name,
description,
position,
color,
});
return response;
} catch (error) {
throw this.handleError(error);
}
}
async getPrivateCalendarSessions(calendarId: string): Promise<KlikSession[]> {
try {
const response = await this.get(`/events/${this.eventId}/calendars/${calendarId}/sessions`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
async getConciergeCalendarSessions(calendarId: string): Promise<ConciergeCalendarSessionsResponse> {
try {
const calendar = await this.get(`/events/${this.eventId}/calendars/${calendarId}?include_sessions=true`);
return {
title: calendar.name,
sessions: calendar.sessions,
};
} catch (error) {
throw this.handleError(error);
}
}
async createPrivateCalendarSession(
calendarId: string,
title: string,
start_date: number,
end_date: number,
custom_fields: object = {},
capacity?: number
): Promise<KlikSession> {
try {
const response = await this.post(`/events/${this.eventId}/calendars/${calendarId}/sessions`, {
title,
start_date,
end_date,
custom_fields,
capacity,
});
return response;
} catch (error) {
console.log(error);
throw this.handleError(error);
}
}
async getCalendarDownloadUrl(): Promise<KlikCalendarUrl> {
try {
const response = await this.get(`/events/${this.eventId}/attendees/me/calendar_url`);
return response;
} catch (error) {
throw this.handleError(error);
}
}
}
export async function getProfile(accessToken: string): Promise<KlikAttendee> {
try {
const { data } = await axios.get(`${klikBaseURL()}/events/${eventId()}/attendees/me`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return data;
} catch (error) {
throw error;
}
}
/**
* Refresh token for Service Account
*/
export async function refreshKlikToken(refreshToken: string): Promise<{ access_token: string }> {
try {
const {
data: { access_token },
} = await axios.post(`${klikBaseURL()}/login`, {
refresh_token: refreshToken,
client_id: process.env.KLIK_CLIENT_ID,
grant_type: 'refresh_token',
});
return { access_token };
} catch (error) {
throw error;
}
}
export const serviceAccount = (function () {
// Instance stores a reference to the Singleton
let instance;
const tokens: ServiceAccountTokens = {
access_token: '',
refresh_token: '',
};
const event_id = eventId();
const setAsyncInterval = util.promisify(setInterval);
let refreshInterval = 3600; // default to one hour
async function init() {
// Singleton
try {
const {
data: { access_token, refresh_token, expires_in },
} = await axios.post(`${klikBaseURL()}/login`, {
username: `email:${serviceEmail()}`,
password: servicePassword(),
client_id: clientId(),
grant_type: 'password',
});
// assign tokens
tokens.access_token = access_token;
tokens.refresh_token = refresh_token; //refresh token is good for 6 months
refreshInterval = expires_in * 1000; // return is in seconds. Convert to milliseconds
// set interval for service token refresh
taskRefreshAccessToken();
} catch (error) {
throw console.error(`Unable to get Service Account Credentials, ${error}`);
}
/**
* setInterval task refreshing access token at 1/2 the expires_in time
* onSuccess assign new access token and trigger task refresh again
*/
function taskRefreshAccessToken() {
// set interval for service token refresh
setAsyncInterval(async () => {
try {
const { access_token, refresh_token } = await refreshKlikServiceToken(tokens.refresh_token);
// assign new access token
tokens.access_token = access_token;
taskRefreshAccessToken();
} catch {}
}, Math.floor(refreshInterval / 2));
}
// Private methods and variables
async function refreshKlikServiceToken(refreshToken: string): Promise<ServiceAccountTokens> {
try {
const {
data: { access_token, refresh_token, expires_in },
} = await axios.post(`${klikBaseURL()}/login`, {
refresh_token: refreshToken,
client_id: process.env.KLIK_CLIENT_ID,
grant_type: 'refresh_token',
});
return { access_token, refresh_token };
} catch (error) {
throw console.log('REFRESH TOKEN ERROR>>>', error.toJSON);
}
}
return {
// Public methods and variables
getAccessToken: () => tokens.access_token,
getEventId: () => event_id,
};
}
return {
// Get the Singleton instance if one exists
// or create one if it doesn't
getInstance: async () => (!instance ? await init() : instance),
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment