feat(owner): 扫码绑定确认时自动把主人id登记进owner.yaml白名单(热生效)
This commit is contained in:
1 parent
0aa387f83c
commit
5a7ee9d772
7 files changed
+487
-65
No files matched your search
@@ -0,0 +1,91 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// SaveOwnerProfile writes p to path as YAML, atomically (temp file + rename) with
|
||||
// mode 0600 and lenient parent-directory creation, matching the runtime config
|
||||
// convention (SaveChannels). The whole struct is marshaled, so curated fields
|
||||
// (notes, identityFile, displayName, ...) survive a round-trip. YAML comments in
|
||||
// the existing file are not part of the struct and are dropped by the encode,
|
||||
// consistent with the project's other config writers.
|
||||
func SaveOwnerProfile(path string, p *OwnerProfile) error {
|
||||
if p == nil {
|
||||
return fmt.Errorf("user: save owner profile: empty profile")
|
||||
}
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return fmt.Errorf("user: save owner profile: empty path")
|
||||
}
|
||||
data, err := yaml.Marshal(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user: encode %s: %w", path, err)
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("user: create %s: %w", dir, err)
|
||||
}
|
||||
tmp, err := os.CreateTemp(dir, ".owner-*.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("user: temp file in %s: %w", dir, err)
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
closed := false
|
||||
defer func() {
|
||||
_ = tmp.Close()
|
||||
if !closed {
|
||||
_ = os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
// CreateTemp opens with 0600; chmod explicitly so the on-disk file never
|
||||
// carries a broader mode even if the umask changes.
|
||||
if err := tmp.Chmod(0o600); err != nil {
|
||||
return fmt.Errorf("user: chmod %s: %w", tmpName, err)
|
||||
}
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
return fmt.Errorf("user: write %s: %w", tmpName, err)
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
return fmt.Errorf("user: sync %s: %w", tmpName, err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("user: close %s: %w", tmpName, err)
|
||||
}
|
||||
closed = true
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
return fmt.Errorf("user: rename to %s: %w", path, err)
|
||||
}
|
||||
// Best-effort dir fsync so the rename is durable across a crash.
|
||||
if d, derr := os.Open(dir); derr == nil {
|
||||
_ = d.Sync()
|
||||
_ = d.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterOwnerAccount loads the owner whitelist at path, appends (platform,
|
||||
// accountID) when it is not already present (idempotent), and writes the updated
|
||||
// profile back. It returns an error when the profile cannot be loaded or saved.
|
||||
// The on-disk replace is atomic, so a concurrent reader never sees a partial
|
||||
// file. Hot-reload of a running owner profile is the caller's concern (see
|
||||
// cmd/cyrene's ownerRegistrar), which applies RegisterAccount to the live profile
|
||||
// the channel bridge already holds.
|
||||
func RegisterOwnerAccount(path, platform, accountID string) error {
|
||||
if strings.TrimSpace(platform) == "" {
|
||||
return fmt.Errorf("user: register owner account: empty platform")
|
||||
}
|
||||
if strings.TrimSpace(accountID) == "" {
|
||||
return fmt.Errorf("user: register owner account: empty account id")
|
||||
}
|
||||
p, err := LoadOwnerProfile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user: load %s: %w", path, err)
|
||||
}
|
||||
p.RegisterAccount(platform, accountID)
|
||||
return SaveOwnerProfile(path, p)
|
||||
}
|
||||
Reference in new issue
Block a user