-- +goose Up -- +goose StatementBegin -- Admin users (analog enconf admin_users). Auth is JWT-based; password -- is bcrypt'd via golang.org/x/crypto/bcrypt. role is a free-form -- text column today (only "admin" exists in v1) so future RBAC roles -- can be added without a CHECK-constraint migration. CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'admin', active BOOLEAN NOT NULL DEFAULT TRUE, last_login_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT users_email_unique UNIQUE (email) ); CREATE INDEX IF NOT EXISTS idx_users_active ON users (active) WHERE active; -- Append-only audit trail. Every config change, login, license event, -- upgrade trigger gets an entry. detail is freeform JSONB so each -- handler can attach whatever context is meaningful (e.g. diff of the -- domain row, apt versions before/after, cluster-join token hash). CREATE TABLE IF NOT EXISTS audit_log ( id BIGSERIAL PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, subject TEXT, detail JSONB, node_id TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log (created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_log_subject ON audit_log (subject); CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log (action); -- Global key/value config. Avoids one-row tables for things like -- cluster FQDN, ACME contact e-mail, default WireGuard listen port. -- value is TEXT — JSON encoding is on the caller if needed. CREATE TABLE IF NOT EXISTS system_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP TABLE IF EXISTS system_settings; DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS users; -- +goose StatementEnd