Files
kawaandClaude Sonnet 5 8d20d9be84
Sync Gitea releases to GitHub / sync-releases (push) Canceled after 0s
Feature 1: Local accounts, replacing shared token
- Local accounts with bcrypt password hashing; first-run setup via POST /api/setup
- Personal API tokens (dmv_<48hex>, SHA-256 hashed at rest) for scripted access
- Server-side in-memory sessions with 32-byte secure cookie (dockmv_session, 7-day TTL, sliding renewal)
- Login rate limiting (exponential backoff 1s–30s cap) per IP
- Refuse to bind non-loopback while no account exists, unless DOCKMV_TRUST_ADDR=1 (for Docker's port mapping)
- Every account can manage every other account (no roles in v1)
- Auth middleware: public-path allowlist (/api/setup, /api/login, /api/logout, /api/me, /api/health) + session cookie check + API token (X-Auth-Token or Authorization: Bearer) check
- Frontend AuthGate gates app on GET /api/me; shows setup screen or login form or app tree as needed
- Account tab for personal token management; sign-out button in topbar
- Break: removed --token flag, DOCKMV_TOKEN env var, ?token= query param, /api/health no longer auto-responds when unauthenticated

Verified: go build/vet clean, frontend tsc+vite clean. Sandbox cannot execute binaries to test setup→login→session→token flow at runtime; recommend manual pass before merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 15:13:54 +02:00

51 lines
1.4 KiB
Go

package api
import (
"net/http"
"github.com/arescom/dockmv/internal/store"
)
// handleListTokens lists only the caller's own tokens; there is no way to see
// another account's.
func (s *Server) handleListTokens(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.tokens.List(identityFrom(r).UserID))
}
type createTokenRequest struct {
Name string `json:"name"`
}
// createTokenResponse embeds the stored token plus the plaintext secret,
// which is only ever shown this once.
type createTokenResponse struct {
store.APIToken
Token string `json:"token"`
}
func (s *Server) handleCreateToken(w http.ResponseWriter, r *http.Request) {
var req createTokenRequest
if err := decode(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "%v", err)
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
tok, plain, err := s.tokens.Create(identityFrom(r).UserID, req.Name)
if err != nil {
writeError(w, http.StatusInternalServerError, "%v", err)
return
}
writeJSON(w, http.StatusCreated, createTokenResponse{APIToken: tok, Token: plain})
}
func (s *Server) handleRevokeToken(w http.ResponseWriter, r *http.Request) {
if err := s.tokens.Revoke(identityFrom(r).UserID, r.PathValue("id")); err != nil {
writeError(w, http.StatusNotFound, "%v", err)
return
}
w.WriteHeader(http.StatusNoContent)
}