193 lines
4.3 KiB
Go
193 lines
4.3 KiB
Go
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)
|
||
|
|
}
|