src/app/services/licence.service.ts

Metadata

Index

Properties
Methods

Constructor

constructor(loggerService: LoggerService, eventService: EventService, sasService: SasService, helperService: HelperService, router: Router)
Parameters :
Name Type Optional
loggerService LoggerService No
eventService EventService No
sasService SasService No
helperService HelperService No
router Router No

Methods

Public Async activation
activation(startupservice: any)
Parameters :
Name Type Optional
startupservice any No
Returns : any
Private applicationActivation
applicationActivation(licenseData: LicenseKeyData, variables?: Globvar, startup_site_id?: string, skipRouting?: boolean)

Checks if:

  • site id match
  • key is/isn't expired
  • user can be registered (user limit)
Parameters :
Name Type Optional Description
licenseData LicenseKeyData No
variables Globvar Yes

from SAS response

startup_site_id string Yes

of current sas environment

skipRouting boolean Yes
Returns : void
Public arrayBufferToBase64
arrayBufferToBase64(arrayBuffer: ArrayBuffer)
Parameters :
Name Type Optional
arrayBuffer ArrayBuffer No
Returns : Promise<string>
Public base64ToArrayBuffer
base64ToArrayBuffer(base64: string)
Parameters :
Name Type Optional
base64 string No
Returns : Promise<ArrayBuffer>
Private calculateExpiry
calculateExpiry(licenseData: LicenseKeyData)
Parameters :
Name Type Optional
licenseData LicenseKeyData No
Public checkLineageLimit
checkLineageLimit(checkOnly?: boolean)

Checks fo lineage daily renderings limit

othervise it will increment number of renderings.

Parameters :
Name Type Optional Description
checkOnly boolean Yes

if true it will only check if limit reached othervise it will increment number of renderings.

Returns : boolean

true if limited and false if not limited

Public deactivateApp
deactivateApp(redirectTo?: string)
Parameters :
Name Type Optional
redirectTo string Yes
Returns : void
Private decodeLicenceFeatures
decodeLicenceFeatures(licenseData: LicenseKeyData)

Decode and set features that are encoded in the key - accepts either an already-issued key's legacy positional string or a newly-issued key's named object, see decodeLicenceFeatures for the format contract.

Parameters :
Name Type Optional Description
licenseData LicenseKeyData No

from the licence key

Returns : void
Public decryptLicenseKey
decryptLicenseKey(licenseKey: string, activationKey: string)

If decryption fails, key will be marked as invalid and app not activated. Public (not just used internally by licensing()) since it's a pure decrypt with no side effects - also used to preview a pasted key's details before it's actually applied.

Parameters :
Name Type Optional
licenseKey string No
activationKey string No
Public getHotLicenseKey
getHotLicenseKey()
Returns : any
Public getLicenseKeyData
getLicenseKeyData()
Returns : any
Private handleExpiry
handleExpiry(expiry_date: moment.Moment, variables: Globvar, startup_site_id: string, skipRouting: boolean)
Parameters :
Name Type Optional
expiry_date moment.Moment No
variables Globvar No
startup_site_id string No
skipRouting boolean No
Returns : void
Private handleSiteIdMissmatch
handleSiteIdMissmatch(licenseData: LicenseKeyData, variables: Globvar, startup_site_id: string, skipRouting: boolean)

One of our customers is having multiple site ids in same organization. So we added support for list od site ids to be placed in licence key. But we also want to have dc backwards compatible so we have to check for both properties legacy and new. New key must work with old DC Old key must work with new DC

Parameters :
Name Type Optional
licenseData LicenseKeyData No
variables Globvar No
startup_site_id string No
skipRouting boolean No
Returns : void
Private handleUserRegistration
handleUserRegistration(licenseData: LicenseKeyData, variables: Globvar, skipRouting: boolean)
Parameters :
Name Type Optional
licenseData LicenseKeyData No
variables Globvar No
skipRouting boolean No
Returns : void
Private handleUsersAllowed
handleUsersAllowed(licenseData: LicenseKeyData)
Parameters :
Name Type Optional
licenseData LicenseKeyData No
Returns : void
Public Async licensing
licensing(globvars: Globvar[], startup_site_id: string)

