src/app/licensing/licensing.component.ts

Implements

OnInit

Metadata

Relationships

Used by

Depends on

Index

Properties
Methods
Accessors

Constructor

constructor(route: ActivatedRoute, router: Router, licenceService: LicenceService, sasService: SasService, appService: AppService, sanitizer: DomSanitizer)
Parameters :
Name Type Optional
route ActivatedRoute No
router Router No
licenceService LicenceService No
sasService SasService No
appService AppService No
sanitizer DomSanitizer No

Methods

Public applyKeys
applyKeys()
Returns : void
Public copySyssite
copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[])
Parameters :
Name Type Optional
copyIconRef any No
copyTooltip any No
syssite string[] No
Returns : void
Private Async maybeSplitCombinedKey
maybeSplitCombinedKey(value: string)
Parameters :
Name Type Optional
value string No
Returns : Promise<boolean>
ngOnInit
ngOnInit()
Returns : void
Public Async onCombinedKeyInput
onCombinedKeyInput()
Returns : any
Public onFileCapture
onFileCapture(event: any, dropped: unknown)
Parameters :
Name Type Optional Default value
event any No
dropped unknown No false
Returns : void
Private Async refreshKeyPreview
refreshKeyPreview()
Returns : Promise<void>
Public switchType
switchType(type: "paste" | "file")
Parameters :
Name Type Optional
type "paste" | "file" No
Returns : void
Public toggleKeyFormat
toggleKeyFormat()
Returns : void
Public Async trimKeys
trimKeys()
Returns : any

Properties

Public action
Type : LicenseActions | null
Default value : null
Public activationKeyValue
Type : string
Default value : ''
Public applyingKeys
Type : boolean
Default value : false
Public combinedKeyValue
Type : string
Default value : ''
Public currentActivationKey
Type : unknown
Default value : this.licenceService.activationKey
Public currentLicenceKey
Type : unknown
Default value : this.licenceService.licenceKey
Public errorDetails
Type : string | undefined
Private Readonly errorIcon
Type : unknown
Default value : `<clr-icon class="is-error" shape="exclamation-circle" size="26"></clr-icon>`
Public inputType
Type : "file" | "paste"
Default value : 'file'
Public isAppFreeTier
Type : unknown
Default value : this.licenceService.isAppFreeTier
Public keyError
Type : string | undefined
Public keyFormat
Type : "combined" | "legacy"
Default value : 'combined'
Public licencefile
Type : { filename: string }
Default value : { filename: '' }
Public licenceFileError
Type : string | undefined
Public licenceFileLoading
Type : boolean
Default value : false
Public licenceKeyValue
Type : string
Default value : ''
Public licenseErrors
Type : { }
Public licenseKeyData
Type : LicenseKeyData | null
Default value : null
Public missmatchedKey
Type : string | undefined
Public protocol
Type : string
Default value : location.protocol === 'https:' ? 'HTTPS - secure connection' : 'HTTP - insecure connection'
Public syssite
Type : unknown
Default value : this.appService.syssite

Accessors

disableApplyButton
getdisableApplyButton()
protocolMismatch
getprotocolMismatch()
enabledFeatures
getenabledFeatures()
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
import { AppService, LicenceService, SasService } from '../services'
import { LicenseKeyData } from '../models/LicenseKeyData'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
import {
  detectLicenceKeyProtocolMismatch,
  LicenceKeyProtocolMismatch
} from './utils/detectLicenceKeyProtocolMismatch'
import {
  isCombinedLicenceKey,
  splitCombinedLicenceKey
} from './utils/combinedLicenceKey'
import { getEnabledFeatureLabels } from './utils/getEnabledFeatureLabels'

enum LicenseActions {
  key = 'key',
  register = 'register',
  limit = 'limit',
  update = 'update'
}

@Component({
  selector: 'app-licensing',
  templateUrl: './licensing.component.html',
  styleUrls: ['./licensing.component.scss'],
  encapsulation: ViewEncapsulation.None,
  standalone: false
})
export class LicensingComponent implements OnInit {
  public action: LicenseActions | null = null

  // Each message carries its own icon markup, rather than relying on a
  // sibling element in the template - these render via [innerHTML], which
  // replaces all of the host element's children, so a real <clr-icon>
  // placed in the template around the binding would never survive.
  private readonly errorIcon = `<clr-icon class="is-error" shape="exclamation-circle" size="26"></clr-icon>`

