bam-ticketing-sdk
Version:
SDK for B.A.M Ticketing API
415 lines (368 loc) • 12 kB
text/typescript
import jwtDecode from 'jwt-decode'
import queryString from 'query-string'
import { AxiosError, AxiosInstance } from 'axios'
import { AxiosCacheInstance, CacheOptions } from 'axios-cache-interceptor'
import { EventService, MyEventsEvent } from './event'
import { OrderService } from './order'
import { TicketService } from './ticket'
import { AccountService } from './account'
import { PaymentService } from './payment'
import { BlockchainService } from './blockchain'
import { ValidatorService } from './validator'
import {
BaseUrl,
buildClient,
createBaseUrl,
IClientBuildOptions,
setAxiosUpdateTokenFunction,
} from './common'
import { ImportData, ImportDataRequest, MyEventsQuery } from '.'
import { AuthService } from './auth'
import { VenueService } from './venue'
import { ClusterHealth } from './common/types'
import { Jwt, RefreshableJwt } from './auth/types'
import { CredentialFactory } from './credential/credential-factory'
import { ICredential, GuestCredentials, ICredentialData } from './credential'
import { UploadService } from './upload/service'
import { OrderbookService } from './orderbook'
import { NftService } from './nft'
import { NotificationService } from './notification'
import { PdfService } from './pdf/service'
import { BAMgregatorService } from './bamgregator'
export type ApiVersion = 'v1' // | 'v2' ... some day
export interface RetryOptions {
retries?: number
retryCondition?: (error: AxiosError<any>) => boolean | Promise<boolean>
shouldResetTimeout?: boolean
retryDelay?: (retryCount: number, error: any) => number
}
export interface BamOptions {
baseUrl: string | BaseUrl
version?: ApiVersion
organizer?: string
credentials?: ICredential | ICredentialData
retry?: RetryOptions
cache?: CacheOptions
camelCaseResponse?: boolean
}
/**
* BAM services class. The BAM instance holds the API URLs, the authorization token
* and keeps track of the API version used.
*/
export class BAM {
// Services
auth: AuthService
account: AccountService
venue: VenueService
upload: UploadService
validator: ValidatorService
nft: NftService
pdf: PdfService
bamgregator: BAMgregatorService
event: EventService
order: OrderService
payment: PaymentService
blockchain: BlockchainService
ticket: TicketService
orderbook: OrderbookService
notification: NotificationService
client: AxiosInstance | AxiosCacheInstance
tenantClient: AxiosInstance | AxiosCacheInstance
// User/service/guest credentials
private _credentials?: ICredential
private _tenant?: string
private _organizer?: string
private _deviceId?: string
constructor(
readonly baseUrl: string | BaseUrl,
readonly version: ApiVersion = 'v1',
retryOptions?: RetryOptions,
cacheOptions?: CacheOptions,
camelCaseResponse = true
) {
let orgDefault: string
switch (baseUrl) {
case BaseUrl.Dev:
orgDefault = 'org1'
break
case BaseUrl.Prod:
orgDefault = 'catapult'
break
case BaseUrl.QA:
orgDefault = 'qa'
break
case BaseUrl.ThProd:
case BaseUrl.ThStaging:
orgDefault = 'tickethead'
break
}
const options: IClientBuildOptions = {
baseUrl,
retryOptions,
cacheOptions,
camelCaseResponse,
}
this.client = buildClient(options)
this.tenantClient = buildClient({
...options,
tenantName: orgDefault,
})
// Global services
this.auth = new AuthService(this.client, version)
this.account = new AccountService(this.client, version)
this.venue = new VenueService(this.client, version)
this.upload = new UploadService(this.client, version)
this.validator = new ValidatorService(this.client, version)
this.nft = new NftService(this.client, version)
this.pdf = new PdfService(this.client, version)
this.bamgregator = new BAMgregatorService(this.client, version)
// Per-tenant services
this.event = new EventService(this.tenantClient, version)
this.order = new OrderService(this.tenantClient, version)
this.payment = new PaymentService(this.tenantClient, version)
this.blockchain = new BlockchainService(this.tenantClient, version)
this.ticket = new TicketService(this.tenantClient, version)
this.orderbook = new OrderbookService(this.tenantClient, version)
// Hybrids
this.notification = new NotificationService(
this.client,
this.tenantClient,
version
)
}
public get credentials() {
return this._credentials
}
public get tenant(): string {
return this._tenant
}
/**
* Sets the tenant used by tenant-specific services.
*
* @param tenantName Name of a tenant to be set
*/
public set tenant(tenantName: string) {
this._tenant = tenantName
// Organizer name is reset to tenant name, as in the general case it no longer corresponds
this._organizer = tenantName
this.tenantClient.defaults.baseURL = createBaseUrl(this.baseUrl, tenantName)
}
public get organizer(): string {
return this._organizer
}
public get deviceId() {
return this._deviceId
}
public set deviceId(deviceId: string) {
this._deviceId = deviceId
this.client.defaults.headers['x-device-id'] = deviceId
this.tenantClient.defaults.headers['x-device-id'] = deviceId
}
/**
* You can use the object in the response to construct JwtCredentials.
*/
public getToken(): Jwt | RefreshableJwt {
return this.credentials?.getToken()
}
public static getTokenPayload(token: string): Record<string, any> {
return jwtDecode(token)
}
/**
* Used to store the state of the SDK.
* Can be used via the build method to restore state.
*/
serialize(): BamOptions {
return {
version: this.version,
baseUrl: this.baseUrl,
organizer: this.organizer,
credentials: this.credentials,
}
}
/**
* Factory method for initializing the SDK.
* Can be used in conjunction with the serialize method to restore state.
*/
static async build(params: BamOptions): Promise<BAM> {
const {
baseUrl,
version = 'v1',
organizer,
retry,
cache,
camelCaseResponse,
} = params
const bam = new BAM(baseUrl, version, retry, cache, camelCaseResponse)
if (params.organizer) await bam.useOrganizer(organizer)
if (params.credentials) {
const creds = isCredential(params.credentials)
? params.credentials
: CredentialFactory.build(params.credentials)
await bam.authorize(creds)
}
return bam
}
/**
* Sets the organizer used on the instance. Any event, payment, reporting or websocket API call
* will be made to the organizer's tenant API.
*
* @param organizerName Name of an organizer to be set on the instance
*/
async useOrganizer(organizerName: string): Promise<void> {
if (organizerName === this._organizer) {
// No need to refresh the token if it is for the selected organizer
return
}
const organizer = await this.account.getOrganizer({ id: organizerName })
// Second term is to support the no case conversion use case
this.tenant =
organizer.organizationName || (organizer as any).organization_name
// Ordering is important as using the tenant setter resets the organizer name
this._organizer = organizer.name
// Refresh the token, fetching the permissions for the specified organizer
// For guest and service tokens, this does not change anything
await this.authorize(this._credentials, this._organizer)
}
/**
* Authorizes the SDK instance with a JWT obtained from the API.
* For user or organizer based login supply `PasswordCredentials`.
* For internal service authentication use `ServiceCredentials` with the appropriate organizer ID.
* For logging in with a wallet, use `WalletCredentials` aka the private key and certificate.
* For guest login use the parameterless version of `authorize`.
*
* @param credentials User, organizer, service or wallet credentials. In case of `undefined`, log in as guest.
* @param organizer The organizer name of the organizer for which the token is requested.
*/
async authorize(
credentials: ICredential = new GuestCredentials(),
organizer?: string
) {
this._credentials = credentials
const jwt = await credentials.authorize(this.auth, organizer)
// Set the auth headers
this.client.defaults.headers['Authorization'] = `Bearer ${jwt.token}`
this.tenantClient.defaults.headers['Authorization'] = `Bearer ${jwt.token}`
// This is an instance of jjs programming
const refreshFunction = async () => {
const newJwt = await this.credentials.refreshToken(this.auth, organizer)
return newJwt.token
}
setAxiosUpdateTokenFunction(this.client, refreshFunction)
setAxiosUpdateTokenFunction(this.tenantClient, refreshFunction)
return jwt
}
/**
* Sets the Bearer header of the calls to the specified token.
* You can also use the authorize method with JwtCredentials.
*
* @param jwt JWT to use for calls to the BAM services
*/
setAuthorization(jwt: Jwt) {
// Set the auth headers
this.client.defaults.headers['Authorization'] = `Bearer ${jwt.token}`
this.tenantClient.defaults.headers['Authorization'] = `Bearer ${jwt.token}`
}
/**
* Returns the status of currently used BAM services.
* This includes global services and the services for the current tenant.
*/
async health(): Promise<ClusterHealth> {
const accHealth = this.account.health()
const eventHealth = this.event.health()
const venueHealth = this.venue.health()
const paymentHealth = this.payment.health()
const blockchainHealth = this.blockchain.health()
// Await at the same time so its parallel
const [account, event, payment, blockchain, venue] = await Promise.all([
accHealth,
eventHealth,
paymentHealth,
blockchainHealth,
venueHealth,
])
// This needs to be changed if a service is extracted from another service
// This is mapped so there are no redundant calls to services
return {
auth: account,
account: account,
payment: payment,
event: event,
order: event,
blockchain: blockchain,
venue: venue,
}
}
/**
* Returns events with secure tickets for the authorized enrolled user.
*
* @param req.date Filter events by `end_at` date comparing with `midnight`. Expected values are `future`, `past` and `all`.
* @returns
*/
async getMyEvents(req: MyEventsQuery): Promise<MyEventsEvent[]> {
const query = queryString.stringify({
date: req.date,
})
const res = await this.client.get(`${this.version}/my_events?${query}`)
return res.data.data
}
/**
* Returns data that needs to be imported to the local validation server.
*/
async getImportData(request: ImportDataRequest): Promise<ImportData> {
const event = await this.event.getEvent({
id: request.eventId,
organizer_id: request.organizerId,
})
const [venue, qrCodes, validators] = await Promise.all([
this.venue.getVenue({
id: parseInt(event.venueId),
}),
this.event.getQrCodes({
id: request.eventId,
organizer_id: request.organizerId,
}),
this.validator.getExportedValidators(
{ id: request.organizerId },
{
event_id: request.eventId,
}
),
])
const importedValidators = validators.map((v) => {
// Casing issue regarding the blockchain service...
const wallet = {
...v.wallet,
private_key: v.wallet.privateKey,
privateKey: undefined as string,
}
return { ...v, wallet }
})
return {
event: {
event,
qr_codes: qrCodes,
},
venue: {
venue,
},
account: {
validators: importedValidators,
},
}
}
/**
* Enable sending the current version to the backend and checking against the configured minimum version
*
* @param version Version which is sent to the backend and checked against the minimum version. Format 'X.Y.Z'
*/
enableMinimumVersionCheck(version: string) {
// set x-app-version headers
this.client.defaults.headers['x-app-version'] = version
this.tenantClient.defaults.headers['x-app-version'] = version
}
}
function isCredential(
credential: ICredential | ICredentialData
): credential is ICredential {
return (<ICredential>credential)?.authorize !== undefined
}