diff --git a/go.mod b/go.mod index d523c2c..583c53f 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,8 @@ require ( github.com/google/uuid v1.6.0 github.com/itsLeonB/ezutil/v2 v2.4.0 github.com/itsLeonB/ginkgo v0.6.1 - github.com/itsLeonB/go-authkit v0.0.3 + github.com/itsLeonB/go-authkit v0.0.5 github.com/itsLeonB/go-crud v1.4.0 - github.com/itsLeonB/sekure v0.1.1 github.com/itsLeonB/ungerr v0.4.0 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 diff --git a/go.sum b/go.sum index c5a4f26..575d93f 100644 --- a/go.sum +++ b/go.sum @@ -318,12 +318,10 @@ github.com/itsLeonB/ezutil/v2 v2.4.0 h1:ylhQWF0yoBGltfuU4zi0wPj+JCoVv9jPoNvZsNON github.com/itsLeonB/ezutil/v2 v2.4.0/go.mod h1:h30JTcbfmdbMXfgc9ARGlqoudR2UMG2EV49dpIZ60Os= github.com/itsLeonB/ginkgo v0.6.1 h1:M6oNaA+V+t2zEiq7Oao53zz1Pn3ucAzl5ESL3JFduTo= github.com/itsLeonB/ginkgo v0.6.1/go.mod h1:BeT3tcIuen91lNjMyUTmP0N+ckCzlZsv+wMoxj4kqEo= -github.com/itsLeonB/go-authkit v0.0.3 h1:ICGLle0hWu5FoV+71Cs01/VqPDagqOZ9WXT+n4DjGfI= -github.com/itsLeonB/go-authkit v0.0.3/go.mod h1:ojg5B2Ld99dnyzKgJZuaVnGeabah9IE+/dnTAaDygCM= +github.com/itsLeonB/go-authkit v0.0.5 h1:1SctZlCh6/X92AZ6zrBD5jNn67BRw1S460NoKmvuNCo= +github.com/itsLeonB/go-authkit v0.0.5/go.mod h1:LaTtHS4Wkn3lqrd5rf5RLYgeJ5UEAQ+KFK1x6Fr6Tx8= github.com/itsLeonB/go-crud v1.4.0 h1:A8gRbxFJ9VrrqkoBUKvhf1I3EmXz2evFM660gk5wexI= github.com/itsLeonB/go-crud v1.4.0/go.mod h1:cndhkAF9Z0m2cMsKfD8Z2R07lPAJqHpazCF0UlfW89U= -github.com/itsLeonB/sekure v0.1.1 h1:4xoL/rZs0ouvR86edkEkjOsxJM+d04R8i0NU4JiPJu8= -github.com/itsLeonB/sekure v0.1.1/go.mod h1:YWC1y5HnG02hy/WltPUqCem3+RtV8Q9BRp2+esDKjX8= github.com/itsLeonB/ungerr v0.4.0 h1:unXts8ahcD7mTnlzJgem5twGcQLMPzo3Vn35PbORito= github.com/itsLeonB/ungerr v0.4.0/go.mod h1:6zc0blpoIkdqkq90Q9rqCYFjyxR1uOn5n+5z7KSgWxM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/internal/adapters/auth/admin/user_store.go b/internal/adapters/auth/admin/user_store.go new file mode 100644 index 0000000..44e3e10 --- /dev/null +++ b/internal/adapters/auth/admin/user_store.go @@ -0,0 +1,113 @@ +package adminauth + +import ( + "context" + + "github.com/google/uuid" + "github.com/itsLeonB/cashback/internal/domain/entity/admin" + "github.com/itsLeonB/go-authkit" + "github.com/itsLeonB/go-crud" +) + +type UserStore struct { + repo crud.Repository[admin.User] +} + +func NewUserStore(repo crud.Repository[admin.User]) *UserStore { + return &UserStore{repo: repo} +} + +func (s *UserStore) FindByID(ctx context.Context, userID string) (authkit.User, error) { + uid, err := uuid.Parse(userID) + if err != nil { + return authkit.User{}, authkit.ErrUserNotFound + } + spec := crud.Specification[admin.User]{} + spec.Model.ID = uid + user, err := s.repo.FindFirst(ctx, spec) + if err != nil { + return authkit.User{}, err + } + if user.IsZero() { + return authkit.User{}, authkit.ErrUserNotFound + } + return toAuthUser(user), nil +} + +func (s *UserStore) FindByEmail(ctx context.Context, email string) (authkit.User, error) { + spec := crud.Specification[admin.User]{} + spec.Model.Email = email + user, err := s.repo.FindFirst(ctx, spec) + if err != nil { + return authkit.User{}, err + } + if user.IsZero() { + return authkit.User{}, authkit.ErrUserNotFound + } + return toAuthUser(user), nil +} + +func (s *UserStore) Create(ctx context.Context, email, passwordHash string) (authkit.User, error) { + user, err := s.repo.Insert(ctx, admin.User{ + Email: email, + Password: passwordHash, + }) + if err != nil { + return authkit.User{}, err + } + return toAuthUser(user), nil +} + +func (s *UserStore) CreateOAuth(_ context.Context, _, _, _ string) (authkit.User, error) { + return authkit.User{}, authkit.ErrNotSupported +} + +func (s *UserStore) SetVerified(_ context.Context, userID, _, _ string) (authkit.User, error) { + // Admin users are always considered verified. + return authkit.User{ID: userID, Verified: true}, nil +} + +func (s *UserStore) UpdatePassword(ctx context.Context, userID, passwordHash string) error { + uid, err := uuid.Parse(userID) + if err != nil { + return authkit.ErrUserNotFound + } + spec := crud.Specification[admin.User]{} + spec.Model.ID = uid + user, err := s.repo.FindFirst(ctx, spec) + if err != nil { + return err + } + if user.IsZero() { + return authkit.ErrUserNotFound + } + user.Password = passwordHash + _, err = s.repo.Update(ctx, user) + return err +} + +func (s *UserStore) Exists(ctx context.Context, userID string) error { + uid, err := uuid.Parse(userID) + if err != nil { + return authkit.ErrUserNotFound + } + spec := crud.Specification[admin.User]{} + spec.Model.ID = uid + user, err := s.repo.FindFirst(ctx, spec) + if err != nil { + return err + } + if user.IsZero() { + return authkit.ErrUserNotFound + } + return nil +} + +func toAuthUser(u admin.User) authkit.User { + return authkit.User{ + ID: u.ID.String(), + Email: u.Email, + PasswordHash: u.Password, + Verified: true, // admin users are always verified + } +} diff --git a/internal/adapters/http/handler/admin/auth_handler.go b/internal/adapters/http/handler/admin/auth_handler.go index b604e1e..66768c3 100644 --- a/internal/adapters/http/handler/admin/auth_handler.go +++ b/internal/adapters/http/handler/admin/auth_handler.go @@ -4,35 +4,26 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/itsLeonB/cashback/internal/core/util" "github.com/itsLeonB/cashback/internal/domain/dto" - "github.com/itsLeonB/cashback/internal/domain/service/admin" + adminEntity "github.com/itsLeonB/cashback/internal/domain/entity/admin" + "github.com/itsLeonB/go-authkit/authgin" + "github.com/itsLeonB/go-crud" "github.com/itsLeonB/ginkgo/pkg/server" + "github.com/itsLeonB/ungerr" ) type AuthHandler struct { - authSvc admin.AuthService + stateless *authgin.StatelessHandler + userRepo crud.Repository[adminEntity.User] } func (ah *AuthHandler) HandleRegister() gin.HandlerFunc { - return server.Handler("AuthHandler.HandleRegister", http.StatusCreated, func(ctx *gin.Context) (any, error) { - request, err := server.BindJSON[dto.RegisterRequest](ctx) - if err != nil { - return nil, err - } - - return nil, ah.authSvc.Register(ctx.Request.Context(), request) - }) + return ah.stateless.Register() } func (ah *AuthHandler) HandleLogin() gin.HandlerFunc { - return server.Handler("AuthHandler.HandleLogin", http.StatusOK, func(ctx *gin.Context) (any, error) { - request, err := server.BindJSON[dto.InternalLoginRequest](ctx) - if err != nil { - return nil, err - } - - return ah.authSvc.Login(ctx.Request.Context(), request) - }) + return ah.stateless.Login() } func (ah *AuthHandler) HandleMe() gin.HandlerFunc { @@ -41,7 +32,15 @@ func (ah *AuthHandler) HandleMe() gin.HandlerFunc { if err != nil { return nil, err } - - return ah.authSvc.Me(ctx.Request.Context(), userID) + spec := crud.Specification[adminEntity.User]{} + spec.Model.ID = userID + user, err := ah.userRepo.FindFirst(ctx.Request.Context(), spec) + if err != nil { + return nil, err + } + if user.IsZero() { + return nil, ungerr.UnauthorizedError("user not found") + } + return dto.AdminMe{ID: user.ID, FullName: util.GetNameFromEmail(user.Email)}, nil }) } diff --git a/internal/adapters/http/handler/admin/handlers.go b/internal/adapters/http/handler/admin/handlers.go index d1bb321..ac3dc8f 100644 --- a/internal/adapters/http/handler/admin/handlers.go +++ b/internal/adapters/http/handler/admin/handlers.go @@ -2,7 +2,8 @@ package admin import ( "github.com/itsLeonB/cashback/internal/provider" - "github.com/itsLeonB/cashback/internal/provider/admin" + adminProvider "github.com/itsLeonB/cashback/internal/provider/admin" + "github.com/itsLeonB/go-authkit/authgin" ) type Handlers struct { @@ -14,9 +15,9 @@ type Handlers struct { Payment PaymentHandler } -func ProvideHandlers(services *admin.Services, domainServices *provider.Services) *Handlers { +func ProvideHandlers(adminServices *adminProvider.Services, adminRepos *adminProvider.Repositories, domainServices *provider.Services) *Handlers { return &Handlers{ - AuthHandler{services.Auth}, + AuthHandler{stateless: authgin.NewStatelessHandler(adminServices.Kit), userRepo: adminRepos.User}, PlanHandler{domainServices.Plan}, PlanVersionHandler{domainServices.PlanVersion}, SubscriptionHandler{domainServices.Subscription}, diff --git a/internal/adapters/http/middlewares/middlewares.go b/internal/adapters/http/middlewares/middlewares.go index 76c088a..19320a3 100644 --- a/internal/adapters/http/middlewares/middlewares.go +++ b/internal/adapters/http/middlewares/middlewares.go @@ -4,7 +4,7 @@ import ( "github.com/gin-gonic/gin" "github.com/itsLeonB/cashback/internal/core/config" "github.com/itsLeonB/cashback/internal/core/logger" - "github.com/itsLeonB/cashback/internal/domain/service/admin" + "github.com/itsLeonB/go-authkit" "github.com/itsLeonB/ginkgo/pkg/middleware" ) @@ -13,9 +13,13 @@ type Middlewares struct { AdminAuth gin.HandlerFunc } -func Provide(configs config.App, adminAuthSvc admin.AuthService) *Middlewares { +func Provide(configs config.App, adminKit *authkit.AuthKit) *Middlewares { adminTokenCheckFunc := func(ctx *gin.Context, token string) (bool, map[string]any, error) { - return adminAuthSvc.VerifyToken(ctx.Request.Context(), token) + claims, err := adminKit.VerifyToken(ctx.Request.Context(), token, "") + if err != nil { + return false, nil, err + } + return true, claims, nil } middlewareProvider := middleware.NewMiddlewareProvider(logger.Global) diff --git a/internal/adapters/http/register_routes.go b/internal/adapters/http/register_routes.go index 27e1618..970134b 100644 --- a/internal/adapters/http/register_routes.go +++ b/internal/adapters/http/register_routes.go @@ -20,7 +20,7 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -func RegisterRoutes(router *gin.Engine, configs config.Config, services *provider.Services, adminServices *admin.Services) (func(), error) { +func RegisterRoutes(router *gin.Engine, configs config.Config, services *provider.Services, adminServices *admin.Services, adminRepos *admin.Repositories) (func(), error) { authCfg := configs.Auth transport, err := authgin.NewCookieTransport(authgin.CookieConfig{ @@ -37,8 +37,8 @@ func RegisterRoutes(router *gin.Engine, configs config.Config, services *provide authMW := authgin.AuthMiddleware(services.AuthKit, transport, authkit.RequireAuth) handlers := handler.ProvideHandlers(services, transport) - adminHandlers := adminHandler.ProvideHandlers(adminServices, services) - mw := middlewares.Provide(configs.App, adminServices.Auth) + adminHandlers := adminHandler.ProvideHandlers(adminServices, adminRepos, services) + mw := middlewares.Provide(configs.App, adminServices.Kit) router.Use(mw.Err) diff --git a/internal/adapters/http/server.go b/internal/adapters/http/server.go index 9700430..6447459 100644 --- a/internal/adapters/http/server.go +++ b/internal/adapters/http/server.go @@ -28,7 +28,7 @@ func Setup(configs config.Config) (*httpserver.Server, func(), error) { return nil, nil, err } - routesShutdown, err := RegisterRoutes(r, configs, providers.Services, providers.AdminServices) + routesShutdown, err := RegisterRoutes(r, configs, providers.Services, providers.AdminServices, providers.AdminRepos) if err != nil { return nil, nil, err } diff --git a/internal/core/config/admin/auth_config.go b/internal/core/config/admin/auth_config.go index f44bf86..91a588b 100644 --- a/internal/core/config/admin/auth_config.go +++ b/internal/core/config/admin/auth_config.go @@ -6,7 +6,6 @@ type Auth struct { SecretKey string `split_words:"true" default:"thisissecret"` TokenDuration time.Duration `split_words:"true" default:"1h"` Issuer string `default:"cashdash"` - HashCost int `split_words:"true" default:"10"` } func (Auth) Prefix() string { diff --git a/internal/domain/service/admin/auth_service.go b/internal/domain/service/admin/auth_service.go deleted file mode 100644 index 99e580e..0000000 --- a/internal/domain/service/admin/auth_service.go +++ /dev/null @@ -1,163 +0,0 @@ -package admin - -import ( - "context" - - "github.com/google/uuid" - "github.com/itsLeonB/cashback/internal/appconstant" - "github.com/itsLeonB/cashback/internal/core/otel" - "github.com/itsLeonB/cashback/internal/core/util" - "github.com/itsLeonB/cashback/internal/domain/dto" - "github.com/itsLeonB/cashback/internal/domain/entity/admin" - "github.com/itsLeonB/ezutil/v2" - "github.com/itsLeonB/go-crud" - "github.com/itsLeonB/sekure" - "github.com/itsLeonB/ungerr" -) - -type AuthService interface { - Register(ctx context.Context, req dto.RegisterRequest) error - Login(ctx context.Context, req dto.InternalLoginRequest) (dto.TokenResponse, error) - VerifyToken(ctx context.Context, token string) (bool, map[string]any, error) - Me(ctx context.Context, id uuid.UUID) (dto.AdminMe, error) -} - -type authService struct { - userRepo crud.Repository[admin.User] - hashService sekure.HashService - jwtService sekure.JWTService -} - -func NewAuthService( - userRepo crud.Repository[admin.User], - hashService sekure.HashService, - jwtService sekure.JWTService, -) *authService { - return &authService{ - userRepo, - hashService, - jwtService, - } -} - -func (as *authService) Register(ctx context.Context, req dto.RegisterRequest) error { - ctx, span := otel.Tracer.Start(ctx, "authService.Register") - defer span.End() - - users, err := as.userRepo.FindAll(ctx, crud.Specification[admin.User]{}) - if err != nil { - return err - } - if len(users) > 0 { - return ungerr.ForbiddenError("cannot register as there exists admin users") - } - - hash, err := as.hashService.Hash(req.Password) - if err != nil { - return err - } - - newUser := admin.User{ - Email: req.Email, - Password: hash, - } - - _, err = as.userRepo.Insert(ctx, newUser) - return err -} - -func (as *authService) Login(ctx context.Context, req dto.InternalLoginRequest) (dto.TokenResponse, error) { - ctx, span := otel.Tracer.Start(ctx, "authService.Login") - defer span.End() - - spec := crud.Specification[admin.User]{} - spec.Model.Email = req.Email - user, err := as.userRepo.FindFirst(ctx, spec) - if err != nil { - return dto.TokenResponse{}, err - } - if user.IsZero() { - return dto.TokenResponse{}, ungerr.NotFoundError(appconstant.ErrAuthUnknownCredentials) - } - - ok, err := as.hashService.CheckHash(user.Password, req.Password) - if err != nil { - return dto.TokenResponse{}, err - } - if !ok { - return dto.TokenResponse{}, ungerr.NotFoundError(appconstant.ErrAuthUnknownCredentials) - } - - token, err := as.jwtService.CreateToken(map[string]any{ - appconstant.ContextUserID.String(): user.ID, - }) - if err != nil { - return dto.TokenResponse{}, err - } - - return dto.NewTokenResp(token, "", ""), nil -} - -func (as *authService) VerifyToken(ctx context.Context, token string) (bool, map[string]any, error) { - ctx, span := otel.Tracer.Start(ctx, "authService.VerifyToken") - defer span.End() - - claims, err := as.jwtService.VerifyToken(token) - if err != nil { - return false, nil, err - } - - tokenUserId, exists := claims.Data[appconstant.ContextUserID.String()] - if !exists { - return false, nil, ungerr.Unknown("missing user ID from token") - } - stringUserID, ok := tokenUserId.(string) - if !ok { - return false, nil, ungerr.Unknown("error asserting userID, is not a string") - } - userID, err := ezutil.Parse[uuid.UUID](stringUserID) - if err != nil { - return false, nil, err - } - - user, err := as.getUser(ctx, userID) - if err != nil { - return false, nil, err - } - - return true, map[string]any{ - appconstant.ContextUserID.String(): user.ID, - }, nil -} - -func (as *authService) Me(ctx context.Context, id uuid.UUID) (dto.AdminMe, error) { - ctx, span := otel.Tracer.Start(ctx, "authService.Me") - defer span.End() - - user, err := as.getUser(ctx, id) - if err != nil { - return dto.AdminMe{}, err - } - - return dto.AdminMe{ - ID: user.ID, - FullName: util.GetNameFromEmail(user.Email), - }, nil -} - -func (as *authService) getUser(ctx context.Context, id uuid.UUID) (admin.User, error) { - ctx, span := otel.Tracer.Start(ctx, "authService.getUser") - defer span.End() - - spec := crud.Specification[admin.User]{} - spec.Model.ID = id - user, err := as.userRepo.FindFirst(ctx, spec) - if err != nil { - return admin.User{}, err - } - if user.IsZero() { - return admin.User{}, ungerr.UnauthorizedError("user not found") - } - - return user, nil -} diff --git a/internal/provider/admin/repository_provider.go b/internal/provider/admin/repository_provider.go index c730445..733abb1 100644 --- a/internal/provider/admin/repository_provider.go +++ b/internal/provider/admin/repository_provider.go @@ -7,11 +7,13 @@ import ( ) type Repositories struct { - User crud.Repository[admin.User] + Transactor crud.Transactor + User crud.Repository[admin.User] } func ProvideRepositories(db *gorm.DB) *Repositories { return &Repositories{ - crud.NewRepository[admin.User](db), + Transactor: crud.NewTransactor(db), + User: crud.NewRepository[admin.User](db), } } diff --git a/internal/provider/admin/service_provider.go b/internal/provider/admin/service_provider.go index 8722b9b..4d2e10c 100644 --- a/internal/provider/admin/service_provider.go +++ b/internal/provider/admin/service_provider.go @@ -1,22 +1,44 @@ package admin import ( + "context" + + adminauth "github.com/itsLeonB/cashback/internal/adapters/auth/admin" adminConfig "github.com/itsLeonB/cashback/internal/core/config/admin" - "github.com/itsLeonB/cashback/internal/domain/service/admin" - "github.com/itsLeonB/sekure" + admin "github.com/itsLeonB/cashback/internal/domain/entity/admin" + "github.com/itsLeonB/go-authkit" + "github.com/itsLeonB/go-crud" + "github.com/itsLeonB/ungerr" ) type Services struct { - Auth admin.AuthService + Kit *authkit.AuthKit } func ProvideServices(repos *Repositories, cfg *adminConfig.Config) *Services { - - return &Services{ - admin.NewAuthService( - repos.User, - sekure.NewHashService(cfg.HashCost), - sekure.NewJwtService(cfg.Issuer, cfg.SecretKey, cfg.TokenDuration), - ), + kit, err := authkit.New(authkit.Config{ + Stateless: true, + JWTIssuer: cfg.Issuer, + JWTSecret: cfg.SecretKey, + JWTDuration: cfg.TokenDuration, + }, authkit.Deps{ + Tx: repos.Transactor, + Users: adminauth.NewUserStore(repos.User), + }, authkit.Hooks{ + BeforeRegister: func(ctx context.Context, _ string) error { + user, err := repos.User.FindFirst(ctx, crud.Specification[admin.User]{}) + if err != nil { + return err + } + if !user.IsZero() { + return ungerr.ForbiddenError("cannot register as there exists admin users") + } + return nil + }, + }) + if err != nil { + panic(err) } + + return &Services{Kit: kit} } diff --git a/internal/provider/service_provider.go b/internal/provider/service_provider.go index f748fd9..fd025b4 100644 --- a/internal/provider/service_provider.go +++ b/internal/provider/service_provider.go @@ -124,7 +124,7 @@ func ProvideServices( debt := service.NewDebtService(repos.DebtTransaction, transferMethod, friendship, profile, groupExpense, coreSvc.Queue) return &Services{ - AuthKit: authkit.New(authKitCfg, authKitDeps, hooks), + AuthKit: mustNewAuthKit(authKitCfg, authKitDeps, hooks), Captcha: service.NewTurnstileService(authConfig.TurnstileSecretKey), User: user, @@ -151,3 +151,11 @@ func ProvideServices( PushNotification: pushNotification, } } + +func mustNewAuthKit(cfg authkit.Config, deps authkit.Deps, hooks authkit.Hooks) *authkit.AuthKit { + kit, err := authkit.New(cfg, deps, hooks) + if err != nil { + panic(err) + } + return kit +}