Files
rogee b6d0af1a56
management-images / build-and-publish (push) Successful in 9m15s
docs: add one-click management deployment and image workflow
2026-09-16 17:57:05 +08:00

152 lines
5.8 KiB
Go

package management
import (
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/gofiber/fiber/v3"
)
var requiredVerificationChecks = []string{"transport", "registration_auth", "caller_id_rules", "codec", "capacity", "whitelist"}
func (s *Store) AddVerification(ctx context.Context, trunkID string, revision int64, input VerificationInput, actor, requestID string) error {
if !validID(trunkID) || revision < 1 || !containsString(requiredVerificationChecks, input.CheckName) {
return newAppError(422, "INVALID_VERIFICATION", "check_name or revision is invalid", nil)
}
if input.Result != "confirmed" && input.Result != "failed" && input.Result != "unknown" && input.Result != "not_applicable" {
return newAppError(422, "INVALID_VERIFICATION_RESULT", "result is invalid", nil)
}
if input.CheckedBy == "" {
input.CheckedBy = actor
}
if strings.ContainsAny(input.EvidenceRef+input.CheckedBy, "\r\n") || len(input.EvidenceRef) > 512 {
return newAppError(422, "INVALID_VERIFICATION_EVIDENCE", "verification evidence is invalid", nil)
}
now := utcString(time.Now())
return s.withTx(ctx, func(tx *sql.Tx) error {
var exists int
if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM trunk_versions WHERE trunk_id=? AND revision=?", trunkID, revision).Scan(&exists); err != nil {
return err
}
if exists == 0 {
return newAppError(404, "VERSION_NOT_FOUND", "trunk version does not exist", nil)
}
_, err := tx.ExecContext(ctx, "INSERT INTO trunk_verifications(trunk_id,revision,check_name,result,evidence_ref,checked_by,checked_at) VALUES(?,?,?,?,?,?,?) ON CONFLICT(trunk_id,revision,check_name) DO UPDATE SET result=excluded.result,evidence_ref=excluded.evidence_ref,checked_by=excluded.checked_by,checked_at=excluded.checked_at", trunkID, revision, input.CheckName, input.Result, input.EvidenceRef, input.CheckedBy, now)
if err != nil {
return err
}
details, _ := json.Marshal(map[string]any{"check_name": input.CheckName, "result": input.Result, "evidence_ref": input.EvidenceRef})
return insertAudit(ctx, tx, "trunk", trunkID, "verification", revision, actor, requestID, string(details), now)
})
}
func (s *Store) RealVerificationComplete(ctx context.Context, trunkID string, revision int64) (bool, []string, error) {
results := map[string]string{}
err := s.withRead(ctx, func(db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT check_name,result FROM trunk_verifications WHERE trunk_id=? AND revision=?", trunkID, revision)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var name, result string
if err := rows.Scan(&name, &result); err != nil {
return err
}
results[name] = result
}
return rows.Err()
})
if err != nil {
return false, nil, err
}
missing := make([]string, 0)
for _, name := range requiredVerificationChecks {
if results[name] != "confirmed" {
missing = append(missing, name)
}
}
return len(missing) == 0, missing, nil
}
func (s *Store) ListVerifications(ctx context.Context, trunkID string, revision int64) ([]map[string]any, error) {
var out []map[string]any
err := s.withRead(ctx, func(db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT check_name,result,evidence_ref,checked_by,checked_at FROM trunk_verifications WHERE trunk_id=? AND revision=? ORDER BY check_name", trunkID, revision)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var name, result, evidence, by, at string
if err := rows.Scan(&name, &result, &evidence, &by, &at); err != nil {
return err
}
out = append(out, map[string]any{"check_name": name, "result": result, "evidence_ref": evidence, "checked_by": by, "checked_at": at})
}
return rows.Err()
})
return out, err
}
func (s *Server) addVerification(c fiber.Ctx) error {
trunkID := c.Params("trunk_id")
p, authErr := s.authorize(c, "admin", "sip.trunk.write", "trunk", trunkID)
if authErr != nil {
return s.fail(c, authErr)
}
expected, err := parseIfMatch(c)
if err != nil {
return s.fail(c, err)
}
requestID, err := requireHeader(c, "X-Request-ID")
if err != nil {
return s.fail(c, err)
}
var body struct {
Revision int64 `json:"revision"`
VerificationInput
}
if err := decodeJSON(c, &body); err != nil {
return s.fail(c, err)
}
if body.Revision < 1 {
return s.fail(c, newAppError(422, "REVISION_REQUIRED", "revision is required", nil))
}
ctx, cancel := s.context(c)
defer cancel()
trunk, trunkErr := s.store.GetTrunk(ctx, trunkID, s.cfg.PublicMode())
if errors.Is(trunkErr, sql.ErrNoRows) {
return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "trunk does not exist", nil))
}
if trunkErr != nil {
return s.fail(c, asAppError(trunkErr))
}
if trunk.LatestRevision != expected || body.Revision > trunk.LatestRevision {
return s.fail(c, newAppError(409, "REVISION_CONFLICT", "verification must target the current or an existing revision", map[string]any{"current_revision": trunk.LatestRevision}))
}
op, replay, beginErr := s.beginOperation(ctx, p, "trunk.verification", "trunk", trunkID, requestID, body, expected)
if replay || beginErr != nil {
return s.replayOrError(c, op, beginErr)
}
if err := s.store.AddVerification(ctx, trunkID, body.Revision, body.VerificationInput, p.Principal, requestID); err != nil {
appErr := asAppError(err)
_ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID))
return s.fail(c, appErr)
}
checks, checksErr := s.store.ListVerifications(ctx, trunkID, body.Revision)
if checksErr != nil {
return s.fail(c, asAppError(checksErr))
}
response := map[string]any{"mode": s.cfg.PublicMode(), "trunk_id": trunkID, "revision": body.Revision, "verifications": checks}
if err := s.finishSuccess(ctx, op, http.StatusOK, response); err != nil {
return s.fail(c, asAppError(err))
}
return c.JSON(response)
}