package api import ( "math/rand" "net/http" "strings" "time" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "github.com/prism/proxy/internal/auth" ) type loginReq struct { Password string `json:"password"` } func (s *Server) authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { settings, err := s.app.Store.GetSettings(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.Abort() return } if !s.authorizeRequest(c, settings) { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) c.Abort() return } c.Next() } } func (s *Server) authStatus(c *gin.Context) { settings, err := s.app.Store.GetSettings(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"enabled": settings["auth_enabled"] == "true"}) } func (s *Server) login(c *gin.Context) { var req loginReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() settings, err := s.app.Store.GetSettings(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } ip := clientIP(c) cfg := auth.ParseLoginConfig(settings) check, err := s.loginLimiter.Check(ctx, ip, cfg) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !check.Allowed { writeLoginLocked(c, check.RetryAfter) return } hash := settings["admin_password"] if !checkPassword(req.Password, hash) { time.Sleep(loginFailureDelay()) result, err := s.loginLimiter.RecordFailure(ctx, ip, cfg) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if result.Locked { writeLoginLocked(c, result.RetryAfter) return } c.JSON(http.StatusUnauthorized, gin.H{ "error": "invalid password", "remaining": result.Remaining, }) return } if err := s.loginLimiter.RecordSuccess(ctx, ip); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } token := issueToken(hash, 24*time.Hour) c.JSON(http.StatusOK, gin.H{"token": token}) } func writeLoginLocked(c *gin.Context, retryAfter time.Duration) { secs := int(retryAfter.Seconds()) if secs < 1 { secs = 1 } c.JSON(http.StatusTooManyRequests, gin.H{ "error": "too many login attempts", "retry_after": secs, }) } func loginFailureDelay() time.Duration { return 300*time.Millisecond + time.Duration(rand.Intn(200))*time.Millisecond } func checkPassword(password, stored string) bool { if strings.HasPrefix(stored, "$2a$") { return bcrypt.CompareHashAndPassword([]byte(stored), []byte(password)) == nil } return password == stored }