Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user