improve identity handling
This commit is contained in:
@@ -50,6 +50,26 @@ type identityImportResponse struct {
|
|||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
Curve string `json:"curve"`
|
Curve string `json:"curve"`
|
||||||
CreatedAt int64 `json:"createdAt"`
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
// Created distinguishes "this key was new here" (201) from "this key was
|
||||||
|
// already in the keystore" (200). Import is the sign-in primitive for
|
||||||
|
// clients: presenting the secret key is the proof of ownership, so a
|
||||||
|
// re-import must succeed rather than conflict. Clients use the flag only
|
||||||
|
// to word the confirmation ("new profile" vs "welcome back").
|
||||||
|
Created bool `json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// identityRevealRequest asks for an existing account's private key back. The
|
||||||
|
// key was shown once at generate; without a way to see it again, a client with
|
||||||
|
// a real sign-out has no recovery path for a user who never saved it.
|
||||||
|
type identityRevealRequest struct {
|
||||||
|
Confirm bool `json:"confirm"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type identityRevealResponse struct {
|
||||||
|
UID string `json:"uid"`
|
||||||
|
Curve string `json:"curve"`
|
||||||
|
PrivateKeyHex string `json:"privateKeyHex"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type identityDeleteRequest struct {
|
type identityDeleteRequest struct {
|
||||||
@@ -172,9 +192,17 @@ func (s *Server) handleIdentityImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
dir := s.config.IdentityAccountsDir()
|
dir := s.config.IdentityAccountsDir()
|
||||||
if identity.ExistsByUID(dir, id.UID) {
|
// Idempotent: importing a key that is already in the keystore is how a
|
||||||
writeErrorSlug(w, http.StatusConflict, "identity_exists",
|
// second device signs in to an existing account. Answer 200 with the
|
||||||
"an account with this key already exists", false)
|
// stored account (preserving its original createdAt) instead of 409 —
|
||||||
|
// the caller proved ownership by presenting the key.
|
||||||
|
if existing, err := identity.LoadByUID(dir, id.UID); err == nil && existing != nil {
|
||||||
|
writeJSON(w, http.StatusOK, identityImportResponse{
|
||||||
|
UID: existing.UID,
|
||||||
|
Curve: existing.Curve,
|
||||||
|
CreatedAt: existing.CreatedAt,
|
||||||
|
Created: false,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := identity.SaveAccount(dir, id); err != nil {
|
if err := identity.SaveAccount(dir, id); err != nil {
|
||||||
@@ -185,6 +213,35 @@ func (s *Server) handleIdentityImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
UID: id.UID,
|
UID: id.UID,
|
||||||
Curve: id.Curve,
|
Curve: id.Curve,
|
||||||
CreatedAt: id.CreatedAt,
|
CreatedAt: id.CreatedAt,
|
||||||
|
Created: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleIdentityReveal returns an existing account's private key. Deliberately
|
||||||
|
// POST + explicit confirm (never a bare GET) so it cannot be triggered by a
|
||||||
|
// link or an image tag, and so it reads as the deliberate act it is. The
|
||||||
|
// endpoint is auth-gated at the perimeter like the rest of /api/identity/*.
|
||||||
|
func (s *Server) handleIdentityReveal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body identityRevealRequest
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
if !body.Confirm && r.URL.Query().Get("confirm") != "true" {
|
||||||
|
writeErrorSlug(w, http.StatusBadRequest, "confirm_required",
|
||||||
|
"revealing a private key requires {\"confirm\": true} or ?confirm=true", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uid := strings.TrimSpace(body.UID)
|
||||||
|
if uid == "" {
|
||||||
|
uid = strings.TrimSpace(r.URL.Query().Get("uid"))
|
||||||
|
}
|
||||||
|
id, status, slug, msg := s.resolveAccount(uid)
|
||||||
|
if status != 0 {
|
||||||
|
writeErrorSlug(w, status, slug, msg, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, identityRevealResponse{
|
||||||
|
UID: id.UID,
|
||||||
|
Curve: id.Curve,
|
||||||
|
PrivateKeyHex: id.PrivateKeyHex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ func (s *Server) setupRoutes() {
|
|||||||
s.router.HandleFunc("/api/identity/accounts", s.handleIdentityAccounts).Methods("GET")
|
s.router.HandleFunc("/api/identity/accounts", s.handleIdentityAccounts).Methods("GET")
|
||||||
s.router.HandleFunc("/api/identity/generate", s.handleIdentityGenerate).Methods("POST")
|
s.router.HandleFunc("/api/identity/generate", s.handleIdentityGenerate).Methods("POST")
|
||||||
s.router.HandleFunc("/api/identity/import", s.handleIdentityImport).Methods("POST")
|
s.router.HandleFunc("/api/identity/import", s.handleIdentityImport).Methods("POST")
|
||||||
|
s.router.HandleFunc("/api/identity/reveal", s.handleIdentityReveal).Methods("POST")
|
||||||
s.router.HandleFunc("/api/identity", s.handleIdentityDelete).Methods("DELETE")
|
s.router.HandleFunc("/api/identity", s.handleIdentityDelete).Methods("DELETE")
|
||||||
s.router.HandleFunc("/api/identity/sign", s.handleIdentitySign).Methods("POST")
|
s.router.HandleFunc("/api/identity/sign", s.handleIdentitySign).Methods("POST")
|
||||||
s.router.HandleFunc("/api/identity/aead-key", s.handleIdentityAEADKey).Methods("GET")
|
s.router.HandleFunc("/api/identity/aead-key", s.handleIdentityAEADKey).Methods("GET")
|
||||||
|
|||||||
Reference in New Issue
Block a user