Merge pull request #2977 from beekeeper-studio/unique-id

Track installation ID for license checks
This commit is contained in:
Matthew Rathbone
2025-04-05 15:25:30 -05:00
committed by GitHub
10 changed files with 181 additions and 13 deletions

View File

@@ -36,4 +36,4 @@ export interface IPlatformInfo {
parsedAppVersion: BksVersion,
cloudUrl: string,
locale: string,
};
}

View File

@@ -13,6 +13,7 @@ import { HiddenEntity } from "./models/HiddenEntity"
import { HiddenSchema } from "./models/HiddenSchema"
import { PinnedConnection } from "./models/PinnedConnection"
import { TokenCache } from "./models/token_cache"
import { InstallationId } from "./models/installation_id"
const models = [
SavedConnection,
@@ -27,7 +28,8 @@ const models = [
HiddenEntity,
HiddenSchema,
PinnedConnection,
TokenCache
TokenCache,
InstallationId
]

View File

@@ -0,0 +1,30 @@
import { Column, Entity } from "typeorm";
import { ApplicationEntity } from "./application_entity";
import rawLog from '@bksLogger'
const log = rawLog.scope('InstallationId')
@Entity({ name: 'installation_ids' })
export class InstallationId extends ApplicationEntity {
withProps(props: any) {
this.installationId = props.installationId
return this
}
@Column({ type: 'varchar', nullable: false })
installationId!: string;
static async get(): Promise<string | null> {
try {
const records = await InstallationId.find()
const record = records[0]
log.info("Found ID:", record)
return record?.installationId || null;
} catch (error) {
console.error('Error retrieving installation ID:', error);
return null;
}
}
}

View File

@@ -134,6 +134,7 @@ export class UserSetting extends ApplicationEntity {
@Column({type: 'varchar'})
windowsDefault?: string
@Column({type: 'integer', nullable: false})
valueType: UserSettingValueType = UserSettingValueType.string
}

View File

@@ -1,6 +1,7 @@
import { LicenseKey } from "@/common/appdb/models/LicenseKey";
import { TransportLicenseKey } from "@/common/transport";
import { LicenseStatus } from "@/lib/license";
import { InstallationId } from "@/common/appdb/models/installation_id";
export interface ILicenseHandlers {
"license/createTrialLicense": () => Promise<void>;
@@ -8,6 +9,7 @@ export interface ILicenseHandlers {
"license/get": () => Promise<TransportLicenseKey[]>;
"license/remove": (({ id }: { id: number }) => Promise<void>);
"license/wipe": () => Promise<void>;
"license/getInstallationId": () => Promise<string>;
}
export const LicenseHandlers: ILicenseHandlers = {
@@ -37,5 +39,10 @@ export const LicenseHandlers: ILicenseHandlers = {
},
"license/wipe": async function() {
await LicenseKey.wipe();
},
"license/getInstallationId": async function() {
// Make sure we return a string, not null
const id = await InstallationId.get();
return id || "";
}
};

View File

@@ -12,6 +12,7 @@ import { ConnectionFoldersController } from '@/lib/cloud/controllers/ConnectionF
import { QueryFoldersController } from '@/lib/cloud/controllers/QueryFoldersController';
import { UsedQueriesController } from '@/lib/cloud/controllers/UsedQueriesController';
import { LicenseKeyController } from './controllers/LicenseKeyController';
import { IPlatformInfo } from '@/common/IPlatformInfo';
const log = rawLog.scope('cloudClient')
@@ -67,9 +68,10 @@ export class CloudClient {
}
public static async getLicense(baseUrl: string, email: string, key: string) {
public static async getLicense(baseUrl: string, email: string, key: string, installationId = "", platformInfo: IPlatformInfo) {
const controller = new LicenseKeyController(staticAxios(baseUrl))
return await controller.get(email, key)
log.info("Fetching license info! Installation id", installationId)
return await controller.get(email, key, installationId, platformInfo)
}
axios: AxiosInstance

View File

@@ -1,9 +1,16 @@
import { ILicenseKey } from "@/common/interfaces/ILicenseKey";
import { AxiosInstance } from "axios";
import { res, url } from "../ClientHelpers";
import { IPlatformInfo } from "@/common/IPlatformInfo";
import rawLog from '@bksLogger'
const log = rawLog.scope('LicenseKeyController')
interface InstallationInfo {
osType: string;
architecture: string;
installationId: string;
}
export class LicenseKeyController {
constructor(protected axios: AxiosInstance) {
@@ -11,11 +18,40 @@ export class LicenseKeyController {
}
path = '/api/license_keys'
async get(email:string, key: string): Promise<ILicenseKey> {
async get(email: string, key: string, installationId = "", platformInfo: IPlatformInfo): Promise<ILicenseKey> {
const params = {
email
}
const response = await this.axios.get(url(this.path, key), { params })
return res(response, 'licenseKey')
log.info("license key get")
// Initialize headers object
const headers = {};
// Only include the installation ID header if we have a valid ID
if (installationId) {
// Create the installation info object with platform info and installation ID
const installationInfo: InstallationInfo = {
osType: platformInfo.platform,
architecture: platformInfo.isArm ? 'arm64' : 'amd64',
installationId: installationId
};
// Base64 encode the installation info for the header
const encodedInstallationInfo = Buffer.from(JSON.stringify(installationInfo)).toString('base64');
// Add the installation info header to the request
headers['X-Installation-Id'] = encodedInstallationInfo;
log.info("made headers")
}
log.info("making request for license", key, installationId, headers)
const response = await this.axios.get(url(this.path, key), {
params,
headers
});
return res(response, 'licenseKey');
}
}

View File

@@ -0,0 +1,58 @@
import { v4 as uuidv4 } from 'uuid';
export default {
name: '20250404_add_installation_id',
async run(runner) {
// Create a new table to store the installation ID
await runner.query(`
CREATE TABLE IF NOT EXISTS installation_ids (
id INTEGER PRIMARY KEY AUTOINCREMENT,
installationId TEXT NOT NULL,
createdAt DATETIME NOT NULL DEFAULT (datetime('now')),
"updatedAt" datetime NOT NULL DEFAULT (datetime('now')),
"version" integer NOT NULL DEFAULT 0
)
`);
// Generate a unique installation ID
const installationId = uuidv4();
// Insert the ID if the table is empty
const existingRecord = await runner.query("SELECT * FROM installation_ids LIMIT 1");
if (existingRecord.length === 0) {
await runner.query(
"INSERT INTO installation_ids (installationId) VALUES (?)",
[installationId]
);
}
// Create a trigger to prevent deletion
await runner.query(`
CREATE TRIGGER IF NOT EXISTS prevent_installation_id_deletion
BEFORE DELETE ON installation_ids
BEGIN
SELECT RAISE(ABORT, 'Cannot delete installation ID record');
END
`);
// Create a trigger to prevent updates
await runner.query(`
CREATE TRIGGER IF NOT EXISTS prevent_installation_id_update
BEFORE UPDATE ON installation_ids
BEGIN
SELECT RAISE(ABORT, 'Cannot update installation ID record');
END
`);
// Create a trigger to prevent additional inserts beyond the first one
await runner.query(`
CREATE TRIGGER IF NOT EXISTS prevent_installation_id_insert
BEFORE INSERT ON installation_ids
WHEN (SELECT COUNT(*) FROM installation_ids) > 0
BEGIN
SELECT RAISE(ABORT, 'Installation ID already exists');
END
`);
}
}

View File

@@ -53,6 +53,7 @@ import deleteDuplicateConnections from './20241115_delete_duplicate_connections'
import addNewUrlField from './20250128_add_new_url_field'
import tabHistoryIndex from './20250211_add_tab_history_index'
import fixOracleData from './20250225_oracle_default_connection_method'
import addInstallationId from './20250404_add_installation_id'
import ultimate from './ultimate/index'
@@ -85,6 +86,7 @@ const realMigrations = [
addNewUrlField,
tabHistoryIndex,
fixOracleData,
addInstallationId,
]
// fixtures require the models

View File

@@ -1,5 +1,5 @@
import _ from 'lodash'
import { Module } from "vuex";
import { install, Module } from "vuex";
import rawLog from '@bksLogger'
import { State as RootState } from '../index'
import { CloudError } from '@/lib/cloud/ClientHelpers';
@@ -15,7 +15,8 @@ interface State {
licenses: TransportLicenseKey[]
error: CloudError | Error | null
now: Date
status: LicenseStatus
status: LicenseStatus,
installationId: string | null
}
const log = rawLog.scope('LicenseModule')
@@ -36,6 +37,7 @@ export const LicenseModule: Module<State, RootState> = {
error: null,
now: new Date(),
status: defaultStatus,
installationId: null
}),
getters: {
trialLicense(state) {
@@ -77,6 +79,9 @@ export const LicenseModule: Module<State, RootState> = {
setInitialized(state, b: boolean) {
state.initialized = b
},
installationId(state, id: string) {
state.installationId = id
},
setNow(state, date: Date) {
state.now = date
},
@@ -91,6 +96,10 @@ export const LicenseModule: Module<State, RootState> = {
return
}
await context.dispatch('sync')
const installationId = await Vue.prototype.$util.send('license/getInstallationId');
context.commit('installationId', installationId)
if (!window.platformInfo.isDevelopment) {
// refreshing in dev mode resets the dev credentials added by the menu
setInterval(() => context.dispatch('sync'), globals.licenseCheckInterval)
@@ -104,9 +113,19 @@ export const LicenseModule: Module<State, RootState> = {
await Vue.prototype.$util.send('license/createTrialLicense')
await Vue.prototype.$noty.info("Your 14 day free trial has started, enjoy!")
} else {
const result = await CloudClient.getLicense(window.platformInfo.cloudUrl, email, key);
// Get the installation ID from the backend
const installationId = context.state.installationId
const result = await CloudClient.getLicense(
window.platformInfo.cloudUrl,
email,
key,
installationId,
window.platformInfo
);
// if we got here, license is good.
let license = {} as TransportLicenseKey;
const license = {} as TransportLicenseKey;
license.key = key;
license.email = email;
license.validUntil = new Date(result.validUntil);
@@ -123,7 +142,17 @@ export const LicenseModule: Module<State, RootState> = {
// This is to allow for dev switching
const isDevUpdate = window.platformInfo.isDevelopment && license.email == "fake_email";
try {
const data = isDevUpdate ? license : await CloudClient.getLicense(window.platformInfo.cloudUrl, license.email, license.key)
// Get the installation ID
const installationId = _context.state.installationId
const data = isDevUpdate ? license : await CloudClient.getLicense(
window.platformInfo.cloudUrl,
license.email,
license.key,
installationId,
window.platformInfo
);
license.validUntil = new Date(data.validUntil)
license.supportUntil = new Date(data.supportUntil)
license.maxAllowedAppRelease = data.maxAllowedAppRelease
@@ -134,6 +163,7 @@ export const LicenseModule: Module<State, RootState> = {
license.validUntil = new Date()
await Vue.prototype.$util.send('appdb/license/save', { obj: license });
} else {
log.error("Problems getting license", error)
// eg 500 errors
// do nothing
}