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:
@@ -0,0 +1,67 @@
|
||||
package onboarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const trustedLoginLifetime = 30 * 24 * time.Hour
|
||||
|
||||
func (s *Service) loginRealm() string {
|
||||
if s.operatorLogin {
|
||||
return "operator"
|
||||
}
|
||||
return "customer"
|
||||
}
|
||||
|
||||
func (s *Service) trustedLoginCookieName() string {
|
||||
name := "platform-" + s.loginRealm() + "-trusted-device"
|
||||
if !s.options.AllowLocalHTTP {
|
||||
name = "__Host-" + name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (s *Service) trustedLoginCookie(r *http.Request) string {
|
||||
if c, err := r.Cookie(s.trustedLoginCookieName()); err == nil && validB64(c.Value, 32) {
|
||||
return c.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Only a successfully verified, single-use SMS can establish device trust.
|
||||
// Bind it to the exact credential so an administrative password reset also
|
||||
// invalidates previously trusted devices without needing a separate revocation.
|
||||
func (s *Service) rememberLoginDevice(ctx context.Context, tx pgx.Tx, phoneHash string) (string, error) {
|
||||
var encoded string
|
||||
if err := tx.QueryRow(ctx, "SELECT COALESCE(password_hash,'') FROM "+s.phoneCredentialTable()+" WHERE phone_digest=$1 FOR SHARE", phoneHash).Scan(&encoded); err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := randomToken(32)
|
||||
_, err := tx.Exec(ctx, `INSERT INTO trusted_login_device(token_hash,realm,phone_digest,credential_digest,verified_at,expires_at) VALUES($1,$2,$3,$4,$5,$6)`, digest([]byte(token)), s.loginRealm(), phoneHash, digest([]byte(encoded)), s.now(), s.now().Add(trustedLoginLifetime))
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (s *Service) requireTrustedLoginDevice(ctx context.Context, tx pgx.Tx, phoneHash, encoded, token string) error {
|
||||
trusted := false
|
||||
if validB64(token, 32) {
|
||||
err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM trusted_login_device WHERE token_hash=$1 AND realm=$2 AND phone_digest=$3 AND credential_digest=$4 AND expires_at>$5 AND revoked_at IS NULL)`, digest([]byte(token)), s.loginRealm(), phoneHash, digest([]byte(encoded)), s.now()).Scan(&trusted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !trusted {
|
||||
return fail(403, "NEW_DEVICE_SMS_REQUIRED", "首次在此设备登录,请使用手机验证码完成验证")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) updateTrustedDevicesAfterPassword(ctx context.Context, tx pgx.Tx, phoneHash, oldHash, newHash, currentToken string) error {
|
||||
// Retain only an existing, unexpired SMS verification on this browser. A
|
||||
// password change must never turn an unverified browser into a trusted one.
|
||||
_, err := tx.Exec(ctx, `UPDATE trusted_login_device SET credential_digest=CASE WHEN token_hash=$3 AND credential_digest=$4 AND expires_at>$6 THEN $5 ELSE credential_digest END,revoked_at=CASE WHEN token_hash=$3 AND credential_digest=$4 AND expires_at>$6 THEN NULL ELSE $6 END WHERE realm=$1 AND phone_digest=$2 AND revoked_at IS NULL`, s.loginRealm(), phoneHash, digest([]byte(currentToken)), digest([]byte(oldHash)), digest([]byte(newHash)), s.now())
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user