feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -2,6 +2,7 @@ package onboarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"data-terminal-cloud/internal/devicefleet"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -27,7 +28,8 @@ type Error struct {
|
||||
Code, Message, Recovery string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string { return e.Code }
|
||||
func (e *Error) Error() string { return e.Code }
|
||||
func (e *Error) HTTPStatus() int { return e.Status }
|
||||
func fail(status int, code, message string) *Error {
|
||||
return &Error{Status: status, Code: code, Message: message, Recovery: "NONE"}
|
||||
}
|
||||
@@ -40,22 +42,30 @@ type LegacyVerifier interface {
|
||||
Verify(*http.Request) (deviceauth.Identity, error)
|
||||
}
|
||||
type Options struct {
|
||||
PauseClaims bool
|
||||
TrustedProxyCIDRs []string
|
||||
Enabled bool
|
||||
MasterKey, EntryURL, ProviderMasterKey string
|
||||
AllowLocalHTTP bool
|
||||
TokenVerifier identity.TokenVerifier
|
||||
LegacyVerifier LegacyVerifier
|
||||
Registrar hardwareprovision.Registrar
|
||||
SaaSCaptchaDisabled bool
|
||||
Captcha CaptchaProvider
|
||||
PauseClaims bool
|
||||
TrustedProxyCIDRs []string
|
||||
Enabled bool
|
||||
MasterKey, EntryURL, ProviderMasterKey string
|
||||
AllowLocalHTTP bool
|
||||
TokenVerifier identity.TokenVerifier
|
||||
LegacyVerifier LegacyVerifier
|
||||
Registrar hardwareprovision.Registrar
|
||||
// Compatibility harness only; production defaults to server registration.
|
||||
LegacyDeviceRegistration bool
|
||||
ActivationEnabled bool
|
||||
InstanceID, ProductKey, ProductSecret, BotID, Transport, MinimumSDKVersion string
|
||||
SMS SMSProvider
|
||||
WeChat WeChatProvider
|
||||
WeChatAppID, WeChatAppSecret string
|
||||
SMSLoginEnabled bool
|
||||
SelfSignupTenantIDs []string
|
||||
Logger *slog.Logger
|
||||
}
|
||||
type Service struct {
|
||||
Fleet *devicefleet.Service
|
||||
operatorLogin bool
|
||||
pool *pgxpool.Pool
|
||||
redis *redis.Client
|
||||
crypt cryptor
|
||||
@@ -81,6 +91,9 @@ func NewService(pool *pgxpool.Pool, redisClient *redis.Client, options Options)
|
||||
if options.Logger == nil {
|
||||
options.Logger = slog.Default()
|
||||
}
|
||||
if options.WeChat == nil && options.WeChatAppID != "" && options.WeChatAppSecret != "" {
|
||||
options.WeChat = newWeChatClient(options.WeChatAppID, options.WeChatAppSecret)
|
||||
}
|
||||
if options.ActivationEnabled && (len(options.ProductSecret) < 16 || options.InstanceID == "" || options.ProductKey == "" || options.BotID == "") {
|
||||
return nil, errors.New("onboarding activation requires configured provider")
|
||||
}
|
||||
@@ -97,6 +110,12 @@ func NewService(pool *pgxpool.Pool, redisClient *redis.Client, options Options)
|
||||
if options.ActivationEnabled && providerCrypt.aead == nil {
|
||||
return nil, errors.New("onboarding activation requires provider credential master key")
|
||||
}
|
||||
if options.ActivationEnabled && options.Registrar == nil && !options.LegacyDeviceRegistration {
|
||||
options.Registrar, err = hardwareprovision.NewHTTPRegistrar("", options.ProductSecret, 20*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Service{pool: pool, redis: redisClient, crypt: crypt, providerCrypt: providerCrypt, options: options, now: time.Now, audit: audit.NewRepository(), outbox: outbox.NewStore(pool)}, nil
|
||||
}
|
||||
func (s *Service) Enabled() bool { return s != nil && s.options.Enabled }
|
||||
@@ -153,7 +172,7 @@ func (s *Service) state(ctx context.Context, tx pgx.Tx, d Device) (State, error)
|
||||
}
|
||||
st := State{DeviceID: d.FactoryID, PlatformDeviceID: d.PublicID, Revision: d.Revision, Generation: d.Generation, BindingState: "UNBOUND", ActivationState: "NOT_STARTED", NextAction: "WAIT_BIND"}
|
||||
var bound, pending bool
|
||||
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL),EXISTS(SELECT 1 FROM device_binding_claim WHERE device_id=$1::uuid AND status='PENDING' AND expires_at>now())`, d.ID).Scan(&bound, &pending)
|
||||
err = tx.QueryRow(ctx, `SELECT (EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL) OR EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=$1::uuid AND released_at IS NULL)),EXISTS(SELECT 1 FROM device_binding_claim WHERE device_id=$1::uuid AND status='PENDING' AND expires_at>now())`, d.ID).Scan(&bound, &pending)
|
||||
if err != nil {
|
||||
return st, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user