Parsing the licence key, checking if it's valid, expired etc. It also sets the features encoded in the key.

Parameters :
Name Type Optional Description
globvars Globvar[] No

from response, includes licence key, is user registered and user registered count

startup_site_id string No

system site id

Returns : unknown
Private setLineageRender
setLineageRender(timestamp: number, renders: number)
Parameters :
Name Type Optional
timestamp number No
renders number No
Returns : void
Private setSiteId
setSiteId(licenseData: LicenseKeyData)
Parameters :
Name Type Optional
licenseData LicenseKeyData No
Returns : void
Public setStartupserviceRules
setStartupserviceRules(startupservice: any)

Sets feature restrictions included in startupservice

Parameters :
Name Type Optional Description
startupservice any No

SAS service response

Returns : void

Properties

Private _licenceState
Type : LicenceState
Default value : freeTierConfig
Public activationKey
Type : string | undefined
Private allFeaturesOn
Type : LicenceState
Default value : { viewer_rows_allowed: Infinity, editor_rows_allowed: Infinity, stage_rows_allowed: Infinity, history_rows_allowed: Infinity, submit_rows_limit: Infinity, tables_in_library_limit: Infinity, viewbox_limit: Infinity, lineage_daily_limit: Infinity, viewbox: true, fileUpload: true, editRecord: true, addRecord: true }
Public appLocked
Type : unknown
Default value : new BehaviorSubject<boolean>(false)
Public currentUserCanRegister
Type : unknown
Default value : new BehaviorSubject<boolean>(false)
Private freeTierLicenceData
Type : LicenseKeyData
Default value : { demo: true, hot_license_key: DEFAULT_HOT_LICENCE_KEY, users_allowed: this._licenceState.users_allowed || 1, valid_until: moment().add(1, 'year').format('YYYY-MM-DD'), site_id: '', site_id_multiple: [] }
Public hot_license_key
Type : unknown
Default value : new BehaviorSubject<string | undefined>(undefined)
Public isAppActivated
Type : unknown
Default value : new BehaviorSubject<boolean | null>(null)
Public isAppFreeTier
Type : unknown
Default value : new BehaviorSubject<boolean>(false)
Public isAppOverCapacity
Type : unknown
Default value : new BehaviorSubject<boolean>(false)
Public licenceKey
Type : string | undefined
Public licenceProblem
Type : unknown
Default value : new BehaviorSubject<string | null>(null)
Public licenceState
Type : BehaviorSubject<LicenceState>
Default value : new BehaviorSubject<LicenceState>(this._licenceState)
Public licenseExpiresInDays
Type : unknown
Default value : new BehaviorSubject<number | null>(null)
Private licenseKeyData
Type : LicenseKeyData | null
Default value : null
Public userCountLimitation
Type : boolean
Default value : false
import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { LicenseKeyData } from '../models/LicenseKeyData'
import { SasService } from './sas.service'
import moment from 'moment'
import * as base64Converter from 'base64-arraybuffer'
import * as encoding from 'text-encoding'
import { Router } from '@angular/router'
import { Globvar } from '../models/sas/public-startupservice.model'
import { freeTierConfig } from '../free-tier.config'
import { HelperService } from './helper.service'
import { LicenceState } from '../models/LicenceState'
import { decodeLicenceFeatures as decodeFeatures } from './utils/decodeLicenceFeatures'
import { LoggerService } from './logger.service'
import { EventService } from './event.service'

/**
 * Handsontable licence key used until a licence is loaded from the backend.
 * The decrypted licence carries the customer's own key (`hot_license_key`), so
 * this is only the pre-licence / free-tier default - it used to be supplied by
 * the `<sasjs>` tag.
 */
export const DEFAULT_HOT_LICENCE_KEY = 'non-commercial-and-evaluation'

@Injectable({
  providedIn: 'root'
})
export class LicenceService {
  public userCountLimitation: boolean = false //toggler for user count limit registration

  public licenceKey: string | undefined
  public activationKey: string | undefined
  private licenseKeyData: LicenseKeyData | null = null

