Release v0.20: регистрация, авторизация, личный кабинет
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user