172 lines
No EOL
4.4 KiB
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,
|
|
})
|
|
} |