343 lines
11 KiB
Go
343 lines
11 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/deviceauth"
|
||
|
|
"data-terminal-cloud/internal/saas"
|
||
|
|
)
|
||
|
|
|
||
|
|
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||
|
|
|
||
|
|
func RegisterFirmwareSigningKeyHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var in struct {
|
||
|
|
KeyID string `json:"key_id"`
|
||
|
|
PublicKey string `json:"public_key_base64"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &in, "固件签名密钥参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
in.KeyID = strings.TrimSpace(in.KeyID)
|
||
|
|
in.PublicKey = strings.TrimSpace(in.PublicKey)
|
||
|
|
if in.KeyID == "" || in.PublicKey == "" {
|
||
|
|
writeValidation(w, r, "固件签名密钥参数不正确", FieldError{Field: "key", Message: "密钥编号和Ed25519公钥均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := service.RegisterFirmwareSigningKey(r.Context(), p, r.PathValue("id"), RequestID(r.Context()), in.KeyID, in.PublicKey); err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusCreated, map[string]any{"key_id": in.KeyID, "status": "ACTIVE"})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListFirmwareSigningKeysHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListFirmwareSigningKeys(r.Context(), p, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func RevokeFirmwareSigningKeyHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
KeyID string `json:"key_id"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "固件签名密钥吊销参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.KeyID = strings.TrimSpace(input.KeyID)
|
||
|
|
if input.KeyID == "" || len([]rune(input.KeyID)) > 120 {
|
||
|
|
writeValidation(w, r, "固件签名密钥吊销参数不正确", FieldError{Field: "key_id", Message: "密钥编号必须为1至120个字符"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.RevokeFirmwareSigningKey(r.Context(), p, r.PathValue("id"), RequestID(r.Context()), input.KeyID)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func PrepareFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var in struct {
|
||
|
|
Model string `json:"model"`
|
||
|
|
HardwareRevision string `json:"hardware_revision"`
|
||
|
|
Version string `json:"version"`
|
||
|
|
ExpectedSize int64 `json:"expected_size"`
|
||
|
|
ExpectedDigest string `json:"expected_digest"`
|
||
|
|
Signature string `json:"signature"`
|
||
|
|
SigningKeyID string `json:"signing_key_id"`
|
||
|
|
MinimumSecurityVersion string `json:"minimum_security_version"`
|
||
|
|
ReleaseNotes string `json:"release_notes"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &in, "固件版本参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
in.Model = strings.TrimSpace(in.Model)
|
||
|
|
in.HardwareRevision = strings.TrimSpace(in.HardwareRevision)
|
||
|
|
in.Version = strings.TrimSpace(in.Version)
|
||
|
|
in.ExpectedDigest = strings.ToLower(strings.TrimSpace(in.ExpectedDigest))
|
||
|
|
in.Signature = strings.TrimSpace(in.Signature)
|
||
|
|
in.SigningKeyID = strings.TrimSpace(in.SigningKeyID)
|
||
|
|
if in.Model == "" || in.Version == "" || in.ExpectedSize < 1 || in.ExpectedSize > 268435456 || !sha256Pattern.MatchString(in.ExpectedDigest) || in.Signature == "" || in.SigningKeyID == "" {
|
||
|
|
writeValidation(w, r, "固件版本参数不正确", FieldError{Field: "firmware", Message: "型号、版本、1至256MB大小、SHA-256、Ed25519签名和密钥编号均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.PrepareFirmwareRelease(r.Context(), p, r.PathValue("id"), RequestID(r.Context()), saas.PrepareFirmwareInput{Model: in.Model, HardwareRevision: in.HardwareRevision, Version: in.Version, ExpectedSize: in.ExpectedSize, ExpectedDigest: in.ExpectedDigest, Signature: in.Signature, SigningKeyID: in.SigningKeyID, MinimumSecurityVersion: strings.TrimSpace(in.MinimumSecurityVersion), ReleaseNotes: strings.TrimSpace(in.ReleaseNotes)})
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusCreated, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListFirmwareReleasesHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListFirmwareReleases(r.Context(), p, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func GetFirmwareReleaseImpactHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.GetFirmwareReleaseImpact(r.Context(), p, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func FinalizeFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.FinalizeFirmwareRelease(r.Context(), p, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func RenewFirmwareUploadHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.RenewFirmwareUpload(r.Context(), p, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func PauseFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.PauseFirmwareRelease(r.Context(), p, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ResumeFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.ResumeFirmwareRelease(r.Context(), p, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func RetireFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.RetireFirmwareRelease(r.Context(), p, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func StartFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var in struct {
|
||
|
|
ReleaseID string `json:"release_id"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
TargetPercentage int `json:"target_percentage"`
|
||
|
|
DeviceIDs []string `json:"device_ids"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &in, "固件灰度批次参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
in.ReleaseID = strings.TrimSpace(in.ReleaseID)
|
||
|
|
in.Name = strings.TrimSpace(in.Name)
|
||
|
|
if in.ReleaseID == "" || in.Name == "" || in.TargetPercentage < 1 || in.TargetPercentage > 100 || len(in.DeviceIDs) == 0 {
|
||
|
|
writeValidation(w, r, "固件灰度批次参数不正确", FieldError{Field: "rollout", Message: "版本、批次名称、1至100灰度比例和目标设备均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.StartFirmwareRollout(r.Context(), p, r.PathValue("id"), in.ReleaseID, RequestID(r.Context()), in.Name, in.TargetPercentage, in.DeviceIDs)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusAccepted, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListFirmwareRolloutsHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListFirmwareRollouts(r.Context(), p, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func DeviceFirmwareDownloadHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
id, ok := deviceauth.FromContext(r.Context())
|
||
|
|
if !ok {
|
||
|
|
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
url, until, err := service.FirmwareDownloadForDevice(r.Context(), id, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
WriteError(w, r, http.StatusForbidden, "FIRMWARE_DOWNLOAD_DENIED", "设备不在该固件批次中或版本不可下载")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"download_url": url, "download_until": until})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func GetFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.GetFirmwareRollout(r.Context(), p, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
func PauseFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.PauseFirmwareRollout(r.Context(), p, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
func RollbackFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
p, ok := resourcePrincipal(w, r, "id")
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var in struct {
|
||
|
|
ReleaseID string `json:"rollback_release_id"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &in, "固件回滚参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
in.ReleaseID = strings.TrimSpace(in.ReleaseID)
|
||
|
|
if in.ReleaseID == "" {
|
||
|
|
writeValidation(w, r, "固件回滚参数不正确", FieldError{Field: "rollback_release_id", Message: "必须选择已验证的回滚版本"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.StartFirmwareRollback(r.Context(), p, r.PathValue("id"), in.ReleaseID, RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusAccepted, item)
|
||
|
|
})
|
||
|
|
}
|