feat(gateway): invite and hard-delete REST endpoints + repo methods
Wire the bus's new account surface into the admin gateway:
- POST /api/invites, GET /api/invites: mint and list single-use registration
invites (CreateInvite/ListInvites on the Repo). The gateway pre-builds the
shareable join link (JoinURL) from a configurable end-user client base URL so
the SPA does not need to know where the client lives.
- DELETE /api/users/{pub}: hard-delete (purge) a user, distinct from the existing
revoke.
- Both backends covered: signed control-plane (cluster default) via the unibus
client's CreateInvite/ListInvites/DeleteUser, and the direct membership store
(single-node --db fallback). For the direct store, ListInvites filters to
pending (the control plane already does so server-side).
- New --join-base-url flag / UNIBUS_JOIN_BASE_URL env feeds the join link base
URL (the END-USER client, NOT the panel's own URL); surfaced on /api/me.
- Mock repo gains the same methods for UI iteration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,12 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("GET /api/users", s.handleListUsers)
|
||||
s.mux.HandleFunc("POST /api/users", s.handleAddUser)
|
||||
s.mux.HandleFunc("POST /api/users/revoke", s.handleRevokeUser)
|
||||
// Hard-delete (purge) a user by signing key — distinct from revoke.
|
||||
s.mux.HandleFunc("DELETE /api/users/{pub}", s.handleDeleteUser)
|
||||
|
||||
// Invites — the wallet-model account-creation path.
|
||||
s.mux.HandleFunc("GET /api/invites", s.handleListInvites)
|
||||
s.mux.HandleFunc("POST /api/invites", s.handleCreateInvite)
|
||||
|
||||
// Everything else is the SPA (and its assets). Registered last as the catch-all.
|
||||
s.mux.Handle("/", s.spa)
|
||||
@@ -179,6 +185,45 @@ func (s *Server) handleRevokeUser(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
pub := strings.TrimSpace(r.PathValue("pub"))
|
||||
if pub == "" {
|
||||
writeErr(w, http.StatusBadRequest, "sign_pub required")
|
||||
return
|
||||
}
|
||||
if err := s.repo.DeleteUser(r.Context(), pub); err != nil {
|
||||
writeErr(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
func (s *Server) handleListInvites(w http.ResponseWriter, r *http.Request) {
|
||||
invites, err := s.repo.ListInvites(r.Context())
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, invites)
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateInviteReq
|
||||
if !decode(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Handle) == "" {
|
||||
writeErr(w, http.StatusBadRequest, "handle required")
|
||||
return
|
||||
}
|
||||
inv, err := s.repo.CreateInvite(r.Context(), req)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, inv)
|
||||
}
|
||||
|
||||
// ---- SPA serving ----------------------------------------------------------
|
||||
|
||||
// spaHandler serves the embedded SPA. A request for an existing asset is served
|
||||
|
||||
Reference in New Issue
Block a user