  // SafeHtml, built via bypassSecurityTrustHtml in the constructor (needs
  // DomSanitizer, not yet available in a field initializer) - Angular's
  // default [innerHTML] sanitizer strips unrecognised tags like <clr-icon>
  // (a custom element, not on its standard-tags allowlist), even though it
  // leaves the plain <a> in these same strings alone. Safe to bypass since
  // this content is entirely hardcoded here, never derived from user input.
  public licenseErrors: { [key: string]: SafeHtml }

  public keyError: string | undefined
  public errorDetails: string | undefined
  public missmatchedKey: string | undefined
  public licenceKeyValue: string = ''
  public activationKeyValue: string = ''

  // Purely which INPUT LAYOUT is shown (one field vs two) - not a strict
  // parsing gate. Recognising a combined key doesn't depend on this: see
  // maybeSplitCombinedKey(), called from both layouts' own input handlers.
  public keyFormat: 'combined' | 'legacy' = 'combined'
  public combinedKeyValue: string = ''

  public applyingKeys: boolean = false
  public protocol: string =
    location.protocol === 'https:'
      ? 'HTTPS - secure connection'
      : 'HTTP - insecure connection'

  public syssite = this.appService.syssite
  public currentLicenceKey = this.licenceService.licenceKey
  public currentActivationKey = this.licenceService.activationKey
  public isAppFreeTier = this.licenceService.isAppFreeTier

  public licenseKeyData: LicenseKeyData | null = null

