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 }