From 099a586484ac2de74a57269326a989ab8b1c80ed Mon Sep 17 00:00:00 2001 From: nquidox Date: Mon, 7 Jul 2025 17:45:58 +0300 Subject: [PATCH] created: auth provider --- internal/interfaces/auth.go | 9 ++++ internal/provider/auth/handler.go | 32 ++++++++++++ internal/provider/auth/model.go | 16 ++++++ internal/provider/auth/repository.go | 46 +++++++++++++++++ internal/provider/auth/service.go | 76 ++++++++++++++++++++++++++++ internal/shared/auth.go | 6 +++ 6 files changed, 185 insertions(+) create mode 100644 internal/interfaces/auth.go create mode 100644 internal/provider/auth/handler.go create mode 100644 internal/provider/auth/model.go create mode 100644 internal/provider/auth/repository.go create mode 100644 internal/provider/auth/service.go create mode 100644 internal/shared/auth.go diff --git a/internal/interfaces/auth.go b/internal/interfaces/auth.go new file mode 100644 index 0000000..3a668a8 --- /dev/null +++ b/internal/interfaces/auth.go @@ -0,0 +1,9 @@ +package interfaces + +import "merch-parser-api/internal/shared" + +type Auth interface { + Login(userUuid string) (shared.AuthData, error) + Logout(userUuid string, refreshUuid string) error + Refresh(userUuid string, refreshUuid string) (shared.AuthData, error) +} diff --git a/internal/provider/auth/handler.go b/internal/provider/auth/handler.go new file mode 100644 index 0000000..66028ed --- /dev/null +++ b/internal/provider/auth/handler.go @@ -0,0 +1,32 @@ +package auth + +import ( + "gorm.io/gorm" + "merch-parser-api/internal/interfaces" +) + +type Handler struct { + *Service + repo *repo + jwtProvider interfaces.JWTProvider + utils interfaces.Utils + RefreshTokenExpTime int64 +} + +type Deps struct { + DB *gorm.DB + JwtProvider interfaces.JWTProvider + Utils interfaces.Utils +} + +func NewHandler(deps Deps) *Handler { + r := newRepository(deps.DB) + s := newService(r, deps.JwtProvider) + + return &Handler{ + Service: s, + repo: r, + jwtProvider: deps.JwtProvider, + utils: deps.Utils, + } +} diff --git a/internal/provider/auth/model.go b/internal/provider/auth/model.go new file mode 100644 index 0000000..0da6172 --- /dev/null +++ b/internal/provider/auth/model.go @@ -0,0 +1,16 @@ +package auth + +import ( + "database/sql" + "time" +) + +type RefreshToken struct { + Id uint `gorm:"primary_key"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt sql.NullTime `gorm:"column:updated_at"` + DeletedAt sql.NullTime `gorm:"column:deleted_at"` + UserUuid string `gorm:"column:user_uuid"` + TokenUuid string `gorm:"column:token_uuid"` + Expires int64 `gorm:"column:expires"` +} diff --git a/internal/provider/auth/repository.go b/internal/provider/auth/repository.go new file mode 100644 index 0000000..53dfb73 --- /dev/null +++ b/internal/provider/auth/repository.go @@ -0,0 +1,46 @@ +package auth + +import ( + "gorm.io/gorm" + "time" +) + +type Repository interface { + CreateRefreshToken(token *RefreshToken) error + ReadRefreshToken(userUuid string, tokenUuid string) (RefreshToken, error) + InvalidateRefreshToken(userUuid string, tokenUuid string) error +} + +type repo struct { + db *gorm.DB +} + +func newRepository(db *gorm.DB) *repo { + return &repo{db: db} +} + +func (r *repo) CreateRefreshToken(token *RefreshToken) error { + return r.db.Create(token).Error +} + +func (r *repo) ReadRefreshToken(userUuid string, tokenUuid string) (RefreshToken, error) { + var tokenData RefreshToken + + if err := r.db. + Where("token_uuid = ?", tokenUuid). + Where("user_uuid = ?", userUuid). + Where("deleted_at IS NULL"). + First(&tokenData).Error; err != nil { + return RefreshToken{}, err + } + + return tokenData, nil +} + +func (r *repo) InvalidateRefreshToken(userUuid string, tokenUuid string) error { + return r.db. + Model(&RefreshToken{}). + Where("user_uuid = ?", userUuid). + Where("token_uuid = ?", tokenUuid). + Update("deleted_at", time.Now().UTC()).Error +} diff --git a/internal/provider/auth/service.go b/internal/provider/auth/service.go new file mode 100644 index 0000000..15cddf5 --- /dev/null +++ b/internal/provider/auth/service.go @@ -0,0 +1,76 @@ +package auth + +import ( + "errors" + "github.com/google/uuid" + "gorm.io/gorm" + "merch-parser-api/internal/interfaces" + "merch-parser-api/internal/shared" + "time" +) + +type Service struct { + repo Repository + jwtProvider interfaces.JWTProvider +} + +func newService(repo Repository, jwtProvider interfaces.JWTProvider) *Service { + return &Service{ + repo: repo, + jwtProvider: jwtProvider, + } +} + +func (s *Service) Login(userUuid string) (shared.AuthData, error) { + return s.generateTokens(userUuid) +} + +func (s *Service) Refresh(userUuid string, refreshUuid string) (shared.AuthData, error) { + var err error + tokenData, err := s.repo.ReadRefreshToken(userUuid, refreshUuid) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return shared.AuthData{}, errors.New("refresh token is not valid or doesn't exist") + } + return shared.AuthData{}, err + } + + if tokenData.Expires < time.Now().UTC().Unix() { + return shared.AuthData{}, errors.New("token expired") + } + + err = s.repo.InvalidateRefreshToken(userUuid, refreshUuid) + if err != nil { + return shared.AuthData{}, err + } + + return s.generateTokens(userUuid) +} + +func (s *Service) Logout(userUuid string, refreshUuid string) error { + return s.repo.InvalidateRefreshToken(userUuid, refreshUuid) +} + +func (s *Service) generateTokens(userUuid string) (shared.AuthData, error) { + accessToken, err := s.jwtProvider.CreateAccessToken(userUuid) + if err != nil { + return shared.AuthData{}, err + } + + refreshTokenUuid := uuid.NewString() + refreshToken, exp, err := s.jwtProvider.CreateRefreshToken(userUuid, refreshTokenUuid) + if err != nil { + return shared.AuthData{}, err + } + + err = s.repo.CreateRefreshToken(&RefreshToken{ + Expires: exp, + UserUuid: userUuid, + TokenUuid: refreshTokenUuid, + }) + + return shared.AuthData{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, nil +} diff --git a/internal/shared/auth.go b/internal/shared/auth.go new file mode 100644 index 0000000..007cb51 --- /dev/null +++ b/internal/shared/auth.go @@ -0,0 +1,6 @@ +package shared + +type AuthData struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +}