Created
July 19, 2021 15:24
-
-
Save jtomchak/34bc88749ce1682534a27f3cf53a46dc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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