refactor: switch API from token auth to cookie-based session auth

- Remove API token system entirely (model, migration, middleware)
- Add migration 007 to drop api_tokens table
- Add POST /api/v1/login endpoint for JSON credential auth
- API routes now use session cookies (same as web UI)
- Remove /api/v1/tokens endpoint
- HandleAPIWhoAmI uses session auth instead of token context
- APISessionAuth middleware returns JSON 401 instead of redirect
- Update all API tests to use cookie-based authentication

Addresses review comment on PR #74.
This commit is contained in:
user
2026-02-16 00:31:10 -08:00
parent 0536f57ec2
commit 9ac1d25788
7 changed files with 221 additions and 407 deletions

View File

@@ -7,7 +7,6 @@ import (
"github.com/go-chi/chi/v5"
"git.eeqj.de/sneak/upaas/internal/middleware"
"git.eeqj.de/sneak/upaas/internal/models"
"git.eeqj.de/sneak/upaas/internal/service/app"
)
@@ -72,6 +71,65 @@ func deploymentToAPI(d *models.Deployment) apiDeploymentResponse {
return resp
}
// HandleAPILoginPOST returns a handler that authenticates via JSON credentials
// and sets a session cookie.
func (h *Handlers) HandleAPILoginPOST() http.HandlerFunc {
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type loginResponse struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
}
return func(writer http.ResponseWriter, request *http.Request) {
var req loginRequest
decodeErr := json.NewDecoder(request.Body).Decode(&req)
if decodeErr != nil {
h.respondJSON(writer, request,
map[string]string{"error": "invalid JSON body"},
http.StatusBadRequest)
return
}
if req.Username == "" || req.Password == "" {
h.respondJSON(writer, request,
map[string]string{"error": "username and password are required"},
http.StatusBadRequest)
return
}
user, authErr := h.auth.Authenticate(request.Context(), req.Username, req.Password)
if authErr != nil {
h.respondJSON(writer, request,
map[string]string{"error": "invalid credentials"},
http.StatusUnauthorized)
return
}
sessionErr := h.auth.CreateSession(writer, request, user)
if sessionErr != nil {
h.log.Error("api: failed to create session", "error", sessionErr)
h.respondJSON(writer, request,
map[string]string{"error": "failed to create session"},
http.StatusInternalServerError)
return
}
h.respondJSON(writer, request, loginResponse{
UserID: user.ID,
Username: user.Username,
}, http.StatusOK)
}
}
// HandleAPIListApps returns a handler that lists all apps as JSON.
func (h *Handlers) HandleAPIListApps() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
@@ -294,59 +352,6 @@ func (h *Handlers) HandleAPITriggerDeploy() http.HandlerFunc {
}
}
// HandleAPICreateToken returns a handler that creates an API token.
func (h *Handlers) HandleAPICreateToken() http.HandlerFunc {
type createTokenRequest struct {
Name string `json:"name"`
}
type createTokenResponse struct {
Token string `json:"token"`
Name string `json:"name"`
ID int64 `json:"id"`
}
return func(writer http.ResponseWriter, request *http.Request) {
user := middleware.APIUserFromContext(request.Context())
if user == nil {
h.respondJSON(writer, request,
map[string]string{"error": "unauthorized"},
http.StatusUnauthorized)
return
}
var req createTokenRequest
decodeErr := json.NewDecoder(request.Body).Decode(&req)
if decodeErr != nil {
req.Name = "default"
}
if req.Name == "" {
req.Name = "default"
}
rawToken, token, err := models.GenerateAPIToken(
request.Context(), h.db, user.ID, req.Name,
)
if err != nil {
h.log.Error("api: failed to create token", "error", err)
h.respondJSON(writer, request,
map[string]string{"error": "failed to create token"},
http.StatusInternalServerError)
return
}
h.respondJSON(writer, request, createTokenResponse{
Token: rawToken,
Name: token.Name,
ID: token.ID,
}, http.StatusCreated)
}
}
// HandleAPIWhoAmI returns a handler that shows the current authenticated user.
func (h *Handlers) HandleAPIWhoAmI() http.HandlerFunc {
type whoAmIResponse struct {
@@ -355,8 +360,8 @@ func (h *Handlers) HandleAPIWhoAmI() http.HandlerFunc {
}
return func(writer http.ResponseWriter, request *http.Request) {
user := middleware.APIUserFromContext(request.Context())
if user == nil {
user, err := h.auth.GetCurrentUser(request.Context(), request)
if err != nil || user == nil {
h.respondJSON(writer, request,
map[string]string{"error": "unauthorized"},
http.StatusUnauthorized)