package apikey import ( "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/claims" validator "RenewCMS/api/pkg/validator" domain "RenewCMS/internal/domain/apikey" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "errors" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" ) type Handler struct { CreateUseCase *useCases.CreateAPIKeyUseCase ListUseCase *useCases.ListAPIKeysUseCase RevokeUseCase *useCases.RevokeAPIKeyUseCase } type CreateAPIKeyRequest struct { Name string `json:"name" validate:"required,max=60"` ExpiresAt *time.Time `json:"expires_at"` } type createdKeyResponse struct { ID uint32 `json:"id"` Name string `json:"name"` Prefix string `json:"prefix"` Key string `json:"key"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at"` } type keyListItem struct { ID uint32 `json:"id"` UserID uint32 `json:"user_id"` Name string `json:"name"` Prefix string `json:"prefix"` Status domain.Status `json:"status"` CreatedAt time.Time `json:"created_at"` LastUsedAt *time.Time `json:"last_used_at"` ExpiresAt *time.Time `json:"expires_at"` RevokedAt *time.Time `json:"revoked_at"` CreatedByName *string `json:"created_by_name,omitempty"` } func NewHandler(create *useCases.CreateAPIKeyUseCase, list *useCases.ListAPIKeysUseCase, revoke *useCases.RevokeAPIKeyUseCase) *Handler { return &Handler{CreateUseCase: create, ListUseCase: list, RevokeUseCase: revoke} } func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { var request CreateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } if err := validator.Validate.Struct(request); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } created, err := h.CreateUseCase.CreateAPIKey(r.Context(), useCases.CreateAPIKeyCommand{ UserID: userId, Name: request.Name, ExpiresAt: request.ExpiresAt, }) if err != nil { if errors.Is(err, domain.ErrExpiryNotInFuture) { http.Error(w, apperr.APIKeyExpiryErrorMessage, http.StatusBadRequest) return } http.Error(w, "Internal server error", http.StatusInternalServerError) return } body, err := json.Marshal(createdKeyResponse{ ID: created.Key.ID, Name: created.Key.Name, Prefix: created.Key.Prefix, Key: created.RawKey, CreatedAt: created.Key.CreatedAt, ExpiresAt: created.Key.ExpiresAt, }) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusCreated) _, _ = w.Write(body) } func (h *Handler) List(w http.ResponseWriter, r *http.Request) { userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } entries, err := h.ListUseCase.ListAPIKeys(userId) if err != nil { if errors.Is(err, user.ErrUserNotFound) { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } http.Error(w, "Internal server error", http.StatusInternalServerError) return } items := make([]keyListItem, 0, len(entries)) for _, entry := range entries { key := entry.Key item := keyListItem{ ID: key.ID, UserID: key.UserID, Name: key.Name, Prefix: key.Prefix, Status: entry.Status, CreatedAt: key.CreatedAt, LastUsedAt: key.LastUsedAt, ExpiresAt: key.ExpiresAt, RevokedAt: key.RevokedAt, } if entry.ListsCreator { item.CreatedByName = &entry.CreatorName } items = append(items, item) } body, err := json.Marshal(items) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") _, _ = w.Write(body) } func (h *Handler) Revoke(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return } userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } if err := h.RevokeUseCase.RevokeAPIKey(r.Context(), userId, uint32(id)); err != nil { switch { case errors.Is(err, domain.ErrAPIKeyNotFound): http.Error(w, apperr.APIKeyNotFoundErrorMessage, http.StatusNotFound) case errors.Is(err, user.ErrUserNotFound): http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } return } w.WriteHeader(http.StatusNoContent) }