mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-04 17:45:24 +00:00
feat: add the operations layer for declared plugin actions
This commit is contained in:
1 parent
b87fe3c6a5
commit
cc725c7889
54 files changed
+22319
-8
No files matched your search
@@ -599,6 +599,70 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
UNIQUE (role_name, permission)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS operation_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
action_id TEXT NOT NULL,
|
||||
action_version INTEGER NOT NULL DEFAULT 1,
|
||||
plugin_id TEXT NOT NULL,
|
||||
plugin_version TEXT NOT NULL,
|
||||
user_id TEXT REFERENCES users (id) ON DELETE SET NULL,
|
||||
username TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
host_id INTEGER,
|
||||
host_name TEXT,
|
||||
batch_id TEXT,
|
||||
dry_run INTEGER NOT NULL DEFAULT 0,
|
||||
risk TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
changed INTEGER,
|
||||
input TEXT,
|
||||
sealed_input TEXT,
|
||||
output TEXT,
|
||||
undo_input TEXT,
|
||||
error TEXT,
|
||||
error_code TEXT,
|
||||
undo_of TEXT,
|
||||
idempotency_key TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
finished_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_runs_created ON operation_runs (created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_runs_user_created ON operation_runs (user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_runs_host_created ON operation_runs (host_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_runs_status ON operation_runs (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_runs_batch ON operation_runs (batch_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS operation_approvals (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL REFERENCES operation_runs (id) ON DELETE CASCADE,
|
||||
requested_by TEXT,
|
||||
reason TEXT,
|
||||
allow_self INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL,
|
||||
decided_by TEXT,
|
||||
comment TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
decided_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_approvals_status ON operation_approvals (status, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_approvals_run ON operation_approvals (run_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS operation_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
event_id TEXT NOT NULL,
|
||||
plugin_id TEXT NOT NULL,
|
||||
host_id INTEGER,
|
||||
severity TEXT,
|
||||
payload TEXT,
|
||||
run_id TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_events_created ON operation_events (created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_events_host_created ON operation_events (host_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
@@ -961,6 +1025,7 @@ const enforceUniqueUsernames = () => {
|
||||
|
||||
const migrateSchema = () => {
|
||||
repairSnippetsColumns(sqlite);
|
||||
addColumnIfNotExists("api_keys", "scopes", "TEXT");
|
||||
addColumnIfNotExists("user_preferences", "theme", "TEXT");
|
||||
addColumnIfNotExists("user_preferences", "font_size", "TEXT");
|
||||
addColumnIfNotExists("user_preferences", "accent_color", "TEXT");
|
||||
|
||||
@@ -673,10 +673,102 @@ export const apiKeys = mysqlTable(
|
||||
expiresAt: text("expires_at"),
|
||||
lastUsedAt: text("last_used_at"),
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
// JSON list. Null means full access, which every older key has.
|
||||
scopes: text("scopes"),
|
||||
},
|
||||
(table) => [index("idx_api_keys_user_id").on(table.userId)],
|
||||
);
|
||||
|
||||
export const operationRuns = mysqlTable(
|
||||
"operation_runs",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
actionId: text("action_id").notNull(),
|
||||
actionVersion: int("action_version").notNull().default(1),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
pluginVersion: text("plugin_version").notNull(),
|
||||
|
||||
// Nullable on purpose: history outlives the account, like audit_logs.
|
||||
userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "set null" }),
|
||||
username: text("username").notNull(),
|
||||
|
||||
origin: text("origin").notNull(),
|
||||
hostId: int("host_id"),
|
||||
hostName: text("host_name"),
|
||||
batchId: varchar("batch_id", { length: 255 }),
|
||||
dryRun: boolean("dry_run").notNull().default(false),
|
||||
risk: text("risk").notNull(),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
summary: text("summary"),
|
||||
changed: boolean("changed"),
|
||||
input: text("input"),
|
||||
// Only until the run ends, then wiped.
|
||||
sealedInput: text("sealed_input"),
|
||||
output: text("output"),
|
||||
undoInput: text("undo_input"),
|
||||
error: text("error"),
|
||||
errorCode: text("error_code"),
|
||||
undoOf: text("undo_of"),
|
||||
idempotencyKey: text("idempotency_key"),
|
||||
createdAt: varchar("created_at", { length: 255 }).notNull(),
|
||||
startedAt: text("started_at"),
|
||||
finishedAt: text("finished_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_runs_created").on(table.createdAt),
|
||||
index("idx_operation_runs_user_created").on(table.userId, table.createdAt),
|
||||
index("idx_operation_runs_host_created").on(table.hostId, table.createdAt),
|
||||
index("idx_operation_runs_status").on(table.status),
|
||||
index("idx_operation_runs_batch").on(table.batchId),
|
||||
],
|
||||
);
|
||||
|
||||
export const operationApprovals = mysqlTable(
|
||||
"operation_approvals",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
runId: varchar("run_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => operationRuns.id, { onDelete: "cascade" }),
|
||||
requestedBy: text("requested_by"),
|
||||
reason: text("reason"),
|
||||
allowSelf: boolean("allow_self")
|
||||
.notNull()
|
||||
.default(false),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
decidedBy: text("decided_by"),
|
||||
comment: text("comment"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
expiresAt: varchar("expires_at", { length: 255 }).notNull(),
|
||||
decidedAt: text("decided_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_approvals_status").on(table.status, table.expiresAt),
|
||||
index("idx_operation_approvals_run").on(table.runId),
|
||||
],
|
||||
);
|
||||
|
||||
export const operationEvents = mysqlTable(
|
||||
"operation_events",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
eventId: text("event_id").notNull(),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
hostId: int("host_id"),
|
||||
severity: text("severity"),
|
||||
payload: text("payload"),
|
||||
runId: text("run_id"),
|
||||
createdAt: varchar("created_at", { length: 255 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_events_created").on(table.createdAt),
|
||||
index("idx_operation_events_host_created").on(
|
||||
table.hostId,
|
||||
table.createdAt,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const userOpenTabs = mysqlTable(
|
||||
"user_open_tabs",
|
||||
{
|
||||
|
||||
@@ -673,10 +673,102 @@ export const apiKeys = pgTable(
|
||||
expiresAt: text("expires_at"),
|
||||
lastUsedAt: text("last_used_at"),
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
// JSON list. Null means full access, which every older key has.
|
||||
scopes: text("scopes"),
|
||||
},
|
||||
(table) => [index("idx_api_keys_user_id").on(table.userId)],
|
||||
);
|
||||
|
||||
export const operationRuns = pgTable(
|
||||
"operation_runs",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
actionId: text("action_id").notNull(),
|
||||
actionVersion: integer("action_version").notNull().default(1),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
pluginVersion: text("plugin_version").notNull(),
|
||||
|
||||
// Nullable on purpose: history outlives the account, like audit_logs.
|
||||
userId: varchar("user_id", { length: 255 }).references(() => users.id, { onDelete: "set null" }),
|
||||
username: text("username").notNull(),
|
||||
|
||||
origin: text("origin").notNull(),
|
||||
hostId: integer("host_id"),
|
||||
hostName: text("host_name"),
|
||||
batchId: varchar("batch_id", { length: 255 }),
|
||||
dryRun: boolean("dry_run").notNull().default(false),
|
||||
risk: text("risk").notNull(),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
summary: text("summary"),
|
||||
changed: boolean("changed"),
|
||||
input: text("input"),
|
||||
// Only until the run ends, then wiped.
|
||||
sealedInput: text("sealed_input"),
|
||||
output: text("output"),
|
||||
undoInput: text("undo_input"),
|
||||
error: text("error"),
|
||||
errorCode: text("error_code"),
|
||||
undoOf: text("undo_of"),
|
||||
idempotencyKey: text("idempotency_key"),
|
||||
createdAt: varchar("created_at", { length: 255 }).notNull(),
|
||||
startedAt: text("started_at"),
|
||||
finishedAt: text("finished_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_runs_created").on(table.createdAt),
|
||||
index("idx_operation_runs_user_created").on(table.userId, table.createdAt),
|
||||
index("idx_operation_runs_host_created").on(table.hostId, table.createdAt),
|
||||
index("idx_operation_runs_status").on(table.status),
|
||||
index("idx_operation_runs_batch").on(table.batchId),
|
||||
],
|
||||
);
|
||||
|
||||
export const operationApprovals = pgTable(
|
||||
"operation_approvals",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
runId: varchar("run_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => operationRuns.id, { onDelete: "cascade" }),
|
||||
requestedBy: text("requested_by"),
|
||||
reason: text("reason"),
|
||||
allowSelf: boolean("allow_self")
|
||||
.notNull()
|
||||
.default(false),
|
||||
status: varchar("status", { length: 255 }).notNull(),
|
||||
decidedBy: text("decided_by"),
|
||||
comment: text("comment"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
expiresAt: varchar("expires_at", { length: 255 }).notNull(),
|
||||
decidedAt: text("decided_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_approvals_status").on(table.status, table.expiresAt),
|
||||
index("idx_operation_approvals_run").on(table.runId),
|
||||
],
|
||||
);
|
||||
|
||||
export const operationEvents = pgTable(
|
||||
"operation_events",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
eventId: text("event_id").notNull(),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
hostId: integer("host_id"),
|
||||
severity: text("severity"),
|
||||
payload: text("payload"),
|
||||
runId: text("run_id"),
|
||||
createdAt: varchar("created_at", { length: 255 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_events_created").on(table.createdAt),
|
||||
index("idx_operation_events_host_created").on(
|
||||
table.hostId,
|
||||
table.createdAt,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const userOpenTabs = pgTable(
|
||||
"user_open_tabs",
|
||||
{
|
||||
|
||||
@@ -663,10 +663,102 @@ export const apiKeys = sqliteTable(
|
||||
expiresAt: text("expires_at"),
|
||||
lastUsedAt: text("last_used_at"),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
// JSON list. Null means full access, which every older key has.
|
||||
scopes: text("scopes"),
|
||||
},
|
||||
(table) => [index("idx_api_keys_user_id").on(table.userId)],
|
||||
);
|
||||
|
||||
export const operationRuns = sqliteTable(
|
||||
"operation_runs",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
actionId: text("action_id").notNull(),
|
||||
actionVersion: integer("action_version").notNull().default(1),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
pluginVersion: text("plugin_version").notNull(),
|
||||
|
||||
// Nullable on purpose: history outlives the account, like audit_logs.
|
||||
userId: text("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
username: text("username").notNull(),
|
||||
|
||||
origin: text("origin").notNull(),
|
||||
hostId: integer("host_id"),
|
||||
hostName: text("host_name"),
|
||||
batchId: text("batch_id"),
|
||||
dryRun: integer("dry_run", { mode: "boolean" }).notNull().default(false),
|
||||
risk: text("risk").notNull(),
|
||||
status: text("status").notNull(),
|
||||
summary: text("summary"),
|
||||
changed: integer("changed", { mode: "boolean" }),
|
||||
input: text("input"),
|
||||
// Only until the run ends, then wiped.
|
||||
sealedInput: text("sealed_input"),
|
||||
output: text("output"),
|
||||
undoInput: text("undo_input"),
|
||||
error: text("error"),
|
||||
errorCode: text("error_code"),
|
||||
undoOf: text("undo_of"),
|
||||
idempotencyKey: text("idempotency_key"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
startedAt: text("started_at"),
|
||||
finishedAt: text("finished_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_runs_created").on(table.createdAt),
|
||||
index("idx_operation_runs_user_created").on(table.userId, table.createdAt),
|
||||
index("idx_operation_runs_host_created").on(table.hostId, table.createdAt),
|
||||
index("idx_operation_runs_status").on(table.status),
|
||||
index("idx_operation_runs_batch").on(table.batchId),
|
||||
],
|
||||
);
|
||||
|
||||
export const operationApprovals = sqliteTable(
|
||||
"operation_approvals",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
runId: text("run_id")
|
||||
.notNull()
|
||||
.references(() => operationRuns.id, { onDelete: "cascade" }),
|
||||
requestedBy: text("requested_by"),
|
||||
reason: text("reason"),
|
||||
allowSelf: integer("allow_self", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
status: text("status").notNull(),
|
||||
decidedBy: text("decided_by"),
|
||||
comment: text("comment"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
expiresAt: text("expires_at").notNull(),
|
||||
decidedAt: text("decided_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_approvals_status").on(table.status, table.expiresAt),
|
||||
index("idx_operation_approvals_run").on(table.runId),
|
||||
],
|
||||
);
|
||||
|
||||
export const operationEvents = sqliteTable(
|
||||
"operation_events",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
eventId: text("event_id").notNull(),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
hostId: integer("host_id"),
|
||||
severity: text("severity"),
|
||||
payload: text("payload"),
|
||||
runId: text("run_id"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_operation_events_created").on(table.createdAt),
|
||||
index("idx_operation_events_host_created").on(
|
||||
table.hostId,
|
||||
table.createdAt,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const userOpenTabs = sqliteTable(
|
||||
"user_open_tabs",
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user