package auth import ( "context" "crypto/rand" "encoding/base64" "fmt" "log/slog" "time" "goAuth/internal/invite" "goAuth/internal/user" "github.com/mileusna/useragent" ) const ( TransactionTimeout = 15 * time.Second ) type Service struct { r *Repository u *user.Service i *invite.Service shortDuration time.Duration longDuration time.Duration tokenSize int } func NewService(r *Repository, u *user.Service, i *invite.Service, shortDuration, longDuration time.Duration, tokenSize int) (*Service, error) { if tokenSize <= 0 { tokenSize = 32 } return &Service{r: r, u: u, i: i, shortDuration: shortDuration, longDuration: longDuration, tokenSize: tokenSize}, nil } func detachWithTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { return context.WithTimeout(context.WithoutCancel(ctx), timeout) } func (s *Service) ClassifiedUserAgent(userAgent string) string { ua := useragent.Parse(userAgent) var class string if ua.Mobile { class = "Mobile" } if ua.Tablet { class = "Tablet" } if ua.Desktop { class = "Desktop" } if ua.Bot { class = "Bot" } device := class if ua.Name != "" { device += fmt.Sprintf(", %s %s", ua.Name, ua.Version) } if ua.OS != "" { device += fmt.Sprintf(", %s %s", ua.OS, ua.OSVersion) } if ua.Device != "" { device += fmt.Sprintf(", %s", ua.Device) } return device } func (s *Service) generateToken() (string, error) { b := make([]byte, s.tokenSize) _, err := rand.Read(b) if err != nil { slog.Error("generate session token", slog.String("op", "service.generateToken"), slog.Any("error", err)) return "", ErrInternal } return base64.RawURLEncoding.EncodeToString(b), nil } func (s *Service) RegisterUser(ctx context.Context, login, password, invite string, address, device string) (*user.User, *Session, error) { err := ctx.Err() if err != nil { return nil, nil, ErrCanceled } newCtx, cancelFunc := detachWithTimeout(ctx, TransactionTimeout) defer cancelFunc() user, err := s.u.CreateUser(newCtx, login, password, false) if err != nil { return nil, nil, err } _, err = s.i.UseInvite(newCtx, invite) if err != nil { s.u.DeleteUser(newCtx, user) return nil, nil, err } _, err = s.u.ActivateUser(newCtx, user) if err != nil { s.u.DeleteUser(newCtx, user) return nil, nil, err } return s.LoginUser(newCtx, login, password, address, device, false) } func (s *Service) LoginUser(ctx context.Context, login, password, address, device string, isLong bool) (*user.User, *Session, error) { token, err := s.generateToken() if err != nil { return nil, nil, err } user, err := s.u.UserByLogin(ctx, login) if err != nil { return nil, nil, err } err = s.u.ValidatePassword(ctx, user, password) if err != nil { return nil, nil, err } var duration time.Duration if isLong { duration = s.longDuration } else { duration = s.shortDuration } session, err := s.r.CreateSession(ctx, token, user.Id, address, device, time.Now().UTC().Add(duration), isLong) if err != nil { return nil, nil, err } return user, session, nil } func (s *Service) BumpSession(ctx context.Context, session *Session) (*Session, error) { var duration time.Duration if session.IsLong { duration = s.longDuration } else { duration = s.shortDuration } halfExpire := time.Now().UTC().Add(duration / 2) if session.ExpiresAt.After(halfExpire) { return session, nil } value := *session cloned := &value cloned.ExpiresAt = time.Now().UTC().Add(duration) err := s.r.UpdateSession(ctx, cloned) if err != nil { return nil, err } return cloned, nil } func (s *Service) SessionByToken(ctx context.Context, token string) (*Session, error) { return s.r.SessionByToken(ctx, token) } func (s *Service) CountSessions(ctx context.Context, user *user.User) (int64, error) { return s.r.CountSessions(ctx, user.Id) } func (s *Service) Sessions(ctx context.Context, user *user.User, limit, offset int64) ([]*Session, error) { return s.r.Sessions(ctx, user.Id, limit, offset) } func (s *Service) DeleteSession(ctx context.Context, session *Session) error { return s.r.DeleteSession(ctx, session.Token) } func (s *Service) DeleteSessions(ctx context.Context, user *user.User) error { return s.r.DeleteSessions(ctx, user.Id) } func (s *Service) CleanupSessions(ctx context.Context) error { return s.r.CleanupSessions(ctx) }