  public inputType: 'file' | 'paste' = 'file'
  public licenceFileError: string | undefined
  public licenceFileLoading: boolean = false
  public licencefile: { filename: string } = {
    filename: ''
  }

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private licenceService: LicenceService,
    private sasService: SasService,
    private appService: AppService,
    private sanitizer: DomSanitizer
  ) {
    this.licenseErrors = {
      missing: this.sanitizer.bypassSecurityTrustHtml(
        `${this.errorIcon} Licence key is missing - please contact <a class="color-green" href="mailto: [email protected]">[email protected]</a> and enter valid keys below.`
      ),
      expired: this.sanitizer.bypassSecurityTrustHtml(
        `${this.errorIcon} Licence key is expired - please contact <a class="color-green" href="mailto: [email protected]">[email protected]</a> and enter valid keys below.`
      ),
      invalid: this.sanitizer.bypassSecurityTrustHtml(
        `${this.errorIcon} Licence key is invalid - please contact <a class="color-green" href="mailto: [email protected]">[email protected]</a> and enter valid keys below.`
      ),
      missmatch: this.sanitizer.bypassSecurityTrustHtml(
        `${this.errorIcon} Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: [email protected]">[email protected]</a> and enter valid keys below.`
      )
    }
  }

  ngOnInit(): void {
    this.licenceKeyValue = this.currentLicenceKey || ''
    this.activationKeyValue = this.currentActivationKey || ''

    this.route.queryParams.subscribe((queryParams: any) => {
      this.keyError = queryParams.error
      this.missmatchedKey = queryParams.missmatchId

      if (queryParams.details) {
        this.errorDetails = atob(queryParams.details)
      }
    })

    this.route.params.subscribe((params: any) => {
      let actionInUrl = params.action

      if (actionInUrl) {
        if (Object.values(LicenseActions).includes(actionInUrl)) {
          this.action = actionInUrl
        }
      }
    })

    this.licenseKeyData = this.licenceService.getLicenseKeyData()
  }

  public async trimKeys() {
    this.licenceKeyValue = this.licenceKeyValue.trim()
    this.activationKeyValue = this.activationKeyValue.trim()

    // Auto-detect regardless of keyFormat - a combined key pasted into the
    // legacy "Licence key" field on its own still gets recognised and
    // splits into both fields, rather than requiring the toggle to match.
    await this.maybeSplitCombinedKey(this.licenceKeyValue)
    await this.refreshKeyPreview()
  }

  public async onCombinedKeyInput() {
    this.combinedKeyValue = this.combinedKeyValue.trim()

    // The combined field is the sole source of truth for
    // licenceKeyValue/activationKeyValue in this layout - if it's been
    // cleared (or isn't a recognisable combined key), there's no key data
    // to preview, so clear rather than leave a stale split from an earlier
    // paste (which would otherwise keep decrypting to that old key).
    const wasSplit = await this.maybeSplitCombinedKey(this.combinedKeyValue)
    if (!wasSplit) {
      this.licenceKeyValue = ''
      this.activationKeyValue = ''
    }

    await this.refreshKeyPreview()
  }

  // Decrypts whatever's currently in licenceKeyValue/activationKeyValue
  // purely to preview its details (valid_until, users_allowed, ...) before
  // the user ever clicks Apply - decryptLicenseKey() has no side effects,
  // so this is safe to call speculatively on every input change. Clears
  // rather than leaves stale data on failure/incomplete input, since
  // showing a previous key's details next to a since-changed paste would
  // be actively misleading, not just uninformative.
  private async refreshKeyPreview(): Promise<void> {
    if (!this.licenceKeyValue || !this.activationKeyValue) {
      this.licenseKeyData = null
      return
    }

    try {
      this.licenseKeyData = await this.licenceService.decryptLicenseKey(
        this.licenceKeyValue,
        this.activationKeyValue
      )
    } catch {
      this.licenseKeyData = null
    }
  }

  public toggleKeyFormat() {
    this.keyFormat = this.keyFormat === 'combined' ? 'legacy' : 'combined'
  }

  // Shared by both input layouts' own handlers above - populates
  // licenceKeyValue/activationKeyValue when value is recognisably a
  // combined key, regardless of which field it was typed/pasted into.
  private async maybeSplitCombinedKey(value: string): Promise<boolean> {
    if (!isCombinedLicenceKey(value)) return false

    const split = await splitCombinedLicenceKey(value)
    if (!split) return false

    this.licenceKeyValue = split.licenceKey
    this.activationKeyValue = split.activationKey
    return true
  }

  public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
    const syssiteString = syssite.join('\n')

    navigator.clipboard.writeText(syssiteString).then(() => {
      copyIconRef.setAttribute('shape', 'check')
      copyIconRef.setAttribute('class', 'is-success')
      copyTooltip.innerText = 'Copied!'

      setTimeout(() => {
        copyIconRef.setAttribute('shape', 'copy')
        copyIconRef.removeAttribute('class')
        copyTooltip.innerText = 'Copy to clipboard'
      }, 1000)
    })
  }

  public applyKeys() {
    this.applyingKeys = true

    let table = {
      keyupload: [
        {
          ACTIVATION_KEY: this.activationKeyValue,
          LICENCE_KEY: this.licenceKeyValue
        }
      ]
    }

    this.sasService
      .request('admin/registerkey', table)
      .then((res: RequestWrapperResponse) => {
        if (
          res.adapterResponse.return &&
          res.adapterResponse.return[0] &&
          res.adapterResponse.return[0].MSG === 'SUCCESS'
        ) {
          this.router.navigateByUrl('/').then(() => {
            window.location.reload()
          })
        }
      })
      .finally(() => {
        this.applyingKeys = false
      })
  }

  public onFileCapture(event: any, dropped = false) {
    let file = dropped ? event[0] : event.target.files[0]
    this.licencefile.filename = file.name

    if (!file) return

    this.licenceFileLoading = true

    const reader = new FileReader()

    reader.onload = async (evt) => {
      this.licenceFileError = 'Error reading file.'

      if (!evt || !evt.target) return
      if (evt.target.readyState != 2) return
      if (evt.target.error) return
      if (!evt.target.result) return

      this.licenceFileLoading = false
      this.licenceFileError = undefined

      const fileArr = evt.target.result.toString().trim().split('\n')

      // A combined-key file is a single line - splits into both fields.
      // Anything else keeps the existing 2-line legacy file behaviour.
      if (
        fileArr.length !== 1 ||
        !(await this.maybeSplitCombinedKey(fileArr[0]))
      ) {
        this.activationKeyValue = fileArr[1]
        this.licenceKeyValue = fileArr[0]
      }

      await this.refreshKeyPreview()
    }

    reader.readAsText(file)
  }

  public switchType(type: 'paste' | 'file') {
    this.inputType = type
  }

  get disableApplyButton(): boolean {
    if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
      return true
    if (
      this.licenceKeyValue === this.currentLicenceKey &&
      this.activationKeyValue === this.currentActivationKey
    )
      return true
    // A protocol mismatch is a deterministic predictor of decrypt failure
    // (see detectLicenceKeyProtocolMismatch's own doc comment) - submitting
    // anyway would just trade this specific warning for a generic "invalid
    // key" error after a round trip to the backend.
    if (this.protocolMismatch) return true

    return false
  }

  // Warns before the user even submits - covers both the file-upload and
  // paste tabs, since both feed licenceKeyValue/activationKeyValue. Passes
  // actual WebCrypto availability, not location.protocol - see
  // detectLicenceKeyProtocolMismatch's own doc comment for why (localhost
  // is a secure context even over http).
  get protocolMismatch(): LicenceKeyProtocolMismatch {
    return detectLicenceKeyProtocolMismatch(
      this.licenceKeyValue,
      this.activationKeyValue,
      !!(window.crypto && window.crypto.subtle)
    )
  }

  get enabledFeatures(): string[] {
    return getEnabledFeatureLabels(this.licenseKeyData?.features)
  }
}
<div *ngIf="action !== 'register'" class="card">
  <div class="card-header">Licencing</div>

  <div [ngSwitch]="action" class="card-block">
    <div class="card-text">
      <ng-container *ngSwitchCase="'key'">
        <p class="key-error" *ngIf="!keyError">
          <clr-icon
            class="is-error"
            shape="exclamation-circle"
            size="26"
          ></clr-icon>
          Licence key is invalid. We can't provide you more details at the
          moment
        </p>

        <p
          class="key-error"
          *ngIf="keyError"
          [innerHTML]="licenseErrors[keyError]"
        ></p>

        <p *ngIf="errorDetails"><strong>Details:</strong> {{ errorDetails }}</p>
      </ng-container>

      <ng-container *ngSwitchCase="'limit'">
        <p class="key-error">
          <clr-icon
            class="is-error"
            shape="exclamation-circle"
            size="26"
          ></clr-icon>
          The registered number of users reached the limit specified for your
          licence. Please contact
          <contact-link classes="color-green" />
          or your reseller to arrange additional licences for this product.
        </p>
      </ng-container>

      <ng-container *ngSwitchCase="'update'">
        <p class="key-error">
          Update the license key by uploading the licence file or by pasting a
          license key and activation key in the inputs below.
        </p>
      </ng-container>

      <p class="m-0 mt-10"><strong>Protocol:</strong> {{ protocol }}</p>

      <p class="m-0">
        <strong>SYSSITE:</strong>
        <span
          *ngFor="let id of syssite.value; let i = index"
          [class.misskey]="missmatchedKey && missmatchedKey === id"
        >
          {{ id }}{{ i === syssite.value?.length! - 1 ? '' : ',' }}
        </span>

        <a
          class="tooltip tooltip-md tooltip-top-right"
          (click)="copySyssite(copyIcon, copyTooltip, syssite.value || [])"
        >
          <clr-icon
            #copyIcon
            class="cursor-pointer"
            shape="copy"
            size="15"
          ></clr-icon>
          <span #copyTooltip class="tooltip-content">Copy to clipboard</span>
        </a>
      </p>

      <clr-tabs>
        <clr-tab>
          <button clrTabLink>Upload licence</button>
          <clr-tab-content>
            <input
              #licenceFile
              (change)="onFileCapture($event)"
              type="file"
              hidden
            />
            <div
              (click)="licenceFile.click()"
              appFileDrop
              (fileDrop)="onFileCapture($event, true)"
              class="drop-area"
            >
              <clr-spinner
                class="spinner-sm"
                *ngIf="licenceFileLoading"
              ></clr-spinner>
              <ng-container *ngIf="!licenceFileLoading">
                <div *ngIf="licencefile.filename === ''">
                  Drop / Browse licence file
                </div>
                <div *ngIf="licencefile.filename !== ''">
                  Selected file: <strong>{{ licencefile.filename }}</strong>
                </div>
                <div *ngIf="licenceFileError">
                  <strong>{{ licenceFileError }}</strong>
                </div>
              </ng-container>
            </div>
          </clr-tab-content>
        </clr-tab>

        <clr-tab>
          <button clrTabLink>Paste licence</button>
          <clr-tab-content>
            <ng-container *ngIf="keyFormat === 'combined'">
              <form class="clr-form combined-key-form">
                <p>Licence key:</p>
                <div class="clr-control-container">
                  <textarea
                    [(ngModel)]="combinedKeyValue"
                    (mouseleave)="onCombinedKeyInput()"
                    name="combined-key-area"
                    placeholder="Paste licence key here"
                    class="clr-textarea"
                  ></textarea>
                </div>
              </form>

              <button
                type="button"
                class="btn btn-sm btn-link p-0"
                (click)="toggleKeyFormat()"
              >
                Paste as two separate keys instead
              </button>
            </ng-container>

            <ng-container *ngIf="keyFormat === 'legacy'">
              <form class="clr-form license-key-form">
                <p>Licence key:</p>
                <div class="clr-control-container">
                  <textarea
                    [(ngModel)]="licenceKeyValue"
                    (mouseleave)="trimKeys()"
                    name="license-key-area"
                    placeholder="Paste licence key here"
                    class="clr-textarea"
                  ></textarea>
                </div>
              </form>

              <form class="clr-form activation-key-form">
                <p>Activation key:</p>
                <div class="clr-control-container">
                  <textarea
                    [(ngModel)]="activationKeyValue"
                    (mouseleave)="trimKeys()"
                    name="activation-key-area"
                    placeholder="Paste activation key here"
                    class="clr-textarea"
                  ></textarea>
                </div>
              </form>

              <button
                type="button"
                class="btn btn-sm btn-link p-0"
                (click)="toggleKeyFormat()"
              >
                Paste as a single key instead
              </button>
            </ng-container>
          </clr-tab-content>
        </clr-tab>
      </clr-tabs>

      <!--
        licenseKeyData starts as whatever key is currently active (set once
        in ngOnInit), then live-updates to preview a pasted-but-not-yet-
        applied key's own details instead - see refreshKeyPreview().
      -->
      <div *ngIf="licenseKeyData" class="key-details">
        <h6 cds-text="subsection" class="mt-15 mb-10">Key Details</h6>

        <p class="m-0">
          <strong>Valid until:</strong>
          {{ licenseKeyData.valid_until }}
          <span *ngIf="licenseKeyData.demo">(demo/free tier key)</span>
        </p>

        <p class="m-0">
          <strong>Allowed users:</strong>
          {{ licenseKeyData.users_allowed }}
        </p>

        <p class="m-0">
          <strong>Site ID(s) in this key:</strong>
          {{
            (licenseKeyData.site_id_multiple?.length
              ? licenseKeyData.site_id_multiple
              : [licenseKeyData.site_id]
            ).join(', ')
          }}
        </p>

        <p *ngIf="enabledFeatures.length" class="m-0">
          <strong>Enabled features:</strong>
          {{ enabledFeatures.join(', ') }}
        </p>
      </div>
    </div>

    <div class="card-footer d-flex clr-align-items-center">
      <button
        (click)="applyKeys()"
        class="btn btn-primary apply-keys"
        [clrLoading]="applyingKeys"
        [disabled]="disableApplyButton"
      >
        Apply licence keys
      </button>

      <button
        *ngIf="isAppFreeTier.value"
        routerLink="/"
        class="btn btn-sm btn-link"
      >
        Continue with free tier
      </button>
    </div>

    <p
      class="key-error protocol-mismatch-warning"
      *ngIf="protocolMismatch === 'requiresHttps'"
    >
      <clr-icon
        class="color-orange"
        shape="exclamation-triangle"
        size="26"
      ></clr-icon>
      This key was generated for a secure (HTTPS) connection, but DataController
      is currently running over HTTP - it will not activate here. Access
      DataController via HTTPS, or contact
      <contact-link classes="color-green" />
      for an HTTP-compatible key.
    </p>

    <p
      class="key-error protocol-mismatch-warning"
      *ngIf="protocolMismatch === 'requiresHttp'"
    >
      <clr-icon
        class="color-orange"
        shape="exclamation-triangle"
        size="26"
      ></clr-icon>
      This key was generated for an insecure (HTTP) connection, but this page is
      running in a secure browsing context (HTTPS, or localhost) - it will not
      activate here. Contact
      <contact-link classes="color-green" />
      for an HTTPS-compatible key.
    </p>
  </div>
</div>

<app-terms *ngIf="action === 'register'"></app-terms>

./licensing.component.scss

.key-error clr-icon {
  // clr-icon defaults to vertical-align: middle, which sits it noticeably
  // above the text baseline next to it - align its bottom edge with the
  // text's instead.
  vertical-align: bottom;
  margin-right: 10px;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""