Files
UNITRONIX dc6b6c088e feat(centralization): move help/chat to Go server and harden agent TLS
Phases 1-4 of the Go-server centralization plan plus optional-TLS transport.

Go server:
- db: HelpRequest model + SQLite/PostgreSQL stores (help_requests_*.go), GetDeviceOrgID.
- cdap: handleHelpRequest/handleChatMessage handlers, SendChatToDevice delivery.
- api: REST help endpoints (help_handlers.go), publish help_request/chat_message events.

Node.js panel:
- bd-api.routes.js: drop local in-memory Maps, proxy all help/chat/notification
  endpoints to the Go server (read-proxy) with status/id/timestamp normalization.

Agent (native Go + Tauri sidecar):
- config.go/agent.go: optional EnforceTLS, ServerCertPin (SPKI pin), TLSInsecureSkipVerify
  with env overlays and dialOptions() cert pinning via VerifyPeerCertificate.
- HTTP (ws://) stays a fully supported transport: TLS enforcement is an explicit
  operator opt-in (never auto-derived from the URL scheme). The agent logs a warning
  recommending wss:// for untrusted networks instead of blocking the connection.
- config.rs/sidecar.rs: propagate enforce_tls + server_cert_pin from AgentConfig
  through SidecarConfig to the Go agent config; warn on plaintext ws:// to remote hosts.

This commit was made possible thanks to Insolve.
2026-06-01 00:51:29 +02:00

132 lines
3.7 KiB
Go

package db
import (
"strings"
"time"
"github.com/jackc/pgx/v5"
)
// ── Help Requests ─────────────────────────────────────────────────────
// CreateHelpRequest inserts a new help request and returns its ID.
func (pg *PostgresDB) CreateHelpRequest(r *HelpRequest) (int64, error) {
status := r.Status
if status == "" {
status = HelpStatusPending
}
var id int64
err := pg.pool.QueryRow(pg.ctx,
`INSERT INTO help_requests (device_id, hostname, org_id, message, status, handled_by)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
r.DeviceID, r.Hostname, r.OrgID, r.Message, status, r.HandledBy,
).Scan(&id)
return id, err
}
// GetHelpRequest returns a single help request by ID.
func (pg *PostgresDB) GetHelpRequest(id int64) (*HelpRequest, error) {
var r HelpRequest
err := pg.pool.QueryRow(pg.ctx,
`SELECT id, device_id, hostname, org_id, message, status, handled_by, created_at, updated_at
FROM help_requests WHERE id = $1`, id,
).Scan(&r.ID, &r.DeviceID, &r.Hostname, &r.OrgID, &r.Message, &r.Status, &r.HandledBy, &r.CreatedAt, &r.UpdatedAt)
if err != nil {
return nil, err
}
return &r, nil
}
// ListHelpRequests returns help requests matching the filter, newest first.
func (pg *PostgresDB) ListHelpRequests(filter HelpRequestFilter) ([]*HelpRequest, error) {
limit := filter.Limit
if limit <= 0 || limit > 500 {
limit = 100
}
var (
conds []string
args []any
)
idx := 1
if filter.Status != "" {
conds = append(conds, "status = $"+itoa(idx))
args = append(args, filter.Status)
idx++
}
if filter.DeviceID != "" {
conds = append(conds, "device_id = $"+itoa(idx))
args = append(args, filter.DeviceID)
idx++
}
if filter.OrgID != "" {
conds = append(conds, "org_id = $"+itoa(idx))
args = append(args, filter.OrgID)
idx++
}
query := `SELECT id, device_id, hostname, org_id, message, status, handled_by, created_at, updated_at
FROM help_requests`
if len(conds) > 0 {
query += " WHERE " + strings.Join(conds, " AND ")
}
query += " ORDER BY id DESC LIMIT $" + itoa(idx)
args = append(args, limit)
rows, err := pg.pool.Query(pg.ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var reqs []*HelpRequest
for rows.Next() {
var r HelpRequest
if err := rows.Scan(&r.ID, &r.DeviceID, &r.Hostname, &r.OrgID, &r.Message, &r.Status, &r.HandledBy, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
reqs = append(reqs, &r)
}
return reqs, rows.Err()
}
// UpdateHelpRequestStatus changes the status (and handler) of a help request.
func (pg *PostgresDB) UpdateHelpRequestStatus(id int64, status, handledBy string) error {
_, err := pg.pool.Exec(pg.ctx,
`UPDATE help_requests SET status = $1, handled_by = $2, updated_at = NOW()
WHERE id = $3`,
status, handledBy, id,
)
return err
}
// PruneHelpRequests deletes resolved/cancelled requests older than maxAge.
func (pg *PostgresDB) PruneHelpRequests(maxAge time.Duration) (int64, error) {
cutoff := time.Now().Add(-maxAge)
result, err := pg.pool.Exec(pg.ctx,
`DELETE FROM help_requests
WHERE status IN ($1, $2) AND updated_at < $3`,
HelpStatusResolved, HelpStatusCancelled, cutoff,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
// GetDeviceOrgID returns the organization ID a device belongs to, or "".
func (pg *PostgresDB) GetDeviceOrgID(deviceID string) (string, error) {
var orgID string
err := pg.pool.QueryRow(pg.ctx,
`SELECT org_id FROM org_devices WHERE device_id = $1 LIMIT 1`, deviceID,
).Scan(&orgID)
if err == pgx.ErrNoRows {
return "", nil
}
if err != nil {
return "", err
}
return orgID, nil
}