Backend: 'Запомнить меня' — remember=true ставит cookie на 30 дней (refresh TTL)

This commit is contained in:
ga 2026-08-21 11:19:00 +00:00
parent 2a765b5df5
commit aafbc6f047

View File

@ -2,6 +2,7 @@ package handlers
import (
"regexp"
"strconv"
"strings"
"time"
@ -20,6 +21,8 @@ const cookieName = "buhapp_at"
// setAuthCookie — кладём access-token в HttpOnly cookie
// Same-origin через nginx /api proxy: cookie host-only, SameSite=Lax
// remember=true → MaxAge = 30 дней (refresh-токен живёт 30 дней в БД)
// remember=false → MaxAge = 1 час (стандарт)
func setAuthCookie(c *fiber.Ctx, token string, ttl time.Duration) {
c.Cookie(&fiber.Cookie{
Name: cookieName,
@ -53,6 +56,7 @@ type registerRequest struct {
Gender string `json:"gender"`
City string `json:"city"`
Consents consentsBlock `json:"consents"`
Remember bool `json:"remember"` // "запомнить меня"
}
type consentsBlock struct {
@ -163,8 +167,12 @@ func (h *AuthHandler) Register(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
}
// ставим access-token в HttpOnly cookie
setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
// запомнить меня — если попросят, cookie на 30 дней
accessTTL := h.cfg.JwTAccessTTL()
if req.Remember {
accessTTL = h.cfg.JwtRefreshTTL()
}
setAuthCookie(c, tokens.Access, accessTTL)
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"user": fiber.Map{
@ -177,6 +185,8 @@ func (h *AuthHandler) Register(c *fiber.Ctx) error {
"birthdate": u.Birthdate,
},
"tokens": tokens,
"remember": req.Remember,
"expires_in": int(accessTTL.Seconds()),
})
}
@ -184,9 +194,10 @@ type loginRequest struct {
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
Remember bool `json:"remember"` // "запомнить меня" — cookie на 30 дней
}
// Login — фикс для Safari: разрыв keep-alive после Set-Cookie
// Login фикс для Safari + remember me
func (h *AuthHandler) Login(c *fiber.Ctx) error {
c.Set("Connection", "close")
c.Set("Cache-Control", "no-store")
@ -194,29 +205,20 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
}
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
req.Phone = strings.TrimSpace(req.Phone)
if req.Email == "" && req.Phone == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email or phone required"})
}
if len(req.Password) < 1 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "password required"})
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
if req.Email == "" || req.Password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email and password required"})
}
var u *users.User
var hash string
var err error
if req.Email != "" {
u, hash, err = h.users.GetByEmail(c.UserContext(), req.Email)
} else {
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "login by phone not implemented yet"})
}
// timing-safe lookup
u, hash, err := h.users.GetByEmail(c.UserContext(), req.Email)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
}
// Constant-time: всегда прогоняем bcrypt даже если юзер не найден — против timing-атак
if u == nil {
_ = bcrypt.CompareHashAndPassword([]byte(dummyHash), []byte(req.Password))
_ = h.audit.Log(c.UserContext(), &audit.Event{UserID: nil, Action: "login_failed", IP: c.IP(), UserAgent: c.Get("User-Agent"), Metadata: []byte(`{"reason":"no_user","email":"` + req.Email + `"}`)})
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
}
if u.IsBlocked {
@ -230,12 +232,21 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
}
// ставим access-token в HttpOnly cookie — браузер шлёт автоматически, без CORS preflight
setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
// "Запомнить меня" — если true, ставим cookie на 30 дней
accessTTL := h.cfg.JwTAccessTTL()
if req.Remember {
accessTTL = h.cfg.JwtRefreshTTL()
}
setAuthCookie(c, tokens.Access, accessTTL)
uid := u.ID
_ = h.audit.Log(c.UserContext(), &audit.Event{UserID: &uid, Action: "login_ok", IP: c.IP(), UserAgent: c.Get("User-Agent"), Metadata: []byte(`{"remember":` + strconv.FormatBool(req.Remember) + `}`)})
return c.JSON(fiber.Map{
"user": u,
"tokens": tokens,
"remember": req.Remember,
"expires_in": int(accessTTL.Seconds()),
})
}