Add basic authentication

This commit is contained in:
2026-05-02 14:59:05 +02:00
parent e1c6ea9e51
commit f1fd72520a
15 changed files with 369 additions and 66 deletions
+52
View File
@@ -0,0 +1,52 @@
package handlers
import (
"log"
"quay/app/repository"
"quay/internal/security"
"time"
"github.com/gofiber/fiber/v3"
)
type AuthHandler struct {
Repo repository.UserRepository
}
func NewAuthHandler(repo repository.UserRepository) *AuthHandler {
return &AuthHandler{Repo: repo}
}
type LoginRequest struct {
Name string `json:"name"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
}
func (h *AuthHandler) Login(c fiber.Ctx) error {
var req LoginRequest
if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"})
}
user, err := h.Repo.GetUserByName(req.Name)
if err != nil || user == nil {
log.Println("login: user lookup failed", err)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
}
if !security.CheckPasswordHash(req.Password, user.HashedPassword) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
}
token, err := security.GenerateToken(user.ID, user.Role, 24*time.Hour)
if err != nil {
log.Println("login: token generation failed", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate token"})
}
return c.JSON(LoginResponse{Token: token})
}
+32
View File
@@ -0,0 +1,32 @@
package middleware
import (
"strings"
"quay/internal/security"
"github.com/gofiber/fiber/v3"
)
func RequireAuth() fiber.Handler {
return func(c fiber.Ctx) error {
auth := c.Get("Authorization")
if auth == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing authorization header"})
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization header"})
}
claims, err := security.ValidateToken(parts[1])
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"})
}
c.Locals("user_id", claims.UserID)
c.Locals("role", claims.Role)
return c.Next()
}
}
+50 -31
View File
@@ -26,50 +26,69 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d
deploymentsHandler := handlers.NewDeploymentHandler(deploymentRepository)
api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost))
api.Get("/health", handlers.HealthCheck)
api.Post("/deploy", deploySiteHandler.PostDeploy)
public := api.Group("")
public.Get("/health", handlers.HealthCheck)
authHandler := handlers.NewAuthHandler(userRepository)
public.Post("/login", authHandler.Login)
// Protected routes - require auth for everything by default
protected := api.Group("", middleware.RequireAuth())
protected.Post("/deploy", deploySiteHandler.PostDeploy)
// Sites
api.Get("/sites", siteHandler.GetSites)
api.Get("/sites/:id", siteHandler.GetSite)
api.Post("/sites", siteHandler.PostSite)
api.Put("/sites/:id", siteHandler.PutSite)
api.Delete("/sites/:id", siteHandler.DeleteSite)
api.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled)
protected.Get("/sites", siteHandler.GetSites)
protected.Get("/sites/:id", siteHandler.GetSite)
protected.Post("/sites", siteHandler.PostSite)
protected.Put("/sites/:id", siteHandler.PutSite)
protected.Delete("/sites/:id", siteHandler.DeleteSite)
protected.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled)
// Forward rules
api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
api.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule)
api.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule)
api.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule)
protected.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
protected.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
protected.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule)
protected.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule)
protected.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule)
// Custom headers (header rules)
api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
api.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders)
api.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders)
api.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders)
protected.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
protected.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
protected.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders)
protected.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders)
protected.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders)
// Headers
api.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders)
api.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader)
api.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader)
api.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader)
api.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader)
protected.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders)
protected.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader)
protected.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader)
protected.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader)
protected.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader)
// Deployments
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
api.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite)
protected.Get("/deployments/:id", deploymentsHandler.GetDeployment)
protected.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite)
// Users
api.Get("/users", userHandler.GetAllUsers)
api.Get("/users/:id", userHandler.GetUserById)
api.Get("/users/by-name/:name", userHandler.GetUserByName)
api.Post("/users", userHandler.CreateUser)
api.Put("/users/:id", userHandler.UpdateUser)
api.Delete("/users/:id", userHandler.DeleteUser)
protected.Get("/users", userHandler.GetAllUsers)
protected.Get("/users/:id", userHandler.GetUserById)
protected.Get("/users/by-name/:name", userHandler.GetUserByName)
// Allow creating the very first admin user without auth (bootstrap).
// If an admin already exists, require auth to create users.
if exists, err := userRepository.AdminUserExists(); err != nil {
// if we can't determine, be conservative and require auth
protected.Post("/users", userHandler.CreateUser)
} else if !exists {
public.Post("/users", userHandler.CreateUser)
} else {
protected.Post("/users", userHandler.CreateUser)
}
protected.Put("/users/:id", userHandler.UpdateUser)
protected.Delete("/users/:id", userHandler.DeleteUser)
api.Use(func(c fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{