Files
poshmanager/server/db.js
2026-06-25 14:43:13 -05:00

594 lines
23 KiB
JavaScript

import fs from 'node:fs';
import { path } from './utils/pathUtils.js';
import { DatabaseSync } from 'node:sqlite';
import bcrypt from 'bcryptjs';
import { nanoid } from 'nanoid';
import { config } from './config.js';
import { settingDefinitions } from './settingsRegistry.js';
fs.mkdirSync(path.dirname(config.databasePath), { recursive: true });
fs.mkdirSync(config.fileLockerDir, { recursive: true });
fs.mkdirSync(config.toolsDir, { recursive: true });
export const db = new DatabaseSync(config.databasePath);
db.exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;');
export function id(prefix) {
return `${prefix}_${nanoid(12)}`;
}
export function now() {
return new Date().toISOString();
}
export function json(value, fallback = []) {
if (value == null || value === '') return JSON.stringify(fallback);
return JSON.stringify(value);
}
export function parseJson(value, fallback = []) {
try {
return value ? JSON.parse(value) : fallback;
} catch {
return fallback;
}
}
function ensureColumn(table, column, definition) {
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
if (!columns.some((row) => row.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
}
}
export function migrate() {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
password_hash TEXT,
auth_provider TEXT NOT NULL DEFAULT 'local',
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_groups (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE IF NOT EXISTS credentials (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
username TEXT,
secret_cipher TEXT NOT NULL,
secret_iv TEXT NOT NULL,
secret_tag TEXT NOT NULL,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS hosts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
fqdn TEXT,
address TEXT NOT NULL,
os_family TEXT NOT NULL DEFAULT 'windows',
transport TEXT NOT NULL DEFAULT 'winrm',
port INTEGER,
credential_id TEXT REFERENCES credentials(id) ON DELETE SET NULL,
tags TEXT NOT NULL DEFAULT '[]',
notes TEXT,
source_type TEXT NOT NULL DEFAULT 'manual',
source_connection_id TEXT,
source_ref TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS vcenter_connections (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
username TEXT NOT NULL,
secret_cipher TEXT NOT NULL,
secret_iv TEXT NOT NULL,
secret_tag TEXT NOT NULL,
api_mode TEXT NOT NULL DEFAULT 'auto',
request_timeout_ms INTEGER NOT NULL DEFAULT 20000,
allow_untrusted_tls INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
last_test_status TEXT,
last_test_message TEXT,
last_test_at TEXT,
visibility TEXT NOT NULL DEFAULT 'shared',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS host_groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
mode TEXT NOT NULL DEFAULT 'manual',
match_mode TEXT NOT NULL DEFAULT 'all',
rules TEXT NOT NULL DEFAULT '[]',
visibility TEXT NOT NULL DEFAULT 'shared',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
last_synced_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS host_group_members (
host_group_id TEXT NOT NULL REFERENCES host_groups(id) ON DELETE CASCADE,
host_id TEXT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL,
PRIMARY KEY (host_group_id, host_id)
);
CREATE TABLE IF NOT EXISTS folders (
id TEXT PRIMARY KEY,
parent_id TEXT REFERENCES folders(id) ON DELETE CASCADE,
name TEXT NOT NULL,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS scripts (
id TEXT PRIMARY KEY,
folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL,
name TEXT NOT NULL,
description TEXT,
content TEXT NOT NULL,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
version INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS asset_folders (
id TEXT PRIMARY KEY,
parent_id TEXT REFERENCES asset_folders(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS assets (
id TEXT PRIMARY KEY,
folder_id TEXT REFERENCES asset_folders(id) ON DELETE SET NULL,
name TEXT NOT NULL,
original_name TEXT NOT NULL,
description TEXT,
kind TEXT NOT NULL DEFAULT 'generic',
mime_type TEXT,
size_bytes INTEGER NOT NULL DEFAULT 0,
checksum TEXT NOT NULL,
storage_path TEXT NOT NULL,
package_path TEXT,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS asset_links (
id TEXT PRIMARY KEY,
asset_id TEXT NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
link_role TEXT NOT NULL DEFAULT 'reference',
package_path TEXT,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS custom_variables (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL DEFAULT 'Custom',
value_type TEXT NOT NULL DEFAULT 'string',
secret_cipher TEXT NOT NULL,
secret_iv TEXT NOT NULL,
secret_tag TEXT NOT NULL,
sensitive INTEGER NOT NULL DEFAULT 0,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS psadt_profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
app_vendor TEXT,
app_name TEXT NOT NULL,
app_version TEXT,
app_arch TEXT,
app_lang TEXT,
app_revision TEXT,
toolkit_version TEXT NOT NULL DEFAULT 'v4',
template_version TEXT NOT NULL DEFAULT 'v4',
deployment_type TEXT NOT NULL DEFAULT 'Install',
deploy_mode TEXT NOT NULL DEFAULT 'Interactive',
require_admin INTEGER NOT NULL DEFAULT 1,
zero_config INTEGER NOT NULL DEFAULT 0,
suppress_reboot INTEGER NOT NULL DEFAULT 0,
close_processes TEXT NOT NULL DEFAULT '[]',
install_tasks TEXT NOT NULL DEFAULT '[]',
ui_plan TEXT NOT NULL DEFAULT '{}',
config_plan TEXT NOT NULL DEFAULT '{}',
admx_plan TEXT NOT NULL DEFAULT '{}',
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS intune_deployments (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
profile_id TEXT REFERENCES psadt_profiles(id) ON DELETE SET NULL,
script_id TEXT REFERENCES scripts(id) ON DELETE SET NULL,
source_folder TEXT,
intunewin_file TEXT,
app_type TEXT NOT NULL DEFAULT 'Windows app (Win32)',
command_style TEXT NOT NULL DEFAULT 'v4',
install_behavior TEXT NOT NULL DEFAULT 'system',
restart_behavior TEXT NOT NULL DEFAULT 'return-code',
ui_mode TEXT NOT NULL DEFAULT 'native-v4',
requirements TEXT NOT NULL DEFAULT '{}',
install_command TEXT NOT NULL,
uninstall_command TEXT NOT NULL,
detection_type TEXT NOT NULL DEFAULT 'custom-script',
detection_rule TEXT NOT NULL DEFAULT '',
return_codes TEXT NOT NULL DEFAULT '[]',
assignments TEXT NOT NULL DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'draft',
graph_app_id TEXT,
notes TEXT,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS graph_connections (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
tenant_id TEXT NOT NULL,
client_id TEXT NOT NULL,
secret_cipher TEXT NOT NULL,
secret_iv TEXT NOT NULL,
secret_tag TEXT NOT NULL,
cloud TEXT NOT NULL DEFAULT 'global',
graph_base_url TEXT NOT NULL DEFAULT 'https://graph.microsoft.com',
authority_host TEXT NOT NULL DEFAULT 'https://login.microsoftonline.com',
default_api_version TEXT NOT NULL DEFAULT 'v1.0',
enabled INTEGER NOT NULL DEFAULT 1,
last_test_status TEXT,
last_test_message TEXT,
last_test_at TEXT,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS intune_deployment_statuses (
id TEXT PRIMARY KEY,
deployment_id TEXT NOT NULL REFERENCES intune_deployments(id) ON DELETE CASCADE,
connection_id TEXT REFERENCES graph_connections(id) ON DELETE SET NULL,
graph_app_id TEXT NOT NULL,
scope TEXT NOT NULL DEFAULT 'device',
principal_id TEXT,
principal_name TEXT,
device_id TEXT,
device_name TEXT,
user_id TEXT,
user_principal_name TEXT,
install_state TEXT,
install_state_detail TEXT,
error_code TEXT,
error_description TEXT,
last_sync_datetime TEXT,
raw_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS intune_deployment_sync_runs (
id TEXT PRIMARY KEY,
deployment_id TEXT NOT NULL REFERENCES intune_deployments(id) ON DELETE CASCADE,
connection_id TEXT REFERENCES graph_connections(id) ON DELETE SET NULL,
graph_app_id TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
summary TEXT NOT NULL DEFAULT '{}',
started_at TEXT NOT NULL,
ended_at TEXT
);
CREATE TABLE IF NOT EXISTS applications (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
vendor TEXT,
publisher TEXT,
current_version TEXT,
latest_known_version TEXT,
version_source TEXT NOT NULL DEFAULT 'manual',
version_source_ref TEXT,
graph_connection_id TEXT REFERENCES graph_connections(id) ON DELETE SET NULL,
current_graph_app_id TEXT,
auto_check INTEGER NOT NULL DEFAULT 0,
last_checked_at TEXT,
last_check_message TEXT,
notes TEXT,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS change_requests (
id TEXT PRIMARY KEY,
deployment_id TEXT REFERENCES intune_deployments(id) ON DELETE CASCADE,
connection_id TEXT REFERENCES graph_connections(id) ON DELETE SET NULL,
action TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
requested_by TEXT REFERENCES users(id) ON DELETE SET NULL,
requester_email TEXT,
approver_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
approver_email TEXT,
reason TEXT,
decided_at TEXT,
executed_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS graph_audit_log (
id TEXT PRIMARY KEY,
connection_id TEXT REFERENCES graph_connections(id) ON DELETE SET NULL,
deployment_id TEXT REFERENCES intune_deployments(id) ON DELETE SET NULL,
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
actor_email TEXT,
action TEXT NOT NULL,
target_graph_id TEXT,
status TEXT NOT NULL,
message TEXT,
request_json TEXT NOT NULL DEFAULT '{}',
response_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS script_versions (
id TEXT PRIMARY KEY,
script_id TEXT NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
content TEXT NOT NULL,
created_by TEXT REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS runplans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
script_id TEXT NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
visibility TEXT NOT NULL DEFAULT 'personal',
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
group_id TEXT REFERENCES groups(id) ON DELETE SET NULL,
parallel INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS runplan_hosts (
runplan_id TEXT NOT NULL REFERENCES runplans(id) ON DELETE CASCADE,
host_id TEXT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
PRIMARY KEY (runplan_id, host_id)
);
CREATE TABLE IF NOT EXISTS runplan_host_groups (
runplan_id TEXT NOT NULL REFERENCES runplans(id) ON DELETE CASCADE,
host_group_id TEXT NOT NULL REFERENCES host_groups(id) ON DELETE CASCADE,
PRIMARY KEY (runplan_id, host_group_id)
);
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
runplan_id TEXT REFERENCES runplans(id) ON DELETE SET NULL,
script_id TEXT REFERENCES scripts(id) ON DELETE SET NULL,
status TEXT NOT NULL,
triggered_by TEXT REFERENCES users(id) ON DELETE SET NULL,
started_at TEXT,
ended_at TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS job_hosts (
id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
host_id TEXT REFERENCES hosts(id) ON DELETE SET NULL,
status TEXT NOT NULL,
exit_code INTEGER,
started_at TEXT,
ended_at TEXT
);
CREATE TABLE IF NOT EXISTS job_logs (
id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
host_id TEXT REFERENCES hosts(id) ON DELETE SET NULL,
stream TEXT NOT NULL,
line TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'db',
updated_at TEXT NOT NULL
);
`);
ensureColumn('users', 'theme', "TEXT NOT NULL DEFAULT 'dashtreme'");
ensureColumn('users', 'job_title', 'TEXT');
ensureColumn('users', 'phone', 'TEXT');
ensureColumn('users', 'timezone', 'TEXT');
ensureColumn('users', 'avatar_url', 'TEXT');
ensureColumn('users', 'updated_at', 'TEXT');
ensureColumn('intune_deployments', 'source_folder', 'TEXT');
ensureColumn('intune_deployments', 'intunewin_file', 'TEXT');
ensureColumn('intune_deployments', 'command_style', "TEXT NOT NULL DEFAULT 'v4'");
ensureColumn('intune_deployments', 'install_behavior', "TEXT NOT NULL DEFAULT 'system'");
ensureColumn('intune_deployments', 'restart_behavior', "TEXT NOT NULL DEFAULT 'return-code'");
ensureColumn('intune_deployments', 'ui_mode', "TEXT NOT NULL DEFAULT 'native-v4'");
ensureColumn('intune_deployments', 'requirements', "TEXT NOT NULL DEFAULT '{}'");
ensureColumn('intune_deployments', 'graph_app_id', 'TEXT');
ensureColumn('intune_deployments', 'graph_connection_id', 'TEXT');
ensureColumn('intune_deployments', 'relationships', "TEXT NOT NULL DEFAULT '[]'");
ensureColumn('intune_deployments', 'application_id', 'TEXT REFERENCES applications(id) ON DELETE SET NULL');
// Auto-promote: after the soak window elapses, advance a rollout ring's intent.
ensureColumn('intune_deployments', 'auto_promote_after_hours', 'INTEGER NOT NULL DEFAULT 0');
ensureColumn('intune_deployments', 'auto_promote_ring', 'TEXT');
ensureColumn('intune_deployments', 'auto_promote_intent', "TEXT NOT NULL DEFAULT 'required'");
ensureColumn('intune_deployments', 'soak_started_at', 'TEXT');
ensureColumn('intune_deployments', 'promoted_at', 'TEXT');
// Host visibility scoping. Existing rows default to 'shared' so prior global
// hosts remain visible to everyone; new hosts can be personal/group-scoped.
ensureColumn('hosts', 'visibility', "TEXT NOT NULL DEFAULT 'shared'");
ensureColumn('hosts', 'owner_user_id', 'TEXT REFERENCES users(id) ON DELETE SET NULL');
ensureColumn('hosts', 'group_id', 'TEXT REFERENCES groups(id) ON DELETE SET NULL');
// Host provenance is hidden from normal manual edit forms but used by
// automation gates such as vCenter power-state checks before execution.
ensureColumn('hosts', 'source_type', "TEXT NOT NULL DEFAULT 'manual'");
ensureColumn('hosts', 'source_connection_id', 'TEXT');
ensureColumn('hosts', 'source_ref', 'TEXT');
// Tenant write-back is opt-in per Graph connection. Default 0 so existing
// read-only connections can never be used to change a tenant by accident.
ensureColumn('graph_connections', 'allow_write', 'INTEGER NOT NULL DEFAULT 0');
// When set, tenant-changing actions through this connection require an
// approved change request before they run.
ensureColumn('graph_connections', 'require_approval', 'INTEGER NOT NULL DEFAULT 0');
// Optional named-RBAC lists (JSON arrays of user ids). Empty = no restriction
// beyond the allow_write/admin gates.
ensureColumn('graph_connections', 'publisher_ids', "TEXT NOT NULL DEFAULT '[]'");
ensureColumn('graph_connections', 'approver_ids', "TEXT NOT NULL DEFAULT '[]'");
}
export function seed() {
const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count;
if (userCount === 0) {
const adminId = id('usr');
db.prepare(`
INSERT INTO users (id, email, display_name, password_hash, auth_provider, role, created_at)
VALUES (?, ?, ?, ?, 'local', 'admin', ?)
`).run(
adminId,
config.defaultAdminEmail.toLowerCase(),
config.defaultAdminName,
bcrypt.hashSync(config.defaultAdminPassword, 12),
now()
);
const groupId = id('grp');
db.prepare('INSERT INTO groups (id, name, description, created_at) VALUES (?, ?, ?, ?)').run(
groupId,
'Operations',
'Default shared operations group',
now()
);
db.prepare('INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)').run(adminId, groupId);
const folderId = id('fld');
db.prepare(`
INSERT INTO folders (id, parent_id, name, visibility, owner_user_id, group_id, created_at, updated_at)
VALUES (?, NULL, 'Examples', 'shared', ?, NULL, ?, ?)
`).run(folderId, adminId, now(), now());
const scriptId = id('scr');
const sample = [
'Write-Output "POSHManager sample run"',
'Write-Output "Target host: $env:POSHM_HOST_NAME"',
'Get-Date'
].join('\n');
db.prepare(`
INSERT INTO scripts (id, folder_id, name, description, content, visibility, owner_user_id, group_id, version, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'shared', ?, NULL, 1, ?, ?)
`).run(scriptId, folderId, 'Sample Health Check.ps1', 'Simple smoke-test script for local execution.', sample, adminId, now(), now());
db.prepare(`
INSERT INTO script_versions (id, script_id, version, content, created_by, created_at)
VALUES (?, ?, 1, ?, ?, ?)
`).run(id('ver'), scriptId, sample, adminId, now());
}
// Env-pinned settings are refreshed from the environment on every boot and
// marked source='env' (locked in the UI). Unpinned settings are seeded once
// as editable defaults so admin edits in the Config screen survive restarts.
const pinned = db.prepare(`
INSERT INTO settings (key, value, source, updated_at)
VALUES (?, ?, 'env', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, source = 'env', updated_at = excluded.updated_at
`);
const defaulted = db.prepare(`
INSERT INTO settings (key, value, source, updated_at)
VALUES (?, ?, 'default', ?)
ON CONFLICT(key) DO NOTHING
`);
for (const def of settingDefinitions()) {
if (def.pinned) pinned.run(def.key, def.value, now());
else defaulted.run(def.key, def.value, now());
}
}
export function visibleClause(alias = '') {
const p = alias ? `${alias}.` : '';
return `(${p}visibility = 'shared' OR ${p}owner_user_id = ? OR ${p}group_id IN (SELECT group_id FROM user_groups WHERE user_id = ?))`;
}