  private _licenceState: LicenceState = freeTierConfig
  private allFeaturesOn: LicenceState = {
    viewer_rows_allowed: Infinity,
    editor_rows_allowed: Infinity,
    stage_rows_allowed: Infinity,
    history_rows_allowed: Infinity,
    submit_rows_limit: Infinity,
    tables_in_library_limit: Infinity,
    viewbox_limit: Infinity,
    lineage_daily_limit: Infinity,
    viewbox: true,
    fileUpload: true,
    editRecord: true,
    addRecord: true
  }

  private freeTierLicenceData: LicenseKeyData = {
    demo: true,
    hot_license_key: DEFAULT_HOT_LICENCE_KEY,
    users_allowed: this._licenceState.users_allowed || 1,
    valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
    site_id: '',
    site_id_multiple: []
  }

  public isAppActivated = new BehaviorSubject<boolean | null>(null)
  public isAppOverCapacity = new BehaviorSubject<boolean>(false)
  public currentUserCanRegister = new BehaviorSubject<boolean>(false)
  public appLocked = new BehaviorSubject<boolean>(false)
  public licenseExpiresInDays = new BehaviorSubject<number | null>(null)
  public isAppFreeTier = new BehaviorSubject<boolean>(false)
  public licenceProblem = new BehaviorSubject<string | null>(null) //contains the link for particular licence error if any
  public hot_license_key = new BehaviorSubject<string | undefined>(undefined)
  public licenceState: BehaviorSubject<LicenceState> =
    new BehaviorSubject<LicenceState>(this._licenceState)

  constructor(
    private loggerService: LoggerService,
    private eventService: EventService,
    private sasService: SasService,
    private helperService: HelperService,
    private router: Router
  ) {
    this.sasService.incorrectSiteIdEmitter.subscribe((missmatchId: string) => {
      if (this.isAppActivated.value !== null && !this.isAppFreeTier.value) {
        const url = `/licensing/key?error=missmatch&missmatchId=${missmatchId}&force=true`
        this.licenceProblem.next(url)
        this.deactivateApp(url)
      }
    })
  }

  public async activation(startupservice: any) {
    this.freeTierLicenceData.hot_license_key = DEFAULT_HOT_LICENCE_KEY

    await this.setStartupserviceRules(startupservice)

    await this.licensing(startupservice.globvars, startupservice.SYSSITE)
  }

  /**
   * Sets feature restrictions included in startupservice
   * @param startupservice SAS service response
   */
  public setStartupserviceRules(startupservice: any) {
    // Whether or not to disable EDIT RECORD modal
    this._licenceState.editRecord = !(
      startupservice.globvars[0].DC_RESTRICT_EDITRECORD === 'YES'
    )
  }

  /**
   * Parsing the licence key, checking if it's valid, expired etc.
   * It also sets the features encoded in the key.
   * @param globvars from response, includes licence key, is user registered and user registered count
   * @param startup_site_id system site id
   */
  public async licensing(globvars: Globvar[], startup_site_id: string) {
    if (!globvars || !globvars[0]) {
      const err = 'Error getting "Globvars" from startupservice response.'
      console.error(err)

      this.isAppActivated.next(false)
      this.router.navigateByUrl(
        `/licensing/key?error=invalid&details=${btoa(err)}`
      )
      return
    }

    let variables = globvars[0]

    // Trim whitespace — the SAS makedata service initialises licence keys
    // as a single space, which is truthy in JS but not a valid key.
    if (variables.LICENCE_KEY)
      variables.LICENCE_KEY = variables.LICENCE_KEY.trim()
    if (variables.ACTIVATION_KEY)
      variables.ACTIVATION_KEY = variables.ACTIVATION_KEY.trim()

    if (
      variables.LICENCE_KEY === undefined ||
      variables.ACTIVATION_KEY === undefined ||
      variables.REGISTERCOUNT === null ||
      variables.REGISTERCOUNT === undefined ||
      variables.ISREGISTERED === null ||
      variables.ISREGISTERED === undefined
    ) {
      console.error('Some of globvars are not present')
      this.isAppActivated.next(false)
      this.eventService.showInfoModal(
        'Error',
        'Some of the globvars are not present in the startupservice',
        'public/startupservice'
      )
      // this.router.navigateByUrl('/licensing/key?error=missing')
      return
    }

    if (!variables.LICENCE_KEY || !variables.ACTIVATION_KEY) {
      //fall back to free tier if key not present
      // setTimeout(() => {
      return await this.applicationActivation(
        this.freeTierLicenceData,
        variables,
        startup_site_id
      )
      // })

      // return
    }

    this.licenceKey = variables.LICENCE_KEY
    this.activationKey = variables.ACTIVATION_KEY

    await this.decryptLicenseKey(
      variables.LICENCE_KEY,
      variables.ACTIVATION_KEY
    ).then(
      async (decryptedLicenseData: LicenseKeyData) => {
        await this.applicationActivation(
          decryptedLicenseData,
          variables,
          startup_site_id
        )
      },
      async (err: any) => {
        const errString = `Error decrypting license key. ${err}`
        console.error(errString)
        // this.isAppActivated.next(false)
        const url = `/licensing/key?error=invalid&details=${btoa(
          errString
        )}&force=true`
        this.licenceProblem.next(url)

        setTimeout(() => {
          this.applicationActivation(
            this.freeTierLicenceData,
            variables,
            startup_site_id,
            true
          )
          this.router.navigateByUrl(url)
        })
      }
    )
  }

