package main import ( "encoding/json" "os" "sort" "time" ) // Client is one entry in the JSON registry. The field tags reproduce the exact // object shape the bash/jq version wrote, so registries are interchangeable. type Client struct { ID int64 `json:"id"` Name string `json:"name"` IP string `json:"ip"` PublicKey string `json:"public_key"` PrivateKey string `json:"private_key"` PSKKey string `json:"psk_key"` IsEnabled string `json:"is_enabled"` CreatedAt int64 `json:"created_at"` // Comment is a free-form note attached to the profile via the web UI. It is // registry-only (never written into the .conf) and omitted when empty so // registries stay byte-compatible with the bash/jq version until a note is set. Comment string `json:"comment,omitempty"` } // initStorage creates the data/client directories and an empty registry, // validating any existing registry JSON (bash: jq empty). Both directories // hold secret material (the registry's private_key field, client .conf/.png // files) so they are created owner-only. func initStorage() { if err := os.MkdirAll(dataDir, 0700); err != nil { die("Failed to create data dir: %v", err) } if err := os.MkdirAll(clientDir, 0700); err != nil { die("Failed to create client dir: %v", err) } if _, err := os.Stat(registryFile); os.IsNotExist(err) { if err := os.WriteFile(registryFile, []byte("[]"), 0644); err != nil { die("Failed to initialise registry: %v", err) } } // Validate JSON (equivalent to `jq empty`). loadRegistry() } // loadRegistry reads and parses the registry, aborting on invalid JSON. func loadRegistry() []Client { data, err := os.ReadFile(registryFile) if err != nil { die("Failed to read registry: %v", err) } var clients []Client if err := json.Unmarshal(data, &clients); err != nil { die("Registry JSON is invalid") } return clients } // saveRegistry writes the registry atomically (temp file + rename), matching // the bash mktemp/mv pattern, then re-validates it. func saveRegistry(clients []Client) { data, err := json.MarshalIndent(clients, "", " ") if err != nil { die("Failed to update registry: %v", err) } tmp, err := os.CreateTemp(dataDir, "registry-*.tmp") if err != nil { die("Failed to update registry: %v", err) } tmpName := tmp.Name() if _, err := tmp.Write(data); err != nil { tmp.Close() os.Remove(tmpName) die("Failed to update registry: %v", err) } tmp.Close() if err := os.Rename(tmpName, registryFile); err != nil { os.Remove(tmpName) die("Failed to update registry: %v", err) } // Re-validate (bash: jq empty after write). loadRegistry() } // getNextID returns max(id)+1, or 1 for an empty registry. func getNextID(clients []Client) int64 { if len(clients) == 0 { return 1 } var max int64 for _, c := range clients { if c.ID > max { max = c.ID } } return max + 1 } // getLastIP returns the IP of the highest-id client (empty registry → ""). func getLastIP(clients []Client) string { if len(clients) == 0 { return "" } sorted := append([]Client(nil), clients...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].ID < sorted[j].ID }) return sorted[len(sorted)-1].IP } // findClient returns a pointer to the client with the given id, or nil. func findClient(clients []Client, id int64) *Client { for i := range clients { if clients[i].ID == id { return &clients[i] } } return nil } // nowUnix returns the current Unix timestamp (bash: now|floor). func nowUnix() int64 { return time.Now().Unix() }