diff --git a/src/api/api.ts b/src/api/api.ts index 88ca611..42f1e8b 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,6 +1,6 @@ /* * TimeLimit Server Admin UI - * Copyright (C) 2020 Jonas Lochmann + * Copyright (C) 2020 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -22,6 +22,10 @@ export interface Api { mail: string duration: 'month' | 'year' }): Promise + unlockPremium2 (params: { + purchaseId: string + purchaseToken: string + }): Promise getStatusMessage (): Promise setStatusMessage (newMessage: string): Promise } diff --git a/src/api/dummyapi.ts b/src/api/dummyapi.ts index 17fe716..0ab2c83 100644 --- a/src/api/dummyapi.ts +++ b/src/api/dummyapi.ts @@ -55,6 +55,16 @@ class DummyApiImpl implements Api { await delay(100 + Math.random() * 400) } + async unlockPremium2 () { + await this.checkAuth() + await delay(100 + Math.random() * 400) + + return { + ok: false, + detail: 'dummy' + } + } + async getStatusMessage () { await this.checkAuth() await delay(100 + Math.random() * 400) diff --git a/src/api/httpapi.ts b/src/api/httpapi.ts index b988fae..ec80aa3 100644 --- a/src/api/httpapi.ts +++ b/src/api/httpapi.ts @@ -1,6 +1,6 @@ /* * TimeLimit Server Admin UI - * Copyright (C) 2020 Jonas Lochmann + * Copyright (C) 2020 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -110,6 +110,33 @@ class HttpApiImpl implements Api { await response.json() } + async unlockPremium2 ({ purchaseId, purchaseToken }: { + purchaseId: string + purchaseToken: string + }): Promise { + const response = await fetch(this.serverUrl + '/unlock-premium-v2', { + method: 'POST', + body: JSON.stringify({ purchaseId, purchaseToken }), + mode: 'cors', + cache: 'no-cache', + referrerPolicy: 'no-referrer', + headers: { + Authorization: 'Basic ' + btoa(':' + this.token), + 'Content-Type': 'application/json' + } + }) + + if (response.status === 401) { + throw new IllegalAccessDataError() + } + + if (!response.ok) { + throw new Error('unexpected response - ' + response.status) + } + + return await response.json() + } + async getStatusMessage (): Promise { const response = await fetch(this.serverUrl + '/status-message', { mode: 'cors', diff --git a/src/controller/app.ts b/src/controller/app.ts index ca488ec..c40cc59 100644 --- a/src/controller/app.ts +++ b/src/controller/app.ts @@ -1,6 +1,6 @@ /* * TimeLimit Server Admin UI - * Copyright (C) 2020 Jonas Lochmann + * Copyright (C) 2020 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -124,6 +124,32 @@ export const initAppController = ({ target, apiCreator }: { } } + view.unlockPremium2.listener = { + onUnlockPremiumRequested: async ({ token, id }) => { + view.unlockPremium2.isEnabled = false + + try { + const response = await api.unlockPremium2({ + purchaseId: id, + purchaseToken: token + }) + + view.unlockPremiumLog2.addMessage( + id + ': ' + JSON.stringify(response, null, 2), + 1000 * 60 * 5 + ) + } catch (ex) { + if (ex instanceof IllegalAccessDataError) { + showAccessDataWrong() + } else { + view.unlockPremiumLog2.addMessage('could not unlock', view.unlockPremiumLog.short) + } + } finally { + view.unlockPremium2.isEnabled = true + } + } + } + // this catches exceptions by itself // tslint:disable-next-line:no-floating-promises ;(async () => { diff --git a/src/view/detailscreen.ts b/src/view/detailscreen.ts index ca53cb9..696db14 100644 --- a/src/view/detailscreen.ts +++ b/src/view/detailscreen.ts @@ -20,11 +20,14 @@ import { LogView } from './logview' import { StatusView } from './status' import { StatusMessageForm } from './statusmessage' import { UnlockPremiumForm } from './unlockpremiumform' +import { UnlockPremiumForm2 } from './unlockpremiumform2' export class DetailScreen { status = new StatusView() unlockPremium = new UnlockPremiumForm() unlockPremiumLog = new LogView() + unlockPremium2 = new UnlockPremiumForm2() + unlockPremiumLog2 = new LogView() statusMessage = new StatusMessageForm() statusMessageLog = new LogView() root = document.createElement('div') @@ -36,6 +39,10 @@ export class DetailScreen { this.unlockPremium.root, this.unlockPremiumLog.root ), + wrapInBorder( + this.unlockPremium2.root, + this.unlockPremiumLog2.root + ), wrapInBorder( this.statusMessage.root, this.statusMessageLog.root diff --git a/src/view/logview.ts b/src/view/logview.ts index d5ca2fa..37832a2 100644 --- a/src/view/logview.ts +++ b/src/view/logview.ts @@ -1,6 +1,6 @@ /* * TimeLimit Server Admin UI - * Copyright (C) 2020 Jonas Lochmann + * Copyright (C) 2020 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -33,7 +33,7 @@ export class LogView { view.innerText = message - this.root.appendChild(view) + this.root.prepend(view) if (duration) { setTimeout(() => { diff --git a/src/view/unlockpremiumform2.ts b/src/view/unlockpremiumform2.ts new file mode 100644 index 0000000..5634fe0 --- /dev/null +++ b/src/view/unlockpremiumform2.ts @@ -0,0 +1,101 @@ +/* + * TimeLimit Server Admin UI + * Copyright (C) 2020 - 2022 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { createHeader } from './header' +import { RadioButton } from './radio' + +function getPurchaseIdPlaceholder(): string { + const now = new Date() + + const day = now.getDate().toString().padStart(2, '0') + const month = (now.getMonth() + 1).toString().padStart(2, '0') + const year = now.getFullYear().toString() + + return [year, month, day, 'XYZ'].join('-') +} + +export class UnlockPremiumForm2 { + listener?: UnlockPremiumFormListener = undefined + readonly root: HTMLElement + private readonly form: HTMLFormElement + private readonly purchaseIdField: HTMLInputElement + private readonly purchaseTokenField: HTMLTextAreaElement + private readonly submitButton: HTMLInputElement + private enabled: boolean = true + + constructor () { + this.form = document.createElement('form') + this.purchaseIdField = document.createElement('input') + this.purchaseTokenField = document.createElement('textarea') + this.submitButton = document.createElement('input') + + this.purchaseIdField.placeholder = getPurchaseIdPlaceholder() + this.purchaseIdField.value = getPurchaseIdPlaceholder() + + this.purchaseTokenField.rows = 10 + + this.submitButton.type = 'submit' + this.submitButton.value = 'Unlock' + + this.reset() + + this.form.addEventListener('submit', (e) => { + e.preventDefault() + + const id = this.purchaseIdField.value + const token = this.purchaseTokenField.value + + if (id.endsWith('XYZ')) return alert('placeholder token used') + + this.listener?.onUnlockPremiumRequested({ token, id }) + }) + + this.form.append( + createHeader('Unlock premium version (new)'), + this.purchaseIdField, + this.purchaseTokenField, + this.submitButton + ) + + this.root = this.form + } + + reset () { + this.purchaseIdField.value = getPurchaseIdPlaceholder() + this.purchaseTokenField.value = '' + this.isEnabled = true + } + + set isEnabled (value: boolean) { + if (value === this.enabled) { + return + } + + this.enabled = value + + this.purchaseIdField.disabled = !value + this.purchaseTokenField.isEnabled = value + this.submitButton.disabled = !value + } +} + +interface UnlockPremiumFormListener { + onUnlockPremiumRequested (params: { + token: string + id: string + }): void +}