P1-27: Add host flow — form + minted-token result page
GET /hosts/new renders the focused two-column form (hostname,
tags, repo URL/username/password). POST /hosts/new validates,
mints a one-time token via the new mintEnrollmentToken helper —
shared with the existing JSON /api/enrollment-tokens endpoint —
and re-renders the same page in result state showing:
- the install command with RM_SERVER + RM_TOKEN filled in (and
an inline copy-to-clipboard button),
- an "awaiting agent connection" panel with the hostname
pre-filled,
- a troubleshooting list pointing at the most common reasons
the agent doesn't appear,
- back-to-dashboard / add-another-host links.
publicURL() resolves RM_BASE_URL first, falling back to scheme +
Host on the inbound request — useful for local smoke without a
proxy.
Browser-verified end-to-end: form submit → token minted → install
command renders with the right values from the form input.
template fn formatRelTime now accepts time.Time *or* *time.Time
so templates can pass either without fighting Go's lack of an
address-of operator.
Deferred: download-preconfigured-installer (a templated .sh with
the values baked in) — copy-paste covers v1; nice-to-have later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -195,37 +195,51 @@ func (s *Server) handleCreateEnrollmentToken(w stdhttp.ResponseWriter, r *stdhtt
|
||||
writeJSONError(w, stdhttp.StatusBadRequest, "invalid_json", err.Error())
|
||||
return
|
||||
}
|
||||
if req.RepoURL == "" || req.RepoPassword == "" {
|
||||
token, expiresAt, err := s.mintEnrollmentToken(r.Context(), req.RepoURL, req.RepoUsername, req.RepoPassword)
|
||||
switch err {
|
||||
case nil:
|
||||
writeJSON(w, stdhttp.StatusCreated, enrollOperatorResponse{Token: token, ExpiresAt: expiresAt})
|
||||
case errMissingRepoCreds:
|
||||
writeJSONError(w, stdhttp.StatusBadRequest, "missing_field",
|
||||
"repo_url and repo_password are required so the agent can run backups on first connect")
|
||||
return
|
||||
default:
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
||||
}
|
||||
}
|
||||
|
||||
// errMissingRepoCreds is returned by mintEnrollmentToken when the
|
||||
// operator hasn't supplied the URL+password pair the agent needs.
|
||||
// Sentinel error so HTML and JSON handlers can map it to their own
|
||||
// surface (form re-render with banner / 400 with code).
|
||||
var errMissingRepoCreds = errAuth("missing_repo_creds")
|
||||
|
||||
// mintEnrollmentToken creates a fresh one-time enrollment token and
|
||||
// stashes the AEAD-encrypted repo creds on its row. Returns the raw
|
||||
// token (shown to the operator exactly once) and the expiry time.
|
||||
//
|
||||
// Shared by the JSON endpoint and the HTML "Add host" flow.
|
||||
func (s *Server) mintEnrollmentToken(ctx context.Context, repoURL, repoUsername, repoPassword string) (string, time.Time, error) {
|
||||
if repoURL == "" || repoPassword == "" {
|
||||
return "", time.Time{}, errMissingRepoCreds
|
||||
}
|
||||
token, err := auth.NewToken()
|
||||
if err != nil {
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
||||
return
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
tokHash := auth.HashToken(token)
|
||||
|
||||
enc, err := s.encryptRepoCreds(repoCredsBlob{
|
||||
RepoURL: req.RepoURL, RepoUsername: req.RepoUsername, RepoPassword: req.RepoPassword,
|
||||
RepoURL: repoURL, RepoUsername: repoUsername, RepoPassword: repoPassword,
|
||||
}, []byte("token:"+tokHash))
|
||||
if err != nil {
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
||||
return
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
const ttl = time.Hour
|
||||
if err := s.deps.Store.CreateEnrollmentToken(r.Context(), tokHash, ttl, enc); err != nil {
|
||||
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
||||
return
|
||||
if err := s.deps.Store.CreateEnrollmentToken(ctx, tokHash, ttl, enc); err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
writeJSON(w, stdhttp.StatusCreated, enrollOperatorResponse{
|
||||
Token: token,
|
||||
ExpiresAt: time.Now().Add(ttl).UTC(),
|
||||
})
|
||||
return token, time.Now().Add(ttl).UTC(), nil
|
||||
}
|
||||
|
||||
// rebindTokenCreds decrypts the creds attached to the token (if any),
|
||||
|
||||
Reference in New Issue
Block a user