Backend: 'Запомнить меня' — remember=true ставит cookie на 30 дней (refresh TTL)
This commit is contained in:
parent
2a765b5df5
commit
aafbc6f047
@ -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()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user