feat: v1 API + frontend migration
Some checks failed
continuous-integration/drone/pr Build is failing
Some checks failed
continuous-integration/drone/pr Build is failing
This commit is contained in:
140
api/v1/server.go
Normal file
140
api/v1/server.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/database"
|
||||
)
|
||||
|
||||
var _ StrictServerInterface = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
db *database.DBManager
|
||||
cfg *config.Config
|
||||
assets fs.FS
|
||||
}
|
||||
|
||||
// NewServer creates a new native HTTP server
|
||||
func NewServer(db *database.DBManager, cfg *config.Config, assets fs.FS) *Server {
|
||||
s := &Server{
|
||||
mux: http.NewServeMux(),
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
assets: assets,
|
||||
}
|
||||
|
||||
if cfg.DisableAuth {
|
||||
log.Warn("DISABLE_AUTH is set — all API requests will bypass authentication")
|
||||
}
|
||||
|
||||
// Create strict handler with authentication middleware
|
||||
strictHandler := NewStrictHandler(s, []StrictMiddlewareFunc{s.authMiddleware})
|
||||
|
||||
s.mux = HandlerFromMuxWithBaseURL(strictHandler, s.mux, "/api/v1").(*http.ServeMux)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// authMiddleware adds authentication context to requests
|
||||
func (s *Server) authMiddleware(handler StrictHandlerFunc, operationID string) StrictHandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) {
|
||||
// Store request and response in context for all handlers
|
||||
ctx = context.WithValue(ctx, "request", r)
|
||||
ctx = context.WithValue(ctx, "response", w)
|
||||
|
||||
// Skip auth for public auth and info endpoints - cover and file require auth via cookies
|
||||
if operationID == "Login" || operationID == "Register" || operationID == "GetInfo" {
|
||||
return handler(ctx, w, r, request)
|
||||
}
|
||||
|
||||
// Dev Auth Bypass - Inject an admin session when DISABLE_AUTH is set.
|
||||
// This avoids repeated logins during local development. Uses the
|
||||
// first user in the database so that DB queries using the user ID
|
||||
// return real data.
|
||||
if s.cfg.DisableAuth {
|
||||
devAuth, ok := s.resolveDevAuth(ctx)
|
||||
if !ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Code: 500, Message: "DISABLE_AUTH: no users in database; register one first"})
|
||||
return nil, nil
|
||||
}
|
||||
ctx = context.WithValue(ctx, "auth", devAuth)
|
||||
return handler(ctx, w, r, request)
|
||||
}
|
||||
|
||||
auth, ok := s.getSession(r)
|
||||
if !ok {
|
||||
// Write 401 response directly
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(401)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Code: 401, Message: "Unauthorized"})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check admin status for admin-only endpoints
|
||||
adminEndpoints := []string{
|
||||
"GetAdmin",
|
||||
"PostAdminAction",
|
||||
"GetUsers",
|
||||
"UpdateUser",
|
||||
"GetImportDirectory",
|
||||
"PostImport",
|
||||
"GetImportResults",
|
||||
"GetLogs",
|
||||
}
|
||||
|
||||
for _, adminEndpoint := range adminEndpoints {
|
||||
if operationID == adminEndpoint && !auth.IsAdmin {
|
||||
// Write 403 response directly
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(403)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Code: 403, Message: "Admin privileges required"})
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Store auth in context for handlers to access
|
||||
ctx = context.WithValue(ctx, "auth", auth)
|
||||
|
||||
return handler(ctx, w, r, request)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveDevAuth determines the dev user identity when DISABLE_AUTH is set.
|
||||
// If DISABLE_AUTH_USER is specified, that user is looked up; otherwise the
|
||||
// first user in the database is used.
|
||||
func (s *Server) resolveDevAuth(ctx context.Context) (authData, bool) {
|
||||
if s.cfg.DisableAuthUser != "" {
|
||||
user, err := s.db.Queries.GetUser(ctx, s.cfg.DisableAuthUser)
|
||||
if err != nil {
|
||||
log.Errorf("DISABLE_AUTH_USER=%q not found in database: %v", s.cfg.DisableAuthUser, err)
|
||||
return authData{}, false
|
||||
}
|
||||
return authData{UserName: user.ID, IsAdmin: user.Admin}, true
|
||||
}
|
||||
|
||||
users, err := s.db.Queries.GetUsers(ctx)
|
||||
if err != nil || len(users) == 0 {
|
||||
return authData{}, false
|
||||
}
|
||||
return authData{UserName: users[0].ID, IsAdmin: users[0].Admin}, true
|
||||
}
|
||||
|
||||
// GetInfo returns server information
|
||||
func (s *Server) GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) {
|
||||
return GetInfo200JSONResponse{
|
||||
Version: s.cfg.Version,
|
||||
SearchEnabled: s.cfg.SearchEnabled,
|
||||
RegistrationEnabled: s.cfg.RegistrationEnabled,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user