  /**
   * Checks if:
   * - site id match
   * - key is/isn't expired
   * - user can be registered (user limit)
   * @param licenseData
   * @param variables from SAS response
   * @param startup_site_id of current sas environment
   */
  private applicationActivation(
    licenseData: LicenseKeyData,
    variables?: Globvar,
    startup_site_id?: string,
    skipRouting?: boolean
  ) {
    if (!skipRouting) skipRouting = false

    this.setSiteId(licenseData)

    if (!licenseData.demo && variables && startup_site_id) {
      this.handleSiteIdMissmatch(
        licenseData,
        variables,
        startup_site_id,
        skipRouting
      )
    }

    let hotLicenceKey =
      licenseData.hot_license_key !== undefined
        ? licenseData.hot_license_key
        : this.hot_license_key.value

    if (!hotLicenceKey) hotLicenceKey = DEFAULT_HOT_LICENCE_KEY

    this.hot_license_key.next(hotLicenceKey)

    if (this.userCountLimitation) {
      this.handleUsersAllowed(licenseData)
    }

    this.licenseKeyData = this.helperService.deepClone(licenseData)
    ;(window as any).appinfo() //Show the table of appinfo in the console

    const { expiry_date, daysToExpiry } = this.calculateExpiry(licenseData)

    ;(window as any).licenseExpiresIn = daysToExpiry
    this.licenseExpiresInDays.next(daysToExpiry)

    if (daysToExpiry <= 0) {
      console.error('License key has expired')

      if (variables && startup_site_id) {
        this.handleExpiry(expiry_date, variables, startup_site_id, skipRouting)
      }

      return
    }

    this.isAppFreeTier.next(!!licenseData.demo)

    if (variables) {
      this.handleUserRegistration(licenseData, variables, skipRouting)
    }

    this.decodeLicenceFeatures(licenseData)
    this.licenceState.next(this._licenceState) //set the global licence state
  }

  /**
   * Decode and set features that are encoded in the key - accepts either
   * an already-issued key's legacy positional string or a newly-issued
   * key's named object, see decodeLicenceFeatures for the format contract.
   * @param licenseData from the licence key
   */
  private decodeLicenceFeatures(licenseData: LicenseKeyData) {
    if (!licenseData.features) {
      if (licenseData.demo) {
        return
      } else {
        this._licenceState = this.allFeaturesOn
        return
      }
    }

    this._licenceState = {
      ...this._licenceState,
      ...decodeFeatures(licenseData.features)
    }

    this.loggerService.log('Licence state:', this._licenceState)
  }

  /**
   * If decryption fails, key will be marked as invalid and app not activated.
   * Public (not just used internally by licensing()) since it's a pure
   * decrypt with no side effects - also used to preview a pasted key's
   * details before it's actually applied.
   */
  public decryptLicenseKey(
    licenseKey: string,
    activationKey: string
  ): Promise<LicenseKeyData> {
    return new Promise<LicenseKeyData>(async (resolve, reject) => {
      if (!(window.crypto && window.crypto.subtle)) {
        try {
          let licenseKeyDecoded = atob(licenseKey.split('').reverse().join(''))

          resolve(JSON.parse(licenseKeyDecoded))
        } catch (ex) {
          reject('Error parsing http license key data. ' + ex)
        }
      }

      let licenseKeyBytes = await this.base64ToArrayBuffer(licenseKey).catch(
        (err: any) => {
          reject(err)
        }
      )

      let activationKeyBytes: ArrayBuffer
      const activationKeyBytesTemp = await this.base64ToArrayBuffer(
        activationKey
      ).catch((err: any) => {
        reject(err)
      })

      if (!activationKeyBytesTemp) {
        reject('Missing activation key')
        return
      }

      activationKeyBytes = activationKeyBytesTemp

      let decryptionKey

      try {
        decryptionKey = await window.crypto.subtle.importKey(
          'pkcs8',
          activationKeyBytes,
          {
            name: 'RSA-OAEP',
            hash: 'SHA-256'
          },
          true,
          ['decrypt']
        )
      } catch (ex) {
        reject('Unable to import decryption key: ' + ex)
      }

      if (!decryptionKey) {
        reject('Unable to import decryption key')
        return
      }

      if (!licenseKeyBytes) {
        reject('License key bytes missing')
      } else {
        try {
          window.crypto.subtle
            .decrypt(
              {
                name: 'RSA-OAEP',
                hash: { name: 'SHA-256' }
              } as any,
              decryptionKey,
              licenseKeyBytes
            )
            .then(
              (value: any) => {
                let keyDataString = new encoding.TextDecoder().decode(value)

                try {
                  resolve(JSON.parse(keyDataString))
                } catch (ex) {
                  reject('Error parsing license key data. ' + ex)
                }
              },
              (err: any) => {
                reject(err)
              }
            )
        } catch (ex) {
          reject(ex)
        }
      }
    })
  }

  public getHotLicenseKey() {
    return this.hot_license_key.value
  }

  public getLicenseKeyData() {
    return this.licenseKeyData
  }

  public deactivateApp(redirectTo?: string) {
    // this.isAppActivated.next(false)

    this._licenceState = freeTierConfig
    this.applicationActivation(this.freeTierLicenceData)

    if (redirectTo) this.router.navigateByUrl(redirectTo)
  }

  /**
   * Checks fo lineage daily renderings limit
   *
   * @param checkOnly if true it will only check if limit reached
   * othervise it will increment number of renderings.
   * @returns true if limited and false if not limited
   */
  public checkLineageLimit(checkOnly?: boolean): boolean {
    if (this.licenceState.value.lineage_daily_limit === Infinity) return false

    const renders = localStorage.getItem('lineage_renders')
    const todayTimestamp = moment().valueOf()

    if (!renders) {
      if (!checkOnly) this.setLineageRender(todayTimestamp, 1)
      return false
    }

    const timestamp = parseInt(renders.split(',')[0])
    const noOfRenders = parseInt(renders.split(',')[1])

    if (moment(timestamp).isSame(moment(), 'day')) {
      if (noOfRenders >= this.licenceState.value.lineage_daily_limit)
        return true

      if (!checkOnly) this.setLineageRender(todayTimestamp, noOfRenders + 1)
      return false
    }

    if (!checkOnly) this.setLineageRender(todayTimestamp, 1)
    return false
  }

  private setLineageRender(timestamp: number, renders: number) {
    localStorage.setItem('lineage_renders', `${timestamp},${renders}`)
  }

  public base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> {
    return new Promise(async (resolve, reject) => {
      resolve(base64Converter.decode(base64))
    })
  }

  public arrayBufferToBase64(arrayBuffer: ArrayBuffer): Promise<string> {
    return new Promise((resolve, reject) => {
      resolve(base64Converter.encode(arrayBuffer))
    })
  }

  private setSiteId(licenseData: LicenseKeyData) {
    if (licenseData.site_id_multiple) {
      this.sasService.setLicenseSiteId(licenseData.site_id_multiple)
    } else if (licenseData.site_id) {
      this.sasService.setLicenseSiteId(licenseData.site_id)
    }
  }

