mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-18 10:12:55 +02:00
463 lines
22 KiB
TypeScript
463 lines
22 KiB
TypeScript
import { APP_PATH } from "@server/lib/consts";
|
|
import Database from "better-sqlite3";
|
|
import fs from "fs";
|
|
import yaml from "js-yaml";
|
|
import path from "path";
|
|
import z from "zod";
|
|
import { fromZodError } from "zod-validation-error";
|
|
|
|
const version = "1.22.0";
|
|
|
|
const actionsToGrant = ["getSiteResource", "listSiteResources"] as const;
|
|
|
|
export default async function migration() {
|
|
console.log(`Running setup script ${version}...`);
|
|
|
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
|
const db = new Database(location);
|
|
|
|
try {
|
|
db.pragma("foreign_keys = OFF");
|
|
|
|
db.transaction(() => {
|
|
db.prepare(
|
|
`ALTER TABLE 'sites' RENAME COLUMN "subnet" TO "exitNodeSubnet";`
|
|
).run();
|
|
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'aiBudgetBreachEvents' (
|
|
'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'orgId' text NOT NULL,
|
|
'budgetId' integer NOT NULL,
|
|
'enforcement' text NOT NULL,
|
|
'unit' text NOT NULL,
|
|
'period' text NOT NULL,
|
|
'amount' real NOT NULL,
|
|
'usageAmount' real NOT NULL,
|
|
'blocked' integer NOT NULL,
|
|
'requestUserId' text,
|
|
'createdAt' integer NOT NULL,
|
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('budgetId') REFERENCES 'aiBudgets'('budgetId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('requestUserId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE set null
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE INDEX 'idx_ai_budget_breach_events_budget_created' ON 'aiBudgetBreachEvents' ('budgetId','createdAt');
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'aiBudgets' (
|
|
'budgetId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'orgId' text NOT NULL,
|
|
'providerId' integer,
|
|
'modelId' integer,
|
|
'resourceId' integer,
|
|
'siteResourceId' integer,
|
|
'roleId' integer,
|
|
'virtualApiKeyId' text,
|
|
'amount' real NOT NULL,
|
|
'unit' text NOT NULL,
|
|
'period' text DEFAULT 'monthly' NOT NULL,
|
|
'enforcement' text DEFAULT 'hard' NOT NULL,
|
|
'enabled' integer DEFAULT true NOT NULL,
|
|
'createdAt' integer NOT NULL,
|
|
'updatedAt' integer NOT NULL,
|
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('modelId') REFERENCES 'aiModels'('modelId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('virtualApiKeyId') REFERENCES 'virtualApiKeys'('virtualApiKeyId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
|
|
db.prepare(
|
|
`CREATE UNIQUE INDEX 'ai_budget_provider_uniq' ON 'aiBudgets' ('providerId','unit','period');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE UNIQUE INDEX 'ai_budget_model_uniq' ON 'aiBudgets' ('modelId','unit','period');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE UNIQUE INDEX 'ai_budget_resource_uniq' ON 'aiBudgets' ('resourceId','unit','period');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE UNIQUE INDEX 'ai_budget_site_resource_uniq' ON 'aiBudgets' ('siteResourceId','unit','period');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE UNIQUE INDEX 'ai_budget_role_uniq' ON 'aiBudgets' ('roleId','unit','period');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE UNIQUE INDEX 'ai_budget_virtual_api_key_uniq' ON 'aiBudgets' ('virtualApiKeyId','unit','period');`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'aiModels' (
|
|
'modelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'providerId' integer NOT NULL,
|
|
'modelKey' text NOT NULL,
|
|
'name' text NOT NULL,
|
|
'listType' text DEFAULT 'allow' NOT NULL,
|
|
'enabled' integer DEFAULT true NOT NULL,
|
|
'createdAt' integer NOT NULL,
|
|
'updatedAt' integer NOT NULL,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE UNIQUE INDEX 'ai_model_provider_key_uniq' ON 'aiModels' ('providerId','modelKey');
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'aiProviders' (
|
|
'providerId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'orgId' text NOT NULL,
|
|
'name' text NOT NULL,
|
|
'niceId' text NOT NULL,
|
|
'type' text NOT NULL,
|
|
'upstreamUrl' text,
|
|
'apiKey' text,
|
|
'apiKeyLastChars' text,
|
|
'authType' text NOT NULL,
|
|
'routingMode' text DEFAULT 'url' NOT NULL,
|
|
'capabilities' text DEFAULT '[]' NOT NULL,
|
|
'headers' text,
|
|
'skipTlsVerification' integer DEFAULT false NOT NULL,
|
|
'enabled' integer DEFAULT true NOT NULL,
|
|
'createdAt' integer NOT NULL,
|
|
'updatedAt' integer NOT NULL,
|
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE INDEX 'idx_aiProviders_orgId_niceId' ON 'aiProviders' ('orgId','niceId');
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'aiSessionLog' (
|
|
'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'sessionId' text NOT NULL,
|
|
'orgId' text,
|
|
'providerId' integer,
|
|
'capability' text NOT NULL,
|
|
'resourceId' integer,
|
|
'siteResourceId' integer,
|
|
'userId' text,
|
|
'virtualApiKeyId' text,
|
|
'requestedModel' text,
|
|
'isStream' integer DEFAULT false NOT NULL,
|
|
'requestBody' text,
|
|
'responseBody' text,
|
|
'normalizedRequest' text,
|
|
'normalizedResponse' text,
|
|
'truncated' integer DEFAULT false NOT NULL,
|
|
'statusCode' integer,
|
|
'createdAt' integer NOT NULL,
|
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('virtualApiKeyId') REFERENCES 'virtualApiKeys'('virtualApiKeyId') ON UPDATE no action ON DELETE set null
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_org_created' ON 'aiSessionLog' ('orgId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_org_provider_created' ON 'aiSessionLog' ('orgId','providerId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_org_resource_created' ON 'aiSessionLog' ('orgId','resourceId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_org_site_resource_created' ON 'aiSessionLog' ('orgId','siteResourceId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_org_user_created' ON 'aiSessionLog' ('orgId','userId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_org_virtual_api_key_created' ON 'aiSessionLog' ('orgId','virtualApiKeyId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_session_log_session' ON 'aiSessionLog' ('sessionId');`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'aiUsageRecords' (
|
|
'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'orgId' text NOT NULL,
|
|
'providerId' integer,
|
|
'resourceId' integer,
|
|
'siteResourceId' integer,
|
|
'userId' text,
|
|
'virtualApiKeyId' text,
|
|
'sessionId' text,
|
|
'requestedModel' text NOT NULL,
|
|
'promptTokens' integer DEFAULT 0 NOT NULL,
|
|
'cacheReadTokens' integer DEFAULT 0 NOT NULL,
|
|
'cacheWriteTokens' integer DEFAULT 0 NOT NULL,
|
|
'completionTokens' integer DEFAULT 0 NOT NULL,
|
|
'reasoningTokens' integer DEFAULT 0 NOT NULL,
|
|
'totalTokens' integer DEFAULT 0 NOT NULL,
|
|
'costUsd' real,
|
|
'estimated' integer DEFAULT false NOT NULL,
|
|
'createdAt' integer NOT NULL,
|
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE set null,
|
|
FOREIGN KEY ('virtualApiKeyId') REFERENCES 'virtualApiKeys'('virtualApiKeyId') ON UPDATE no action ON DELETE set null
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_usage_records_org_provider_created' ON 'aiUsageRecords' ('orgId','providerId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_usage_records_org_resource_created' ON 'aiUsageRecords' ('orgId','resourceId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_usage_records_org_site_resource_created' ON 'aiUsageRecords' ('orgId','siteResourceId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_usage_records_org_user_created' ON 'aiUsageRecords' ('orgId','userId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_usage_records_org_virtual_api_key_created' ON 'aiUsageRecords' ('orgId','virtualApiKeyId','createdAt');`
|
|
).run();
|
|
db.prepare(
|
|
`CREATE INDEX 'idx_ai_usage_records_session' ON 'aiUsageRecords' ('sessionId');`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'resourceAiModels' (
|
|
'resourceId' integer NOT NULL,
|
|
'modelId' integer NOT NULL,
|
|
'listType' text DEFAULT 'allow' NOT NULL,
|
|
PRIMARY KEY('resourceId', 'modelId'),
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('modelId') REFERENCES 'aiModels'('modelId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'resourceAiProviders' (
|
|
'resourceId' integer NOT NULL,
|
|
'providerId' integer NOT NULL,
|
|
'accessMode' text DEFAULT 'inherit' NOT NULL,
|
|
'enabled' integer DEFAULT true NOT NULL,
|
|
PRIMARY KEY('resourceId', 'providerId'),
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'siteResourceAiModels' (
|
|
'siteResourceId' integer NOT NULL,
|
|
'modelId' integer NOT NULL,
|
|
'listType' text DEFAULT 'allow' NOT NULL,
|
|
PRIMARY KEY('siteResourceId', 'modelId'),
|
|
FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('modelId') REFERENCES 'aiModels'('modelId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'siteResourceAiProviders' (
|
|
'siteResourceId' integer NOT NULL,
|
|
'providerId' integer NOT NULL,
|
|
'accessMode' text DEFAULT 'inherit' NOT NULL,
|
|
'enabled' integer DEFAULT true NOT NULL,
|
|
PRIMARY KEY('siteResourceId', 'providerId'),
|
|
FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'virtualApiKeyResources' (
|
|
'virtualApiKeyId' text NOT NULL,
|
|
'resourceId' integer NOT NULL,
|
|
PRIMARY KEY('virtualApiKeyId', 'resourceId'),
|
|
FOREIGN KEY ('virtualApiKeyId') REFERENCES 'virtualApiKeys'('virtualApiKeyId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE 'virtualApiKeys' (
|
|
'virtualApiKeyId' text PRIMARY KEY NOT NULL,
|
|
'orgId' text NOT NULL,
|
|
'kind' text NOT NULL,
|
|
'userId' text,
|
|
'name' text,
|
|
'description' text,
|
|
'token' text NOT NULL,
|
|
'lastChars' text NOT NULL,
|
|
'allResources' integer DEFAULT false NOT NULL,
|
|
'expiresAt' integer,
|
|
'lastUsedAt' integer,
|
|
'createdAt' integer NOT NULL,
|
|
'createdByUserId' text,
|
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('createdByUserId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE set null
|
|
);
|
|
`
|
|
).run();
|
|
db.prepare(
|
|
`
|
|
CREATE UNIQUE INDEX 'virtual_api_key_user_identity_uniq' ON 'virtualApiKeys' ('orgId','userId') WHERE "virtualApiKeys"."kind" = 'user';
|
|
`
|
|
).run();
|
|
|
|
db.prepare(
|
|
`ALTER TABLE 'roles' ADD COLUMN 'sshSudoModeNew' text DEFAULT 'full';`
|
|
).run();
|
|
db.prepare(
|
|
`UPDATE 'roles' SET "sshSudoModeNew" = "sshSudoMode";`
|
|
).run();
|
|
db.prepare(`ALTER TABLE 'roles' DROP COLUMN "sshSudoMode";`).run();
|
|
db.prepare(
|
|
`ALTER TABLE 'roles' RENAME COLUMN "sshSudoModeNew" TO "sshSudoMode";`
|
|
).run();
|
|
|
|
db.prepare(
|
|
`
|
|
CREATE TABLE '__new_targets' (
|
|
'targetId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
'resourceId' integer,
|
|
'providerId' integer,
|
|
'siteId' integer NOT NULL,
|
|
'ip' text NOT NULL,
|
|
'method' text,
|
|
'port' integer NOT NULL,
|
|
'internalPort' integer,
|
|
'enabled' integer DEFAULT true NOT NULL,
|
|
'path' text,
|
|
'pathMatchType' text,
|
|
'rewritePath' text,
|
|
'rewritePathType' text,
|
|
'priority' integer DEFAULT 100 NOT NULL,
|
|
'mode' text DEFAULT 'http' NOT NULL,
|
|
'authToken' text,
|
|
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('providerId') REFERENCES 'aiProviders'('providerId') ON UPDATE no action ON DELETE cascade,
|
|
FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade
|
|
);
|
|
`
|
|
).run();
|
|
|
|
db.prepare(
|
|
`INSERT INTO '__new_targets'("targetId", "resourceId", "providerId", "siteId", "ip", "method", "port", "internalPort", "enabled", "path", "pathMatchType", "rewritePath", "rewritePathType", "priority", "mode", "authToken") SELECT "targetId", "resourceId", "providerId", "siteId", "ip", "method", "port", "internalPort", "enabled", "path", "pathMatchType", "rewritePath", "rewritePathType", "priority", "mode", "authToken" FROM 'targets';`
|
|
).run();
|
|
db.prepare(`DROP TABLE 'targets';`).run();
|
|
db.prepare(
|
|
`ALTER TABLE '__new_targets' RENAME TO 'targets';`
|
|
).run();
|
|
db.prepare(
|
|
`ALTER TABLE 'subscriptions' ADD 'override' integer DEFAULT false;`
|
|
).run();
|
|
db.prepare(
|
|
`ALTER TABLE 'clients' ADD 'exitNodeSubnet' text;`
|
|
).run();
|
|
db.prepare(
|
|
`ALTER TABLE 'orgs' ADD 'settingsLogRetentionDaysAISessions' integer DEFAULT 7 NOT NULL;`
|
|
).run();
|
|
db.prepare(
|
|
`ALTER TABLE 'siteResources' ADD 'requiresExitNodeConnection' integer DEFAULT false NOT NULL;`
|
|
).run();
|
|
|
|
const insertRoleAction = db.prepare(`
|
|
INSERT INTO 'roleActions' ("roleId", "actionId", "orgId")
|
|
SELECT r."roleId", ?, r."orgId"
|
|
FROM 'roles' r
|
|
WHERE COALESCE(r."isAdmin", 0) = 0
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM 'roleActions' ra
|
|
WHERE ra."roleId" = r."roleId"
|
|
AND ra."actionId" = ?
|
|
AND ra."orgId" = r."orgId"
|
|
);
|
|
`);
|
|
|
|
for (const actionId of actionsToGrant) {
|
|
insertRoleAction.run(actionId, actionId);
|
|
}
|
|
})();
|
|
|
|
db.pragma("foreign_keys = ON");
|
|
|
|
console.log("Migrated database");
|
|
} catch (e) {
|
|
console.log("Failed to migrate db:", e);
|
|
throw e;
|
|
}
|
|
|
|
try {
|
|
const traefikPath = path.join(
|
|
APP_PATH,
|
|
"traefik",
|
|
"traefik_config.yml"
|
|
);
|
|
|
|
const schema = z.object({
|
|
experimental: z.object({
|
|
plugins: z.object({
|
|
badger: z.object({
|
|
moduleName: z.string(),
|
|
version: z.string()
|
|
})
|
|
})
|
|
})
|
|
});
|
|
|
|
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
|
|
const traefikConfig = yaml.load(traefikFileContents) as any;
|
|
|
|
const parsedConfig = schema.safeParse(traefikConfig);
|
|
|
|
if (!parsedConfig.success) {
|
|
throw new Error(fromZodError(parsedConfig.error).toString());
|
|
}
|
|
|
|
traefikConfig.experimental.plugins.badger.version = "v1.6.0";
|
|
|
|
const updatedTraefikYaml = yaml.dump(traefikConfig);
|
|
|
|
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
|
|
|
|
console.log(
|
|
"Updated the version of Badger in your Traefik configuration to v1.6.0"
|
|
);
|
|
} catch (e) {
|
|
console.log(
|
|
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information."
|
|
);
|
|
console.error(e);
|
|
}
|
|
|
|
console.log(`${version} migration complete`);
|
|
}
|