P1-34: e2e smoke runbook + redacted GET /repo-credentials
Adds docs/e2e-smoke.md — an ~5-minute runbook that walks the full
P1 happy path against a sibling restic/rest-server: bootstrap
admin, mint token with repo creds, enrol an agent, watch the
config.update push land, run a backup, confirm the snapshot, edit
creds and watch the second push fire. Per the design discussion
this is a runbook (not a Go integration test); the Playwright
version lands in P5-06.
GET /api/hosts/{id}/repo-credentials returns the redacted view —
{repo_url, repo_username, has_password} — so the UI can pre-fill
the edit form without ever pulling the password out of the AEAD
blob.
Marks P1-32 / P1-33 / P1-34 done in tasks.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,53 @@ import (
|
||||
|
||||
func nowUTC() time.Time { return time.Now().UTC() }
|
||||
|
||||
// hostRepoCredsView is the body of GET /api/hosts/{id}/repo-credentials.
|
||||
// Password is always redacted; the UI uses this to pre-fill an edit
|
||||
// form with the URL/username already populated.
|
||||
type hostRepoCredsView struct {
|
||||
RepoURL string `json:"repo_url"`
|
||||
RepoUsername string `json:"repo_username,omitempty"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
}
|
||||
|
||||
// handleGetHostCredentials returns a redacted view of the host's repo
|
||||
// creds for UI display. 404 if no credential has ever been set.
|
||||
func (s *Server) handleGetHostCredentials(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
if !s.authedUser(r) {
|
||||
writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorized", "")
|
||||
return
|
||||
}
|
||||
hostID := chi.URLParam(r, "id")
|
||||
if hostID == "" {
|
||||
writeJSONError(w, stdhttp.StatusBadRequest, "missing_id", "")
|
||||
return
|
||||
}
|
||||
enc, err := s.deps.Store.GetHostCredentials(r.Context(), hostID)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
writeJSONError(w, stdhttp.StatusNotFound, "not_set", "")
|
||||
return
|
||||
}
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
||||
return
|
||||
}
|
||||
plain, err := s.deps.AEAD.Decrypt(enc, []byte("host:"+hostID))
|
||||
if err != nil {
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "decrypt_failed", "")
|
||||
return
|
||||
}
|
||||
var blob repoCredsBlob
|
||||
if err := json.Unmarshal(plain, &blob); err != nil {
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
||||
return
|
||||
}
|
||||
writeJSON(w, stdhttp.StatusOK, hostRepoCredsView{
|
||||
RepoURL: blob.RepoURL,
|
||||
RepoUsername: blob.RepoUsername,
|
||||
HasPassword: blob.RepoPassword != "",
|
||||
})
|
||||
}
|
||||
|
||||
// hostRepoCredsRequest is the body of PUT /api/hosts/{id}/repo-credentials.
|
||||
// Operator can edit any subset; missing fields preserve the existing
|
||||
// value (so changing only the password doesn't require resending the URL).
|
||||
|
||||
Reference in New Issue
Block a user