package handlers import ( "context" "encoding/json" "fmt" "net/http" "github.com/go-chi/chi/v5" "git.eeqj.de/sneak/upaas/internal/database" "git.eeqj.de/sneak/upaas/internal/models" ) // apiTokenResponse is the JSON representation of an API token. type apiTokenResponse struct { ID string `json:"id"` Name string `json:"name"` CreatedAt string `json:"createdAt"` ExpiresAt *string `json:"expiresAt,omitempty"` LastUsedAt *string `json:"lastUsedAt,omitempty"` } // apiTokenCreateResponse includes the plaintext token (shown once). type apiTokenCreateResponse struct { apiTokenResponse Token string `json:"token"` } func tokenToAPI(t *models.APIToken) apiTokenResponse { resp := apiTokenResponse{ ID: t.ID, Name: t.Name, CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z"), } if t.ExpiresAt.Valid { s := t.ExpiresAt.Time.Format("2006-01-02T15:04:05Z") resp.ExpiresAt = &s } if t.LastUsedAt.Valid { s := t.LastUsedAt.Time.Format("2006-01-02T15:04:05Z") resp.LastUsedAt = &s } return resp } // createTokenRequest is the JSON body for token creation. type createTokenRequest struct { Name string `json:"name"` } // createAndSaveToken generates a token, saves it, and returns // the plaintext and model. func (h *Handlers) createAndSaveToken( ctx context.Context, userID int64, name string, ) (string, *models.APIToken, error) { plaintext, err := models.GenerateToken() if err != nil { return "", nil, fmt.Errorf("generating: %w", err) } token := models.NewAPIToken(h.db) token.UserID = userID token.Name = name token.TokenHash = database.HashAPIToken(plaintext) saveErr := token.Save(ctx) if saveErr != nil { return "", nil, fmt.Errorf("saving: %w", saveErr) } return plaintext, token, nil } // HandleAPICreateToken returns a handler that creates an API token. func (h *Handlers) HandleAPICreateToken() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { user, err := h.auth.GetCurrentUser( request.Context(), request, ) if err != nil || 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 { h.respondJSON(writer, request, map[string]string{"error": "invalid JSON body"}, http.StatusBadRequest) return } if req.Name == "" { h.respondJSON(writer, request, map[string]string{"error": "name is required"}, http.StatusBadRequest) return } plaintext, token, createErr := h.createAndSaveToken( request.Context(), user.ID, req.Name, ) if createErr != nil { h.log.Error("api: token creation failed", "error", createErr) h.respondJSON(writer, request, map[string]string{"error": "internal error"}, http.StatusInternalServerError) return } h.respondJSON(writer, request, apiTokenCreateResponse{ apiTokenResponse: tokenToAPI(token), Token: plaintext, }, http.StatusCreated) } } // HandleAPIListTokens returns a handler that lists API tokens. func (h *Handlers) HandleAPIListTokens() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { user, err := h.auth.GetCurrentUser( request.Context(), request, ) if err != nil || user == nil { h.respondJSON(writer, request, map[string]string{"error": "unauthorized"}, http.StatusUnauthorized) return } tokens, listErr := models.ListAPITokensByUser( request.Context(), h.db, user.ID, ) if listErr != nil { h.log.Error("api: failed to list tokens", "error", listErr) h.respondJSON(writer, request, map[string]string{"error": "internal error"}, http.StatusInternalServerError) return } result := make([]apiTokenResponse, 0, len(tokens)) for _, t := range tokens { result = append(result, tokenToAPI(t)) } h.respondJSON(writer, request, result, http.StatusOK) } } // HandleAPIDeleteToken returns a handler that revokes an API token. func (h *Handlers) HandleAPIDeleteToken() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { user, err := h.auth.GetCurrentUser( request.Context(), request, ) if err != nil || user == nil { h.respondJSON(writer, request, map[string]string{"error": "unauthorized"}, http.StatusUnauthorized) return } tokenID := chi.URLParam(request, "tokenID") token, findErr := models.FindAPIToken( request.Context(), h.db, tokenID, ) if findErr != nil { h.respondJSON(writer, request, map[string]string{"error": "internal error"}, http.StatusInternalServerError) return } if token == nil || token.UserID != user.ID { h.respondJSON(writer, request, map[string]string{"error": "token not found"}, http.StatusNotFound) return } deleteErr := token.Delete(request.Context()) if deleteErr != nil { h.log.Error("api: failed to delete token", "error", deleteErr) h.respondJSON(writer, request, map[string]string{"error": "internal error"}, http.StatusInternalServerError) return } h.respondJSON(writer, request, map[string]string{"status": "deleted"}, http.StatusOK) } }