pjpjq's picture
fix: build usage keeper from source
b034029 verified
package api
import (
"crypto/subtle"
"net"
"net/http"
"sync"
"time"
"cpa-usage-keeper/internal/auth"
"github.com/gin-gonic/gin"
)
const sessionCookieName = "cpa_usage_keeper_session"
const maxFailedLoginAttempts = 5
type AuthConfig struct {
Enabled bool
LoginPassword string
SessionTTL time.Duration
BasePath string
}
type authHandler struct {
config AuthConfig
sessions *auth.SessionManager
mu sync.Mutex
failedAttempts map[string]int
}
type loginRequest struct {
Password string `json:"password"`
}
type sessionResponse struct {
Authenticated bool `json:"authenticated"`
}
func NewAuthHandler(config AuthConfig, sessions *auth.SessionManager) *authHandler {
return &authHandler{config: config, sessions: sessions, failedAttempts: make(map[string]int)}
}
func (h *authHandler) registerRoutes(router gin.IRoutes) {
router.GET("/session", h.getSession)
router.POST("/login", h.login)
router.POST("/logout", h.logout)
}
func (h *authHandler) middleware() gin.HandlerFunc {
return func(c *gin.Context) {
if h == nil || !h.config.Enabled {
c.Next()
return
}
if h.sessions == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
token, err := c.Cookie(sessionCookieName)
if err != nil || !h.sessions.Validate(token) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
c.Next()
}
}
func (h *authHandler) getSession(c *gin.Context) {
if h == nil || !h.config.Enabled {
c.JSON(http.StatusOK, sessionResponse{Authenticated: true})
return
}
if h.sessions == nil {
c.JSON(http.StatusOK, sessionResponse{Authenticated: false})
return
}
token, err := c.Cookie(sessionCookieName)
if err != nil {
c.JSON(http.StatusOK, sessionResponse{Authenticated: false})
return
}
c.JSON(http.StatusOK, sessionResponse{Authenticated: h.sessions.Validate(token)})
}
func (h *authHandler) login(c *gin.Context) {
if h == nil || !h.config.Enabled {
c.Status(http.StatusNoContent)
return
}
if h.sessions == nil {
writeInternalError(c, "session manager is not configured", nil)
return
}
var request loginRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
clientKey := loginClientKey(c)
passwordMatches := subtle.ConstantTimeCompare([]byte(request.Password), []byte(h.config.LoginPassword)) == 1
if h.tooManyFailedAttempts(clientKey) && !passwordMatches {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many failed login attempts"})
return
}
if !passwordMatches {
h.recordFailedAttempt(clientKey)
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid password"})
return
}
h.clearFailedAttempts(clientKey)
token, expiresAt, err := h.sessions.Create()
if err != nil {
writeInternalError(c, "create auth session failed", err)
return
}
secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
cookiePath := h.config.BasePath
if cookiePath == "" {
cookiePath = "/"
}
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionCookieName,
Value: token,
Path: cookiePath,
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
Expires: expiresAt,
MaxAge: int(time.Until(expiresAt).Seconds()),
})
c.Status(http.StatusNoContent)
}
func (h *authHandler) logout(c *gin.Context) {
if h == nil || !h.config.Enabled {
c.Status(http.StatusNoContent)
return
}
if h.sessions != nil {
if token, err := c.Cookie(sessionCookieName); err == nil {
h.sessions.Delete(token)
}
}
clearSessionCookie(c, h.config.BasePath)
c.Status(http.StatusNoContent)
}
func (h *authHandler) tooManyFailedAttempts(key string) bool {
h.mu.Lock()
defer h.mu.Unlock()
return h.failedAttempts[key] >= maxFailedLoginAttempts
}
func (h *authHandler) recordFailedAttempt(key string) {
h.mu.Lock()
defer h.mu.Unlock()
h.failedAttempts[key]++
}
func (h *authHandler) clearFailedAttempts(key string) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.failedAttempts, key)
}
func loginClientKey(c *gin.Context) string {
host, _, err := net.SplitHostPort(c.Request.RemoteAddr)
if err == nil && host != "" {
return host
}
return c.ClientIP()
}
func clearSessionCookie(c *gin.Context, basePath string) {
cookiePath := basePath
if cookiePath == "" {
cookiePath = "/"
}
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: cookiePath,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Expires: time.Unix(0, 0),
MaxAge: -1,
})
}