package handlers import ( "errors" "net/http" "time" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" "booklib/internal/auth" ) const loginWindow = time.Minute const loginMax = 5 func (h *H) Login(c *gin.Context) { var req struct{ Username, Password string } if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" { err(c, http.StatusBadRequest, "bad_request", "username and password required") return } if n := h.rdb.IncrWindow(c, "loginrl:"+c.ClientIP(), loginWindow); n > loginMax { err(c, http.StatusTooManyRequests, "rate_limited", "too many login attempts") return } u, qerr := h.st.GetUserByName(c, req.Username) if qerr != nil { if !errors.Is(qerr, pgx.ErrNoRows) { dbErr(c, qerr) return } // 用户不存在也走一次 bcrypt,防用户名枚举时序差 auth.CheckPassword("$2a$12$V5TmlpkEi9G/DFAmnkt9YunNdGLnnW921/5TcSo4OeE6iaaLDPN1K", req.Password) err(c, http.StatusUnauthorized, "unauthorized", "bad credentials") return } if !auth.CheckPassword(u.PasswordHash, req.Password) { err(c, http.StatusUnauthorized, "unauthorized", "bad credentials") return } tok, serr := auth.Sign(h.cfg.JWTSecret, u.ID, u.Role) if serr != nil { err(c, http.StatusInternalServerError, "internal", "sign") return } c.JSON(http.StatusOK, gin.H{"token": tok}) } func (h *H) Me(c *gin.Context) { // B7: only no-rows → 401; other errors (PG down) go through dbErr → 503. u, qerr := h.st.GetUserByID(c, uid(c)) if qerr != nil { if errors.Is(qerr, pgx.ErrNoRows) { err(c, http.StatusUnauthorized, "unauthorized", "no such user") return } dbErr(c, qerr) return } c.JSON(http.StatusOK, gin.H{"id": u.ID, "username": u.Username, "role": u.Role}) }