  /**
   * One of our customers is having multiple site ids in same organization.
   * So we added support for list od site ids to be placed in licence key.
   * But we also want to have dc backwards compatible so we have to check
   * for both properties legacy and new.
   * New key must work with old DC
   * Old key must work with new DC
   */
  private handleSiteIdMissmatch(
    licenseData: LicenseKeyData,
    variables: Globvar,
    startup_site_id: string,
    skipRouting: boolean
  ) {
    let noLegacySiteId: boolean = false
    let noMultipleSiteId: boolean = false

    if (!licenseData.site_id || licenseData.site_id !== startup_site_id) {
      noLegacySiteId = true
    }

    if (
      !licenseData.site_id_multiple ||
      !licenseData.site_id_multiple.includes(startup_site_id)
    ) {
      noMultipleSiteId = true
    }

    if (noLegacySiteId && noMultipleSiteId) {
      console.error('The key provided is for different organization.')
      // this.isAppActivated.next(false)

      setTimeout(() => {
        const url = '/licensing/key?error=missmatch&force=true'
        this.licenceProblem.next(url)

        this._licenceState = freeTierConfig

        this.applicationActivation(
          this.freeTierLicenceData,
          variables,
          startup_site_id,
          true
        )

        if (!skipRouting) this.router.navigateByUrl(url)
      })

      return
    }
  }

  private calculateExpiry(licenseData: LicenseKeyData): {
    expiry_date: moment.Moment
    daysToExpiry: number
  } {
    let expiry_date = moment(licenseData.valid_until, 'YYYY-MM-DD').startOf(
      'day'
    )
    let current_date = moment().startOf('day')
    let daysToExpiry = expiry_date.diff(current_date, 'days')

    return {
      expiry_date,
      daysToExpiry
    }
  }

  private handleExpiry(
    expiry_date: moment.Moment,
    variables: Globvar,
    startup_site_id: string,
    skipRouting: boolean
  ) {
    setTimeout(() => {
      const url = `/licensing/key?force=true&error=expired&details=${btoa(
        'Expiry date: ' + expiry_date.format('DD/MM/YYYY')
      )}`
      this.licenceProblem.next(url)
      // Instead of blocking the app we revert to free tier version of the app
      this.applicationActivation(
        this.freeTierLicenceData,
        variables,
        startup_site_id,
        true
      )

      if (!skipRouting) {
        this.router.navigateByUrl(url)
      }
    })
  }

  private handleUsersAllowed(licenseData: LicenseKeyData) {
    if (licenseData.demo) {
      if (this._licenceState.users_allowed !== undefined)
        licenseData.users_allowed =
          licenseData.users_allowed > 0
            ? licenseData.users_allowed
            : this._licenceState.users_allowed
    }

    this._licenceState.users_allowed = licenseData.users_allowed
  }

  private handleUserRegistration(
    licenseData: LicenseKeyData,
    variables: Globvar,
    skipRouting: boolean
  ) {
    if (this.userCountLimitation) {
      //if limit disabled we don't track the count
      if (variables.REGISTERCOUNT > licenseData.users_allowed) {
        //App is over capacity of users
        console.warn('App has more users registered then licensed.')
        this.isAppOverCapacity.next(true)
      } else if (variables.REGISTERCOUNT === licenseData.users_allowed) {
        //App reached capacity of users
        console.warn('App has hit limit of users registered.')
      }
    }

    if (variables.ISREGISTERED === 1) {
      this.isAppActivated.next(true)
      return
    } else {
      // if limit is disabled we always go into register directly
      if (
        variables.REGISTERCOUNT < licenseData.users_allowed ||
        !this.userCountLimitation
      ) {
        console.log('User can register')
        this.currentUserCanRegister.next(true)
        // this.appLocked.next(true)
        this.isAppActivated.next(false)
        if (!skipRouting) this.router.navigateByUrl('/licensing/register')
        //After register, activate app
        return
      } else {
        this.currentUserCanRegister.next(false)
        this.isAppActivated.next(false)
        // this.appLocked.next(true)
        if (!skipRouting) this.router.navigateByUrl('/licensing/limit')
      }
    }
  }
}

results matching ""

    No results matching ""