UNPKG

@cocalc/server

Version:

CoCalc server functionality: functions used by either the hub and the next.js server

132 lines (117 loc) 4.26 kB
/* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ import { getLogger } from "@cocalc/backend/logger"; import type { PostgreSQL } from "@cocalc/database/postgres/types"; import { PurchaseInfo } from "@cocalc/util/licenses/purchase/types"; import { adjustDateRangeEndOnSameDay } from "@cocalc/util/stripe/timecalcs"; import { getDedicatedDiskKey, PRICES } from "@cocalc/util/upgrades/dedicated"; import { v4 as uuid } from "uuid"; const logger = getLogger("createLicense"); // ATTN: for specific intervals, the activates/expires start/end dates should be at the start/end of the day in the user's timezone. // this is done while selecting the time interval – here, server side, we no longer know the user's time zone. export default async function createLicense( database: PostgreSQL, account_id: string, info: PurchaseInfo ): Promise<string> { const license_id = await getUUID(database, info); logger.debug("creating a license...", license_id, info); const [start, end] = info.type !== "disk" ? adjustDateRangeEndOnSameDay([info.start, info.end]) : [info.start, undefined]; const values: { [key: string]: any } = { "id::UUID": license_id, "info::JSONB": { purchased: { account_id, ...info }, }, "activates::TIMESTAMP": info.subscription != "no" ? new Date(new Date().valueOf() - 60000) // one minute in past to avoid any funny confusion. : start, "created::TIMESTAMP": new Date(), "managers::TEXT[]": [account_id], "quota::JSONB": await getQuota(info, license_id), "title::TEXT": info.title, "description::TEXT": info.description, "run_limit::INTEGER": info.quantity, }; if (info.type !== "disk" && end != null) { values["expires::TIMESTAMP"] = end; } await database.async_query({ query: "INSERT INTO site_licenses", values, }); return license_id; } // this constructs the "quota" object for the license, // while it also sanity checks all fields. Last chance to find a problem! async function getQuota(info: PurchaseInfo, license_id: string) { switch (info.type) { case "quota": return { user: info.user, ram: info.custom_ram, cpu: info.custom_cpu, dedicated_ram: info.custom_dedicated_ram, dedicated_cpu: info.custom_dedicated_cpu, disk: info.custom_disk, always_running: info.custom_uptime === "always_running", idle_timeout: info.custom_uptime, member: info.custom_member, boost: info.boost ?? false, }; case "vm": const { machine } = info.dedicated_vm; if (PRICES.vms[machine] == null) { throw new Error(`VM type ${machine} does not exist`); } return { dedicated_vm: { machine, name: uuid2name(license_id), }, }; case "disk": if (info.dedicated_disk === false) { throw new Error(`info.dedicated_disk cannot be false`); } const diskID = getDedicatedDiskKey(info.dedicated_disk); if (PRICES.disks[diskID] == null) { throw new Error(`Disk type ${diskID} does not exist`); } return { dedicated_disk: info.dedicated_disk, }; } } const VM_NAME_EXISTS = ` SELECT EXISTS( SELECT 1 FROM site_licenses WHERE quota -> 'dedicated_vm' ->> 'name' = $1 ) AS exists`; async function getUUID(database: PostgreSQL, info: PurchaseInfo) { // in the case of type == 'vm', we derive the "name" from the UUID // and double check that this is a unique name. if (info.type !== "vm") return uuid(); // we try up to 10 times for (let i = 0; i < 10; i++) { const id = uuid(); // use the last part of the UUID id after the last dash const name = uuid2name(id); const res = await database.async_query({ query: VM_NAME_EXISTS, params: [name], }); if (res.rows[0]?.exists === false) { return id; } } throw new Error(`Unable to generate a unique name for VM`); } // last part of the UUID, we show this to users even if they're not license managers. hence no leak of information. function uuid2name(id: string) { return id.split("-").pop(); }