src/app/licensing/licensing.component.ts
| encapsulation | ViewEncapsulation.None |
| selector | app-licensing |
| styleUrls | ./licensing.component.scss |
| templateUrl | ./licensing.component.html |
Properties |
|
Methods |
|
Accessors |
constructor(route: ActivatedRoute, router: Router, licenceService: LicenceService, sasService: SasService, appService: AppService, sanitizer: DomSanitizer)
|
|||||||||||||||||||||
|
Defined in src/app/licensing/licensing.component.ts:78
|
|||||||||||||||||||||
|
Parameters :
|
| Public applyKeys |
applyKeys()
|
|
Defined in src/app/licensing/licensing.component.ts:215
|
|
Returns :
void
|
| Public copySyssite |
copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[])
|
|
Defined in src/app/licensing/licensing.component.ts:199
|
|
Returns :
void
|
| Private Async maybeSplitCombinedKey | ||||||
maybeSplitCombinedKey(value: string)
|
||||||
|
Defined in src/app/licensing/licensing.component.ts:188
|
||||||
|
Parameters :
Returns :
Promise<boolean>
|
| ngOnInit |
ngOnInit()
|
|
Defined in src/app/licensing/licensing.component.ts:104
|
|
Returns :
void
|
| Public Async onCombinedKeyInput |
onCombinedKeyInput()
|
|
Defined in src/app/licensing/licensing.component.ts:141
|
|
Returns :
any
|
| Public onFileCapture | ||||||||||||
onFileCapture(event: any, dropped: unknown)
|
||||||||||||
|
Defined in src/app/licensing/licensing.component.ts:245
|
||||||||||||
|
Parameters :
Returns :
void
|
| Private Async refreshKeyPreview |
refreshKeyPreview()
|
|
Defined in src/app/licensing/licensing.component.ts:165
|
|
Returns :
Promise<void>
|
| Public switchType | ||||||
switchType(type: "paste" | "file")
|
||||||
|
Defined in src/app/licensing/licensing.component.ts:284
|
||||||
|
Parameters :
Returns :
void
|
| Public toggleKeyFormat |
toggleKeyFormat()
|
|
Defined in src/app/licensing/licensing.component.ts:181
|
|
Returns :
void
|
| Public Async trimKeys |
trimKeys()
|
|
Defined in src/app/licensing/licensing.component.ts:130
|
|
Returns :
any
|
| Public action |
Type : LicenseActions | null
|
Default value : null
|
|
Defined in src/app/licensing/licensing.component.ts:32
|
| Public activationKeyValue |
Type : string
|
Default value : ''
|
|
Defined in src/app/licensing/licensing.component.ts:52
|
| Public applyingKeys |
Type : boolean
|
Default value : false
|
|
Defined in src/app/licensing/licensing.component.ts:60
|
| Public combinedKeyValue |
Type : string
|
Default value : ''
|
|
Defined in src/app/licensing/licensing.component.ts:58
|
| Public currentActivationKey |
Type : unknown
|
Default value : this.licenceService.activationKey
|
|
Defined in src/app/licensing/licensing.component.ts:68
|
| Public currentLicenceKey |
Type : unknown
|
Default value : this.licenceService.licenceKey
|
|
Defined in src/app/licensing/licensing.component.ts:67
|
| Public errorDetails |
Type : string | undefined
|
|
Defined in src/app/licensing/licensing.component.ts:49
|
| Private Readonly errorIcon |
Type : unknown
|
Default value : `<clr-icon class="is-error" shape="exclamation-circle" size="26"></clr-icon>`
|
|
Defined in src/app/licensing/licensing.component.ts:38
|
| Public inputType |
Type : "file" | "paste"
|
Default value : 'file'
|
|
Defined in src/app/licensing/licensing.component.ts:73
|
| Public isAppFreeTier |
Type : unknown
|
Default value : this.licenceService.isAppFreeTier
|
|
Defined in src/app/licensing/licensing.component.ts:69
|
| Public keyError |
Type : string | undefined
|
|
Defined in src/app/licensing/licensing.component.ts:48
|
| Public keyFormat |
Type : "combined" | "legacy"
|
Default value : 'combined'
|
|
Defined in src/app/licensing/licensing.component.ts:57
|
| Public licencefile |
Type : { filename: string }
|
Default value : {
filename: ''
}
|
|
Defined in src/app/licensing/licensing.component.ts:76
|
| Public licenceFileError |
Type : string | undefined
|
|
Defined in src/app/licensing/licensing.component.ts:74
|
| Public licenceFileLoading |
Type : boolean
|
Default value : false
|
|
Defined in src/app/licensing/licensing.component.ts:75
|
| Public licenceKeyValue |
Type : string
|
Default value : ''
|
|
Defined in src/app/licensing/licensing.component.ts:51
|
| Public licenseErrors |
Type : { }
|
|
Defined in src/app/licensing/licensing.component.ts:46
|
| Public licenseKeyData |
Type : LicenseKeyData | null
|
Default value : null
|
|
Defined in src/app/licensing/licensing.component.ts:71
|
| Public missmatchedKey |
Type : string | undefined
|
|
Defined in src/app/licensing/licensing.component.ts:50
|
| Public protocol |
Type : string
|
Default value : location.protocol === 'https:'
? 'HTTPS - secure connection'
: 'HTTP - insecure connection'
|
|
Defined in src/app/licensing/licensing.component.ts:61
|
| Public syssite |
Type : unknown
|
Default value : this.appService.syssite
|
|
Defined in src/app/licensing/licensing.component.ts:66
|
| disableApplyButton |
getdisableApplyButton()
|
|
Defined in src/app/licensing/licensing.component.ts:288
|
| protocolMismatch |
getprotocolMismatch()
|
|
Defined in src/app/licensing/licensing.component.ts:310
|
| enabledFeatures |
getenabledFeatures()
|
|
Defined in src/app/licensing/licensing.component.ts:318
|
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;
}