Backend: 'Запомнить меня' — remember=true ставит cookie на 30 дней (refresh TTL)
This commit is contained in:
parent
2a765b5df5
commit
aafbc6f047
@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -20,6 +21,8 @@ const cookieName = "buhapp_at"
|
|||||||
|
|
||||||
// setAuthCookie — кладём access-token в HttpOnly cookie
|
// setAuthCookie — кладём access-token в HttpOnly cookie
|
||||||
// Same-origin через nginx /api proxy: cookie host-only, SameSite=Lax
|
// 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) {
|
func setAuthCookie(c *fiber.Ctx, token string, ttl time.Duration) {
|
||||||
c.Cookie(&fiber.Cookie{
|
c.Cookie(&fiber.Cookie{
|
||||||
Name: cookieName,
|
Name: cookieName,
|
||||||
@ -53,6 +56,7 @@ type registerRequest struct {
|
|||||||
Gender string `json:"gender"`
|
Gender string `json:"gender"`
|
||||||
City string `json:"city"`
|
City string `json:"city"`
|
||||||
Consents consentsBlock `json:"consents"`
|
Consents consentsBlock `json:"consents"`
|
||||||
|
Remember bool `json:"remember"` // "запомнить меня"
|
||||||
}
|
}
|
||||||
|
|
||||||
type consentsBlock struct {
|
type consentsBlock struct {
|
||||||
@ -163,8 +167,12 @@ func (h *AuthHandler) Register(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
|
||||||
}
|
}
|
||||||
// ставим access-token в HttpOnly cookie
|
// запомнить меня — если попросят, cookie на 30 дней
|
||||||
setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
|
accessTTL := h.cfg.JwTAccessTTL()
|
||||||
|
if req.Remember {
|
||||||
|
accessTTL = h.cfg.JwtRefreshTTL()
|
||||||
|
}
|
||||||
|
setAuthCookie(c, tokens.Access, accessTTL)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||||
"user": fiber.Map{
|
"user": fiber.Map{
|
||||||
@ -177,6 +185,8 @@ func (h *AuthHandler) Register(c *fiber.Ctx) error {
|
|||||||
"birthdate": u.Birthdate,
|
"birthdate": u.Birthdate,
|
||||||
},
|
},
|
||||||
"tokens": tokens,
|
"tokens": tokens,
|
||||||
|
"remember": req.Remember,
|
||||||
|
"expires_in": int(accessTTL.Seconds()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,9 +194,10 @@ type loginRequest struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Password string `json:"password"`
|
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 {
|
func (h *AuthHandler) Login(c *fiber.Ctx) error {
|
||||||
c.Set("Connection", "close")
|
c.Set("Connection", "close")
|
||||||
c.Set("Cache-Control", "no-store")
|
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 {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
|
||||||
}
|
}
|
||||||
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
|
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||||
req.Phone = strings.TrimSpace(req.Phone)
|
if req.Email == "" || req.Password == "" {
|
||||||
if req.Email == "" && req.Phone == "" {
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email and password required"})
|
||||||
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"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var u *users.User
|
// timing-safe lookup
|
||||||
var hash string
|
u, hash, err := h.users.GetByEmail(c.UserContext(), req.Email)
|
||||||
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"})
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
||||||
}
|
}
|
||||||
// Constant-time: всегда прогоняем bcrypt даже если юзер не найден — против timing-атак
|
// Constant-time: всегда прогоняем bcrypt даже если юзер не найден — против timing-атак
|
||||||
if u == nil {
|
if u == nil {
|
||||||
_ = bcrypt.CompareHashAndPassword([]byte(dummyHash), []byte(req.Password))
|
_ = 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"})
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
|
||||||
}
|
}
|
||||||
if u.IsBlocked {
|
if u.IsBlocked {
|
||||||
@ -230,12 +232,21 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "token failed"})
|
||||||
}
|
}
|
||||||
// ставим access-token в HttpOnly cookie — браузер шлёт автоматически, без CORS preflight
|
// "Запомнить меня" — если true, ставим cookie на 30 дней
|
||||||
setAuthCookie(c, tokens.Access, h.cfg.JwTAccessTTL())
|
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{
|
return c.JSON(fiber.Map{
|
||||||
"user": u,
|
"user": u,
|
||||||
"tokens": tokens,
|
"tokens": tokens,
|
||||||
|
"remember": req.Remember,
|
||||||
|
"expires_in": int(accessTTL.Seconds()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user