refactor: move auth token to HttpOnly cookie, fix CORS and artefacts
Backend: - Set JWT as HttpOnly cookie on login, add cookie fallback in auth middleware - Add /auth/logout endpoint to clear cookie - Fix CORS to echo origin with Allow-Credentials for withCredentials requests - Make SPA static serving optional via SERVE_SPA config flag Frontend: - Remove manual token management; rely on HttpOnly cookie + withCredentials - Simplify auth-slice to authenticated boolean flag - Fix Artefact interface to match backend fields (sha256Hash, fileName, fileSize) - Fix artefact upload endpoint and send version in FormData - Use KiB/MiB for file size display - Move Redux hooks from store/hooks to hooks/store
This commit is contained in:
@@ -28,5 +28,16 @@ func (h *Handler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
// Set JWT as HttpOnly cookie so the frontend doesn't need to manage it.
|
||||
// The browser sends it automatically on every request.
|
||||
maxAge := h.svc.ExpiryHrs() * 3600
|
||||
c.SetCookie("pipely_token", resp.Token, maxAge, "/", "", false, true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
|
||||
}
|
||||
|
||||
func (h *Handler) Logout(c *gin.Context) {
|
||||
// Clear the auth cookie by setting MaxAge=-1
|
||||
c.SetCookie("pipely_token", "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ func NewService(db *gorm.DB, secret []byte, expiryHrs int) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ExpiryHrs() int {
|
||||
return int(s.expiry.Hours())
|
||||
}
|
||||
|
||||
func (s *Service) Login(username, password string) (*model.LoginResponse, error) {
|
||||
user, err := database.GetAdminUserByUsername(s.db, username)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ type Config struct {
|
||||
MaxConcurrent int
|
||||
BandwidthBPS int
|
||||
RateLimitRPS int
|
||||
ServeSPA bool
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
@@ -34,6 +35,7 @@ func Load() *Config {
|
||||
MaxConcurrent: envOrDefaultInt("MAX_CONCURRENT_DOWNLOADS", 50),
|
||||
BandwidthBPS: envOrDefaultInt("BANDWIDTH_BPS", 10*1024*1024/50),
|
||||
RateLimitRPS: envOrDefaultInt("RATE_LIMIT_RPS", 100),
|
||||
ServeSPA: envOrDefaultBool("SERVE_SPA", true),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +54,12 @@ func envOrDefaultInt(key string, def int) int {
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envOrDefaultBool(key string, def bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
+22
-10
@@ -10,19 +10,13 @@ import (
|
||||
|
||||
func AuthMiddleware(authSvc *auth.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorisation header is missing"})
|
||||
token := extractToken(c)
|
||||
if token == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorisation token is missing"})
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if !(len(parts) == 2 && parts[0] == "Bearer") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorisation format"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := authSvc.ValidateToken(parts[1])
|
||||
user, err := authSvc.ValidateToken(token)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorised: " + err.Error()})
|
||||
return
|
||||
@@ -32,3 +26,21 @@ func AuthMiddleware(authSvc *auth.Service) gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func extractToken(c *gin.Context) string {
|
||||
// First try the Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the cookie set at login
|
||||
if cookie, err := c.Cookie("pipely_token"); err == nil {
|
||||
return cookie
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user