Release v0.20: регистрация, авторизация, личный кабинет

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shop
2026-05-16 17:31:56 +03:00
parent 4ea2b429b3
commit b3e3a06858
23 changed files with 981 additions and 27 deletions
+3
View File
@@ -0,0 +1,3 @@
package auth
const SessionCookieName = "shop_session"
+27
View File
@@ -0,0 +1,27 @@
package auth
import (
"errors"
"unicode/utf8"
"golang.org/x/crypto/bcrypt"
)
const (
minPasswordLen = 8
bcryptCost = 12
)
var ErrInvalidCredentials = errors.New("неверный email или пароль")
func HashPassword(password string) (string, error) {
if utf8.RuneCountInString(password) < minPasswordLen {
return "", errors.New("пароль должен быть не короче 8 символов")
}
b, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
return string(b), err
}
func CheckPassword(hash, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
+137
View File
@@ -0,0 +1,137 @@
package auth
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"shop/internal/models"
"shop/internal/repository"
)
var (
ErrEmailTaken = errors.New("этот email уже зарегистрирован")
ErrInvalidEmail = errors.New("некорректный email")
ErrInvalidName = errors.New("имя должно быть не короче 2 символов")
ErrNotAuthenticated = errors.New("требуется авторизация")
)
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
type Service struct {
users *repository.UserRepository
sessions *repository.SessionRepository
ttl time.Duration
secure bool
}
func NewService(users *repository.UserRepository, sessions *repository.SessionRepository, ttl time.Duration, cookieSecure bool) *Service {
return &Service{users: users, sessions: sessions, ttl: ttl, secure: cookieSecure}
}
func (s *Service) Register(ctx context.Context, email, password, name string) (*models.User, error) {
email = strings.TrimSpace(strings.ToLower(email))
name = strings.TrimSpace(name)
if !emailRe.MatchString(email) {
return nil, ErrInvalidEmail
}
if len([]rune(name)) < 2 {
return nil, ErrInvalidName
}
hash, err := HashPassword(password)
if err != nil {
return nil, err
}
user, err := s.users.Create(ctx, email, hash, name)
if err != nil {
if repository.IsUniqueViolation(err) {
return nil, ErrEmailTaken
}
return nil, err
}
return user, nil
}
func (s *Service) Login(ctx context.Context, w http.ResponseWriter, email, password string) error {
email = strings.TrimSpace(strings.ToLower(email))
user, hash, err := s.users.ByEmailWithHash(ctx, email)
if err != nil || !CheckPassword(hash, password) {
return ErrInvalidCredentials
}
return s.setSession(ctx, w, user.ID)
}
func (s *Service) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(SessionCookieName)
if err == nil && c.Value != "" {
_ = s.sessions.Delete(ctx, c.Value)
}
s.clearCookie(w)
}
func (s *Service) UserFromRequest(ctx context.Context, r *http.Request) (*models.User, error) {
c, err := r.Cookie(SessionCookieName)
if err != nil || c.Value == "" {
return nil, nil
}
userID, err := s.sessions.UserID(ctx, c.Value)
if err != nil || userID == 0 {
return nil, nil
}
return s.users.ByID(ctx, userID)
}
func (s *Service) UpdateName(ctx context.Context, userID int, name string) error {
name = strings.TrimSpace(name)
if len([]rune(name)) < 2 {
return ErrInvalidName
}
return s.users.UpdateName(ctx, userID, name)
}
func (s *Service) setSession(ctx context.Context, w http.ResponseWriter, userID int) error {
token, err := newToken()
if err != nil {
return err
}
expires := time.Now().Add(s.ttl)
if err := s.sessions.Create(ctx, token, userID, expires); err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: SessionCookieName,
Value: token,
Path: "/",
Expires: expires,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: s.secure,
})
return nil
}
func (s *Service) clearCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: SessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: s.secure,
})
}
func newToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("session token: %w", err)
}
return hex.EncodeToString(b), nil
}