This commit is contained in:
Rory Powell 2023-07-14 16:55:48 +01:00
parent 1964148581
commit c8fc67d230
18 changed files with 88 additions and 55 deletions

View File

@ -29,7 +29,7 @@ export const account = (partial: Partial<Account> = {}): Account => {
size: "10+", size: "10+",
profession: "Software Engineer", profession: "Software Engineer",
quotaUsage: quotas.usage(), quotaUsage: quotas.usage(),
...partial ...partial,
} }
} }

View File

@ -7,6 +7,6 @@ export function install(): Installation {
_id: "install", _id: "install",
_rev: db.rev(), _rev: db.rev(),
installId: generator.guid(), installId: generator.guid(),
version: generator.string() version: generator.string(),
} }
} }

View File

@ -138,9 +138,7 @@ interface GenerateLicenseOpts {
billing?: Billing billing?: Billing
} }
export const license = ( export const license = (opts: GenerateLicenseOpts = {}): License => {
opts: GenerateLicenseOpts = {}
): License => {
return { return {
features: opts.features || [], features: opts.features || [],
quotas: opts.quotas || quotas(), quotas: opts.quotas || quotas(),
@ -149,23 +147,21 @@ export const license = (
} }
} }
export function offlineLicense ( export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense {
opts: GenerateLicenseOpts = {}
): OfflineLicense {
const base = license(opts) const base = license(opts)
return { return {
...base, ...base,
expireAt: new Date().toISOString(), expireAt: new Date().toISOString(),
identifier: offlineIdentifier() identifier: offlineIdentifier(),
} }
} }
export function offlineIdentifier( export function offlineIdentifier(
installId: string = generator.guid(), installId: string = generator.guid(),
tenantId: string = generator.guid(), tenantId: string = generator.guid()
): OfflineIdentifier { ): OfflineIdentifier {
return { return {
installId, installId,
tenantId tenantId,
} }
} }

View File

@ -11,7 +11,7 @@
ButtonGroup, ButtonGroup,
notifications, notifications,
CopyInput, CopyInput,
File File,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth, admin } from "stores/portal" import { auth, admin } from "stores/portal"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
@ -35,9 +35,7 @@
let offlineLicenseIdentifier = "" let offlineLicenseIdentifier = ""
let offlineLicense = undefined let offlineLicense = undefined
const offlineLicenseExtensions = [ const offlineLicenseExtensions = [".txt"]
".txt",
]
// Make sure page can't be visited directly in cloud // Make sure page can't be visited directly in cloud
$: { $: {
@ -97,7 +95,7 @@
const license = await API.getOfflineLicense() const license = await API.getOfflineLicense()
if (license) { if (license) {
offlineLicense = { offlineLicense = {
name: "license" name: "license",
} }
} else { } else {
offlineLicense = undefined offlineLicense = undefined
@ -147,7 +145,7 @@
// prevent file preview jitter by assigning constant // prevent file preview jitter by assigning constant
// as soon as possible // as soon as possible
offlineLicense = { offlineLicense = {
name: "license" name: "license",
} }
const reader = new FileReader() const reader = new FileReader()
reader.readAsText(event.detail) reader.readAsText(event.detail)
@ -202,7 +200,9 @@
{#if $admin.offlineMode} {#if $admin.offlineMode}
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="XS">Installation identifier</Heading> <Heading size="XS">Installation identifier</Heading>
<Body size="S">Share this with support@budibase.com to obtain your offline license</Body> <Body size="S"
>Share this with support@budibase.com to obtain your offline license</Body
>
</Layout> </Layout>
<Layout noPadding> <Layout noPadding>
<div class="fields"> <div class="fields">
@ -263,8 +263,12 @@
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<Body size="S">You are currently on the {license.plan.type} plan</Body> <Body size="S">You are currently on the {license.plan.type} plan</Body>
<div> <div>
<Body size="S">If you purchase or update your plan on the account</Body> <Body size="S"
<Body size="S">portal, click the refresh button to sync those changes</Body> >If you purchase or update your plan on the account</Body
>
<Body size="S"
>portal, click the refresh button to sync those changes</Body
>
</div> </div>
<Body size="XS"> <Body size="XS">
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { {processStringSync("Updated {{ duration time 'millisecond' }} ago", {

View File

@ -17,7 +17,7 @@ export const DEFAULT_CONFIG = {
adminUser: { checked: false }, adminUser: { checked: false },
sso: { checked: false }, sso: { checked: false },
}, },
offlineMode: false offlineMode: false,
} }
export function createAdminStore() { export function createAdminStore() {

View File

@ -1,5 +1,4 @@
export const buildLicensingEndpoints = API => ({ export const buildLicensingEndpoints = API => ({
// LICENSE KEY // LICENSE KEY
activateLicenseKey: async data => { activateLicenseKey: async data => {
@ -31,7 +30,7 @@ export const buildLicensingEndpoints = API => ({
return API.post({ return API.post({
url: "/api/global/license/offline", url: "/api/global/license/offline",
body: { body: {
offlineLicenseToken offlineLicenseToken,
}, },
}) })
}, },

@ -1 +1 @@
Subproject commit 1a5207d91fb9e0835562c708dd9c421973026543 Subproject commit b5124e76b9fa8020641e8d019ac1713c6245d6e6

View File

@ -5,7 +5,7 @@ export interface ActivateLicenseKeyRequest {
} }
export interface GetLicenseKeyResponse { export interface GetLicenseKeyResponse {
licenseKey: string, licenseKey: string
} }
// OFFLINE LICENSE // OFFLINE LICENSE

View File

@ -2,7 +2,7 @@ import { PurchasedPlan, Quotas, Feature, Billing } from "."
import { ISO8601 } from "../../shared" import { ISO8601 } from "../../shared"
export interface OfflineIdentifier { export interface OfflineIdentifier {
installId: string, installId: string
tenantId: string tenantId: string
} }

View File

@ -11,16 +11,16 @@ const pro = {
activateOfflineLicense: jest.fn(), activateOfflineLicense: jest.fn(),
getOfflineLicenseToken: jest.fn(), getOfflineLicenseToken: jest.fn(),
deleteOfflineLicenseToken: jest.fn(), deleteOfflineLicenseToken: jest.fn(),
getIdentifierBase64: jest.fn() getIdentifierBase64: jest.fn(),
}, },
cache: { cache: {
...actual.licensing.cache, ...actual.licensing.cache,
refresh: jest.fn(), refresh: jest.fn(),
} },
}, },
quotas: { quotas: {
...actual.quotas, ...actual.quotas,
getQuotaUsage: jest.fn() getQuotaUsage: jest.fn(),
}, },
} }

View File

@ -10,7 +10,9 @@ import {
// LICENSE KEY // LICENSE KEY
export async function activateLicenseKey(ctx: UserCtx<ActivateLicenseKeyRequest>) { export async function activateLicenseKey(
ctx: UserCtx<ActivateLicenseKeyRequest>
) {
const { licenseKey } = ctx.request.body const { licenseKey } = ctx.request.body
await licensing.keys.activateLicenseKey(licenseKey) await licensing.keys.activateLicenseKey(licenseKey)
ctx.status = 200 ctx.status = 200
@ -33,13 +35,17 @@ export async function deleteLicenseKey(ctx: UserCtx<void, void>) {
// OFFLINE LICENSE // OFFLINE LICENSE
export async function activateOfflineLicenseToken(ctx: UserCtx<ActivateOfflineLicenseTokenRequest>) { export async function activateOfflineLicenseToken(
ctx: UserCtx<ActivateOfflineLicenseTokenRequest>
) {
const { offlineLicenseToken } = ctx.request.body const { offlineLicenseToken } = ctx.request.body
await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken) await licensing.offline.activateOfflineLicenseToken(offlineLicenseToken)
ctx.status = 200 ctx.status = 200
} }
export async function getOfflineLicenseToken(ctx: UserCtx<void, GetOfflineLicenseTokenResponse>) { export async function getOfflineLicenseToken(
ctx: UserCtx<void, GetOfflineLicenseTokenResponse>
) {
const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken() const offlineLicenseToken = await licensing.offline.getOfflineLicenseToken()
if (offlineLicenseToken) { if (offlineLicenseToken) {
ctx.body = { offlineLicenseToken: "*" } ctx.body = { offlineLicenseToken: "*" }
@ -54,7 +60,9 @@ export async function deleteOfflineLicenseToken(ctx: UserCtx<void, void>) {
ctx.status = 204 ctx.status = 204
} }
export async function getOfflineLicenseIdentifier(ctx: UserCtx<void, GetOfflineIdentifierResponse>) { export async function getOfflineLicenseIdentifier(
ctx: UserCtx<void, GetOfflineIdentifierResponse>
) {
const identifierBase64 = await licensing.offline.getIdentifierBase64() const identifierBase64 = await licensing.offline.getIdentifierBase64()
ctx.body = { identifierBase64 } ctx.body = { identifierBase64 }
ctx.status = 200 ctx.status = 200

View File

@ -3,13 +3,17 @@ import * as controller from "../../controllers/global/license"
import { middleware } from "@budibase/backend-core" import { middleware } from "@budibase/backend-core"
import Joi from "joi" import Joi from "joi"
const activateLicenseKeyValidator = middleware.joiValidator.body(Joi.object({ const activateLicenseKeyValidator = middleware.joiValidator.body(
Joi.object({
licenseKey: Joi.string().required(), licenseKey: Joi.string().required(),
}).required()) }).required()
)
const activateOfflineLicenseValidator = middleware.joiValidator.body(Joi.object({ const activateOfflineLicenseValidator = middleware.joiValidator.body(
Joi.object({
offlineLicenseToken: Joi.string().required(), offlineLicenseToken: Joi.string().required(),
}).required()) }).required()
)
const router: Router = new Router() const router: Router = new Router()
@ -17,13 +21,24 @@ router
.post("/api/global/license/refresh", controller.refresh) .post("/api/global/license/refresh", controller.refresh)
.get("/api/global/license/usage", controller.getQuotaUsage) .get("/api/global/license/usage", controller.getQuotaUsage)
// LICENSE KEY // LICENSE KEY
.post("/api/global/license/key", activateLicenseKeyValidator, controller.activateLicenseKey) .post(
"/api/global/license/key",
activateLicenseKeyValidator,
controller.activateLicenseKey
)
.get("/api/global/license/key", controller.getLicenseKey) .get("/api/global/license/key", controller.getLicenseKey)
.delete("/api/global/license/key", controller.deleteLicenseKey) .delete("/api/global/license/key", controller.deleteLicenseKey)
// OFFLINE LICENSE // OFFLINE LICENSE
.post("/api/global/license/offline", activateOfflineLicenseValidator, controller.activateOfflineLicenseToken) .post(
"/api/global/license/offline",
activateOfflineLicenseValidator,
controller.activateOfflineLicenseToken
)
.get("/api/global/license/offline", controller.getOfflineLicenseToken) .get("/api/global/license/offline", controller.getOfflineLicenseToken)
.delete("/api/global/license/offline", controller.deleteOfflineLicenseToken) .delete("/api/global/license/offline", controller.deleteOfflineLicenseToken)
.get("/api/global/license/offline/identifier", controller.getOfflineLicenseIdentifier) .get(
"/api/global/license/offline/identifier",
controller.getOfflineLicenseIdentifier
)
export default router export default router

View File

@ -37,7 +37,9 @@ describe("/api/global/license", () => {
describe("POST /api/global/license/key", () => { describe("POST /api/global/license/key", () => {
it("returns 200", async () => { it("returns 200", async () => {
const res = await config.api.license.activateLicenseKey({ licenseKey: "licenseKey" }) const res = await config.api.license.activateLicenseKey({
licenseKey: "licenseKey",
})
expect(res.status).toBe(200) expect(res.status).toBe(200)
expect(licensing.keys.activateLicenseKey).toBeCalledWith("licenseKey") expect(licensing.keys.activateLicenseKey).toBeCalledWith("licenseKey")
}) })
@ -53,7 +55,7 @@ describe("/api/global/license", () => {
const res = await config.api.license.getLicenseKey() const res = await config.api.license.getLicenseKey()
expect(res.status).toBe(200) expect(res.status).toBe(200)
expect(res.body).toEqual({ expect(res.body).toEqual({
licenseKey: "*" licenseKey: "*",
}) })
}) })
}) })
@ -68,8 +70,12 @@ describe("/api/global/license", () => {
describe("POST /api/global/license/offline", () => { describe("POST /api/global/license/offline", () => {
it("activates offline license", async () => { it("activates offline license", async () => {
const res = await config.api.license.activateOfflineLicense({ offlineLicenseToken: "offlineLicenseToken"}) const res = await config.api.license.activateOfflineLicense({
expect(licensing.offline.activateOfflineLicense).toBeCalledWith("offlineLicenseToken") offlineLicenseToken: "offlineLicenseToken",
})
expect(licensing.offline.activateOfflineLicenseToken).toBeCalledWith(
"offlineLicenseToken"
)
expect(res.status).toBe(200) expect(res.status).toBe(200)
}) })
}) })
@ -80,11 +86,13 @@ describe("/api/global/license", () => {
expect(res.status).toBe(404) expect(res.status).toBe(404)
}) })
it("returns 200 + offline license token", async () => { it("returns 200 + offline license token", async () => {
licensing.offline.getOfflineLicenseToken.mockResolvedValue("offlineLicenseToken") licensing.offline.getOfflineLicenseToken.mockResolvedValue(
"offlineLicenseToken"
)
const res = await config.api.license.getOfflineLicense() const res = await config.api.license.getOfflineLicense()
expect(res.status).toBe(200) expect(res.status).toBe(200)
expect(res.body).toEqual({ expect(res.body).toEqual({
offlineLicenseToken: "*" offlineLicenseToken: "*",
}) })
}) })
}) })
@ -103,7 +111,7 @@ describe("/api/global/license", () => {
const res = await config.api.license.getOfflineLicenseIdentifier() const res = await config.api.license.getOfflineLicenseIdentifier()
expect(res.status).toBe(200) expect(res.status).toBe(200)
expect(res.body).toEqual({ expect(res.body).toEqual({
identifierBase64: "base64" identifierBase64: "base64",
}) })
}) })
}) })

View File

@ -1,6 +1,9 @@
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
import { ActivateLicenseKeyRequest, ActivateOfflineLicenseRequest } from "@budibase/types" import {
ActivateLicenseKeyRequest,
ActivateOfflineLicenseTokenRequest,
} from "@budibase/types"
export class LicenseAPI extends TestAPI { export class LicenseAPI extends TestAPI {
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
@ -35,7 +38,7 @@ export class LicenseAPI extends TestAPI {
.delete("/api/global/license/key") .delete("/api/global/license/key")
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
} }
activateOfflineLicense = async (body: ActivateOfflineLicenseRequest) => { activateOfflineLicense = async (body: ActivateOfflineLicenseTokenRequest) => {
return this.request return this.request
.post("/api/global/license/offline") .post("/api/global/license/offline")
.send(body) .send(body)