Traefik_Control/backend/internal/api/handlers/auth.go

172 lines
No EOL
4.4 KiB
Go

package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/traefik/traefik-gui/backend/internal/auth"
"github.com/traefik/traefik-gui/backend/internal/database/repositories"
"github.com/traefik/traefik-gui/backend/internal/models"
)
const SessionCookieName = "traefik_gui_session"
type AuthHandler struct {
userRepo *repositories.UserRepository
sessionRepo *repositories.SessionRepository
sessionSecret string
cookieDomain string
cookieSecure bool
}
func NewAuthHandler(
userRepo *repositories.UserRepository,
sessionRepo *repositories.SessionRepository,
sessionSecret string,
cookieDomain string,
cookieSecure bool,
) *AuthHandler {
return &AuthHandler{
userRepo: userRepo,
sessionRepo: sessionRepo,
sessionSecret: sessionSecret,
cookieDomain: cookieDomain,
cookieSecure: cookieSecure,
}
}
func (h *AuthHandler) Login(c *gin.Context) {
// Rate limiting per IP
clientIP := c.ClientIP()
if !auth.DefaultLoginLimiter.Allow(clientIP) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many login attempts, try again later"})
return
}
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
user, err := h.userRepo.GetByUsername(req.Username)
if err != nil || user == nil {
auth.DefaultLoginLimiter.RecordFailure(clientIP)
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
return
}
if !auth.CheckPassword(req.Password, user.PasswordHash) {
auth.DefaultLoginLimiter.RecordFailure(clientIP)
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
return
}
auth.DefaultLoginLimiter.RecordSuccess(clientIP)
// INVALIDATE ANY EXISTING SESSION FOR THIS USER
// Delete any old sessions for this user before creating new one
h.sessionRepo.DeleteByUserID(user.ID)
sessionData, err := auth.NewSessionData(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create session"})
return
}
sessionID, err := auth.GenerateSessionID()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create session"})
return
}
session := &models.Session{
ID: sessionID,
UserID: user.ID,
CSRFToken: sessionData.CSRFToken,
CreatedAt: sessionData.CreatedAt,
ExpiresAt: sessionData.ExpiresAt,
}
if err := h.sessionRepo.Create(session); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save session"})
return
}
h.setSessionCookie(c, session.ID, session.ExpiresAt)
if err := h.userRepo.UpdateLastLogin(user.ID); err != nil {
// Log but don't fail
}
c.JSON(http.StatusOK, gin.H{
"user": gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
},
"csrf_token": session.CSRFToken,
})
}
func (h *AuthHandler) Logout(c *gin.Context) {
sessionID, err := c.Cookie(SessionCookieName)
if err == nil {
h.sessionRepo.Delete(sessionID)
}
h.clearSessionCookie(c)
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}
func (h *AuthHandler) Me(c *gin.Context) {
val, _ := c.Get("user"); user, _ := val.(*models.User)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
resp := gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
}
c.JSON(http.StatusOK, resp)
}
func (h *AuthHandler) GetCSRF(c *gin.Context) {
val, _ := c.Get("session"); sess, _ := val.(*models.Session)
if sess == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
c.JSON(http.StatusOK, gin.H{"csrf_token": sess.CSRFToken})
}
func (h *AuthHandler) setSessionCookie(c *gin.Context, sessionID string, expiresAt time.Time) {
http.SetCookie(c.Writer, &http.Cookie{
Name: SessionCookieName,
Value: sessionID,
MaxAge: int(time.Until(expiresAt).Seconds()),
Path: "/",
Domain: h.cookieDomain,
Secure: h.cookieSecure,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Expires: expiresAt,
})
}
func (h *AuthHandler) clearSessionCookie(c *gin.Context) {
http.SetCookie(c.Writer, &http.Cookie{
Name: SessionCookieName,
Value: "",
MaxAge: -1,
Path: "/",
Domain: h.cookieDomain,
Secure: h.cookieSecure,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
}