mirror of
https://github.com/beekeeper-studio/beekeeper-studio.git
synced 2026-03-13 10:12:54 +08:00
Merge pull request #2977 from beekeeper-studio/unique-id
Track installation ID for license checks
This commit is contained in:
@@ -36,4 +36,4 @@ export interface IPlatformInfo {
|
||||
parsedAppVersion: BksVersion,
|
||||
cloudUrl: string,
|
||||
locale: string,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
|
||||
30
apps/studio/src/common/appdb/models/installation_id.ts
Normal file
30
apps/studio/src/common/appdb/models/installation_id.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,7 @@ export class UserSetting extends ApplicationEntity {
|
||||
@Column({type: 'varchar'})
|
||||
windowsDefault?: string
|
||||
|
||||
|
||||
@Column({type: 'integer', nullable: false})
|
||||
valueType: UserSettingValueType = UserSettingValueType.string
|
||||
}
|
||||
|
||||
@@ -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 || "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
58
apps/studio/src/migration/20250404_add_installation_id.js
Normal file
58
apps/studio/src/migration/20250404_add_installation_id.js
Normal 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
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user