feat: expand companion platform, device controls, and memoir workflows
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:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -0,0 +1,73 @@
package onboarding
import (
"context"
"data-terminal-cloud/internal/audit"
"errors"
"github.com/jackc/pgx/v5"
)
// ProvisionCustomerPhone is an administrative migration, deliberately absent from HTTP routes.
// The operator must establish the old identity and authorize the phone association beforehand.
// It creates only a credential mapping: no user, household, role or SMS-verification state is changed.
func (s *Service) ProvisionCustomerPhone(ctx context.Context, userID, issuer, subject, phone, password, requestID string) (bool, error) {
return s.provisionPhone(ctx, userID, issuer, subject, phone, password, requestID)
}
func (s *Service) provisionPhone(ctx context.Context, userID, issuer, subject, phone, password, requestID string) (bool, error) {
if !uuidValid(userID) || issuer == "" || subject == "" || !phonePattern.MatchString(phone) || !validNewPassword(password) || !uuidV4(requestID) {
return false, errors.New("invalid phone provisioning input")
}
phoneHash := domainMAC(s.crypt.hashKey, "PHONE1\n"+phone)
tx, err := s.pool.Begin(ctx)
if err != nil {
return false, err
}
defer tx.Rollback(ctx)
var tenant, publicID string
err = tx.QueryRow(ctx, `SELECT ua.tenant_id::text,ua.public_id::text FROM user_account ua JOIN tenant t ON t.id=ua.tenant_id AND t.status='active' WHERE ua.id=$1::uuid AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' AND `+s.accountPredicate()+` FOR SHARE OF ua,t`, userID, issuer, subject).Scan(&tenant, &publicID)
if errors.Is(err, pgx.ErrNoRows) {
return false, errors.New("expected active account identity does not match")
}
if err != nil {
return false, err
}
if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, "admin-phone:"+phoneHash); err != nil {
return false, err
}
var existingPhone, existingIssuer, existingSubject, encoded string
err = tx.QueryRow(ctx, `SELECT phone_digest,issuer,subject,COALESCE(password_hash,'') FROM `+s.phoneCredentialTable()+` WHERE phone_digest=$1 OR (issuer=$2 AND subject=$3) ORDER BY phone_digest LIMIT 1 FOR UPDATE`, phoneHash, issuer, subject).Scan(&existingPhone, &existingIssuer, &existingSubject, &encoded)
if err == nil {
if existingPhone != phoneHash || existingIssuer != issuer || existingSubject != subject {
return false, errors.New("phone or identity is already bound; no changes made")
}
ok, e := verifyPassword(ctx, encoded, password)
if e != nil {
return false, e
}
if !ok {
return false, errors.New("credential already exists; refusing to replace its password")
}
return false, tx.Commit(ctx)
}
if !errors.Is(err, pgx.ErrNoRows) {
return false, err
}
encoded, err = hashPassword(ctx, password)
if err != nil {
return false, err
}
masked := phone[:len(phone)-8] + "****" + phone[len(phone)-4:]
_, err = tx.Exec(ctx, `INSERT INTO `+s.phoneCredentialTable()+`(phone_digest,phone_masked,issuer,subject,password_hash) VALUES($1,$2,$3,$4,$5)`, phoneHash, masked, issuer, subject, encoded)
if err != nil {
return false, err
}
action := "customer.phone.provision"
if s.operatorLogin {
action = "operator.phone.provision"
}
err = s.audit.Record(ctx, tx, audit.Event{TenantID: &tenant, ActorType: "system", Action: action, ResourceType: "user_account", ResourceID: &publicID, RequestID: requestID, Purpose: "authorized account migration", Result: "success", Detail: map[string]any{"phone_masked": masked, "sms_verified": false}})
if err != nil {
return false, err
}
return true, tx.Commit(ctx)
}