b3e3a06858
Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
3.5 KiB
Go
138 lines
3.5 KiB
Go
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
|
|
}
|