mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(api): add apikey support (#408)
* feat(api): add apikey support * feat(web): api settings crud
This commit is contained in:
parent
9c036033e9
commit
fa20978d58
31 changed files with 834 additions and 70 deletions
|
@ -8,7 +8,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type actionService interface {
|
||||
|
|
78
internal/http/apikey.go
Normal file
78
internal/http/apikey.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
type apikeyService interface {
|
||||
List(ctx context.Context) ([]domain.APIKey, error)
|
||||
Store(ctx context.Context, key *domain.APIKey) error
|
||||
Update(ctx context.Context, key *domain.APIKey) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
ValidateAPIKey(ctx context.Context, token string) bool
|
||||
}
|
||||
|
||||
type apikeyHandler struct {
|
||||
encoder encoder
|
||||
service apikeyService
|
||||
}
|
||||
|
||||
func newAPIKeyHandler(encoder encoder, service apikeyService) *apikeyHandler {
|
||||
return &apikeyHandler{
|
||||
encoder: encoder,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h apikeyHandler) Routes(r chi.Router) {
|
||||
r.Get("/", h.list)
|
||||
r.Post("/", h.store)
|
||||
r.Delete("/{apikey}", h.delete)
|
||||
}
|
||||
|
||||
func (h apikeyHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||
keys, err := h.service.List(r.Context())
|
||||
if err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, r, keys)
|
||||
}
|
||||
|
||||
func (h apikeyHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var (
|
||||
ctx = r.Context()
|
||||
data domain.APIKey
|
||||
)
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
// encode error
|
||||
h.encoder.StatusInternalError(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Store(ctx, &data); err != nil {
|
||||
// encode error
|
||||
h.encoder.StatusInternalError(w)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.StatusResponse(ctx, w, data, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h apikeyHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.service.Delete(r.Context(), chi.URLParam(r, "apikey")); err != nil {
|
||||
h.encoder.StatusInternalError(w)
|
||||
return
|
||||
}
|
||||
h.encoder.NoContent(w)
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
|
|
@ -3,7 +3,7 @@ package http
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type configJson struct {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ type errorResponse struct {
|
|||
|
||||
func (e encoder) StatusResponse(ctx context.Context, w http.ResponseWriter, response interface{}, status int) {
|
||||
if response != nil {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf=8")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
@ -30,6 +30,15 @@ func (e encoder) StatusCreated(w http.ResponseWriter) {
|
|||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (e encoder) StatusCreatedData(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) NoContent(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
@ -47,7 +56,7 @@ func (e encoder) Error(w http.ResponseWriter, err error) {
|
|||
Message: err.Error(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf=8")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type feedService interface {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/autobrr/autobrr/internal/database"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type healthHandler struct {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type indexerService interface {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
)
|
||||
|
|
|
@ -4,14 +4,30 @@ import "net/http"
|
|||
|
||||
func (s Server) IsAuthenticated(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// check session
|
||||
session, _ := s.cookieStore.Get(r, "user_session")
|
||||
if token := r.Header.Get("X-API-Token"); token != "" {
|
||||
// check header
|
||||
if !s.apiService.ValidateAPIKey(r.Context(), token) {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
} else if key := r.URL.Query().Get("apikey"); key != "" {
|
||||
// check query param lke ?apikey=TOKEN
|
||||
if !s.apiService.ValidateAPIKey(r.Context(), key) {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// check session
|
||||
session, _ := s.cookieStore.Get(r, "user_session")
|
||||
|
||||
// Check if user is authenticated
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type notificationService interface {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type releaseService interface {
|
||||
|
|
|
@ -10,7 +10,8 @@ import (
|
|||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/web"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/r3labs/sse/v2"
|
||||
"github.com/rs/cors"
|
||||
|
@ -28,6 +29,7 @@ type Server struct {
|
|||
date string
|
||||
|
||||
actionService actionService
|
||||
apiService apikeyService
|
||||
authService authService
|
||||
downloadClientService downloadClientService
|
||||
filterService filterService
|
||||
|
@ -38,7 +40,7 @@ type Server struct {
|
|||
releaseService releaseService
|
||||
}
|
||||
|
||||
func NewServer(config *domain.Config, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, releaseSvc releaseService) Server {
|
||||
func NewServer(config *domain.Config, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, releaseSvc releaseService) Server {
|
||||
return Server{
|
||||
config: config,
|
||||
sse: sse,
|
||||
|
@ -50,6 +52,7 @@ func NewServer(config *domain.Config, sse *sse.Server, db *database.DB, version
|
|||
cookieStore: sessions.NewCookieStore([]byte(config.SessionSecret)),
|
||||
|
||||
actionService: actionService,
|
||||
apiService: apiService,
|
||||
authService: authService,
|
||||
downloadClientService: downloadClientSvc,
|
||||
filterService: filterSvc,
|
||||
|
@ -78,6 +81,10 @@ func (s Server) Open() error {
|
|||
func (s Server) Handler() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowCredentials: true,
|
||||
AllowedMethods: []string{"HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||
|
@ -116,6 +123,7 @@ func (s Server) Handler() http.Handler {
|
|||
r.Route("/feeds", newFeedHandler(encoder, s.feedService).Routes)
|
||||
r.Route("/irc", newIrcHandler(encoder, s.ircService).Routes)
|
||||
r.Route("/indexer", newIndexerHandler(encoder, s.indexerService, s.ircService).Routes)
|
||||
r.Route("/keys", newAPIKeyHandler(encoder, s.apiService).Routes)
|
||||
r.Route("/notification", newNotificationHandler(encoder, s.notificationService).Routes)
|
||||
r.Route("/release", newReleaseHandler(encoder, s.releaseService).Routes)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue