diff --git a/go.mod b/go.mod index 78f51dc..0364a4e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.23.0 require ( ariga.io/atlas-provider-gorm v0.5.0 github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v4 v4.5.1 + github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/pieceowater-dev/lotof.hub.proto v0.0.24 diff --git a/go.sum b/go.sum index bb31525..227e522 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= @@ -122,10 +124,6 @@ github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3P github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pieceowater-dev/lotof.hub.proto v0.0.22 h1:jGDLAACixTVetj08UvT6GQszKyw1P8OL9w95qBwmWNQ= -github.com/pieceowater-dev/lotof.hub.proto v0.0.22/go.mod h1:9uwfvIUGGzTrTIVzQ4gH8hmrRC8sgtnLUhyPN5swIME= -github.com/pieceowater-dev/lotof.hub.proto v0.0.23 h1:J4sqptho83k6EcJ7AkdWbkE4MnUYKKYHoIN5GENcvIs= -github.com/pieceowater-dev/lotof.hub.proto v0.0.23/go.mod h1:9uwfvIUGGzTrTIVzQ4gH8hmrRC8sgtnLUhyPN5swIME= github.com/pieceowater-dev/lotof.hub.proto v0.0.24 h1:ULHeaE5zOaulaSfgbl+U1wYat+PGYiNayJZpYnxuaC8= github.com/pieceowater-dev/lotof.hub.proto v0.0.24/go.mod h1:9uwfvIUGGzTrTIVzQ4gH8hmrRC8sgtnLUhyPN5swIME= github.com/pieceowater-dev/lotof.lib.gossiper/v2 v2.0.6 h1:5WEnZAd/hwMDAL8sVUoL+zO4wWeQetVO4Zo+NgxzC80= diff --git a/internal/core/cfg/cfg.go b/internal/core/cfg/cfg.go index 48632c6..91eebea 100644 --- a/internal/core/cfg/cfg.go +++ b/internal/core/cfg/cfg.go @@ -10,10 +10,13 @@ import ( ) type Config struct { - GrpcPort string - RestPort string + GrpcPort string + RestPort string + PostgresDatabaseDSN string PostgresModels []any + + SecretKey string } var ( @@ -29,8 +32,9 @@ func Inst() *Config { } instance = &Config{ - GrpcPort: getEnv("GRPC_PORT", "50051"), - RestPort: getEnv("REST_PORT", "3000"), + GrpcPort: getEnv("GRPC_PORT", "50051"), + RestPort: getEnv("REST_PORT", "3000"), + PostgresDatabaseDSN: getEnv("POSTGRES_DB_DSN", "postgres://pieceouser:pieceopassword@localhost:5432/users?sslmode=disable"), PostgresModels: []any{ // models to migration here: @@ -38,6 +42,8 @@ func Inst() *Config { &user.User{}, &frinedship.Friendship{}, }, + + SecretKey: getEnv("SECRET_KEY", "secret"), } }) return instance diff --git a/internal/pkg/auth/auth.module.go b/internal/pkg/auth/auth.module.go index 07800d6..e563d94 100644 --- a/internal/pkg/auth/auth.module.go +++ b/internal/pkg/auth/auth.module.go @@ -18,6 +18,7 @@ func New() *Module { gossiper.PostgresDB, cfg.Inst().PostgresDatabaseDSN, false, + []any{}, ) if err != nil { log.Fatalf("Failed to create database instance: %v", err) diff --git a/internal/pkg/auth/ctrl/auth.ctrl.go b/internal/pkg/auth/ctrl/auth.ctrl.go index eb99c1d..f1e8d0f 100644 --- a/internal/pkg/auth/ctrl/auth.ctrl.go +++ b/internal/pkg/auth/ctrl/auth.ctrl.go @@ -1,9 +1,15 @@ package ctrl -import "app/internal/pkg/auth/svc" +import ( + pb "app/internal/core/grpc/generated" + "app/internal/pkg/auth/svc" + "app/internal/pkg/user/ent" + "context" +) type AuthController struct { authService *svc.AuthService + pb.UnimplementedAuthServiceServer } func NewAuthController(service *svc.AuthService) *AuthController { @@ -12,4 +18,38 @@ func NewAuthController(service *svc.AuthService) *AuthController { } } -// todo: implement methods +func (a AuthController) Login(_ context.Context, request *pb.LoginRequest) (*pb.AuthResponse, error) { + token, user, err := a.authService.Login(request.Email, request.Password) + if err != nil { + return nil, err + } + + return &pb.AuthResponse{ + Token: *token, + User: &pb.User{ + Id: user.ID.String(), + Username: user.Username, + Email: user.Email, + }, + }, nil +} + +func (a AuthController) Register(_ context.Context, request *pb.RegisterRequest) (*pb.AuthResponse, error) { + token, user, err := a.authService.Register(&ent.User{ + Username: request.Username, + Email: request.Email, + Password: request.Password, + }) + if err != nil { + return nil, err + } + + return &pb.AuthResponse{ + Token: *token, + User: &pb.User{ + Id: user.ID.String(), + Username: user.Username, + Email: user.Email, + }, + }, nil +} diff --git a/internal/pkg/auth/svc/auth.svc.go b/internal/pkg/auth/svc/auth.svc.go index f283aa7..b0f9fd2 100644 --- a/internal/pkg/auth/svc/auth.svc.go +++ b/internal/pkg/auth/svc/auth.svc.go @@ -1,9 +1,11 @@ package svc import ( + "app/internal/core/cfg" "app/internal/pkg/user/ent" - "context" "errors" + "fmt" + "github.com/golang-jwt/jwt/v5" gossiper "github.com/pieceowater-dev/lotof.lib.gossiper/v2" "golang.org/x/crypto/bcrypt" "time" @@ -17,33 +19,69 @@ func NewAuthService(db gossiper.Database) *AuthService { return &AuthService{db: db} } -func (s *AuthService) Login(ctx context.Context, email, password string) (*ent.User, error) { +// Generate JWT Token +func (s *AuthService) generateJWT(user *ent.User) (*string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "userId": user.ID.String(), + "email": user.Email, + "exp": time.Now().Add(168 * time.Hour).Unix(), + }) + + secret := cfg.Inst().SecretKey + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return nil, fmt.Errorf("failed to generate token: %w", err) + } + + return &tokenString, nil +} + +func (s *AuthService) Login(email, password string) (*string, *ent.User, error) { var user ent.User - if err := s.db.GetDB().Where("email = ? AND deleted = ?", email, false).First(&user).Error; err != nil { - return nil, errors.New("incorrect user or password") + + if err := s.db.GetDB().Where("email = ? AND deleted_at IS NULL", email).First(&user).Error; err != nil { + return nil, nil, errors.New("invalid email or password") } - // Compare hashed password + // Check user state todo: implement this logic later + //if user.State != ent.Active { + // return nil, nil, errors.New("account is not active") + //} + + // Validate password if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return nil, errors.New("incorrect user or password") + return nil, nil, errors.New("invalid email or password") + } + + // Generate JWT token + token, err := s.generateJWT(&user) + if err != nil { + return nil, nil, err } - return &user, nil + return token, &user, nil } -func (s *AuthService) Register(ctx context.Context, user *ent.User) (*ent.User, error) { - // Hash the password before saving +func (s *AuthService) Register(user *ent.User) (*string, *ent.User, error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { - return nil, err + return nil, nil, err } user.Password = string(hashedPassword) user.CreatedAt = time.Now() + user.State = ent.Suspended + // Save user to DB if err := s.db.GetDB().Create(user).Error; err != nil { - return nil, err + return nil, nil, err + } + + // Generate JWT token + token, err := s.generateJWT(user) + if err != nil { + return nil, nil, err } - return user, nil + return token, user, nil } diff --git a/internal/pkg/router.go b/internal/pkg/router.go index eea224a..cb73f35 100644 --- a/internal/pkg/router.go +++ b/internal/pkg/router.go @@ -2,6 +2,7 @@ package pkg import ( pb "app/internal/core/grpc/generated" + "app/internal/pkg/auth" "app/internal/pkg/friendship" "app/internal/pkg/user" "github.com/gin-gonic/gin" @@ -9,15 +10,15 @@ import ( ) type Router struct { - userModule *user.Module - //authModule *auth.Module + userModule *user.Module + authModule *auth.Module friendshipModule *friendship.Module } func NewRouter() *Router { return &Router{ - userModule: user.New(), - //authModule: auth.New(), + userModule: user.New(), + authModule: auth.New(), friendshipModule: friendship.New(), } } @@ -26,6 +27,7 @@ func NewRouter() *Router { func (r *Router) InitGRPC(grpcServer *grpc.Server) { // Register gRPC services pb.RegisterUserServiceServer(grpcServer, r.userModule.Controller) + pb.RegisterAuthServiceServer(grpcServer, r.authModule.Controller) pb.RegisterFriendshipServiceServer(grpcServer, r.friendshipModule.Controller) } diff --git a/internal/pkg/user/ent/user.ent.go b/internal/pkg/user/ent/user.ent.go index 70e85f2..55e1f0a 100644 --- a/internal/pkg/user/ent/user.ent.go +++ b/internal/pkg/user/ent/user.ent.go @@ -2,7 +2,6 @@ package ent import ( "github.com/google/uuid" - "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) @@ -24,30 +23,10 @@ type User struct { Friends []*User `gorm:"many2many:friendships;joinForeignKey:UserID;joinReferences:FriendID"` } -// BeforeCreate Hook for generating custom UUID and password hashing +// BeforeCreate Hook for generating custom UUID func (u *User) BeforeCreate(tx *gorm.DB) (err error) { // Generate custom UUID for user u.ID = uuid.New() - - // Hash password if it's not empty - if u.Password != "" { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) - if err != nil { - return err - } - u.Password = string(hashedPassword) - } - return nil -} - -// BeforeSave Hook for updating timestamp and hashing password (if necessary) -func (u *User) BeforeSave(tx *gorm.DB) (err error) { - if u.Password != "" { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) - if err != nil { - return err - } - u.Password = string(hashedPassword) - } + //todo: generate hashed password here ONLY return nil } diff --git a/internal/pkg/user/svc/user.svc.go b/internal/pkg/user/svc/user.svc.go index 5ad1b77..b1a65c1 100644 --- a/internal/pkg/user/svc/user.svc.go +++ b/internal/pkg/user/svc/user.svc.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" gossiper "github.com/pieceowater-dev/lotof.lib.gossiper/v2" + "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) @@ -17,7 +18,12 @@ func NewUserService(db gossiper.Database) *UserService { return &UserService{db: db} } -func (s *UserService) CreateUser(user *ent.User) (*ent.User, error) { +func (s *UserService) CreateUser(user *ent.User) (*ent.User, error) { // todo: delete this later + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + user.Password = string(hashedPassword) if err := s.db.GetDB().Create(user).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, errors.New("email already exists") @@ -85,14 +91,3 @@ func (s *UserService) GetUsers(filter gossiper.Filter[string]) (gossiper.Paginat // Create paginated result return gossiper.NewPaginatedResult(grpcUsers, int(count)), nil } - -//func (s *UserService) AddFriend(userID, friendID string) error { -// var user, friend ent.User -// if err := s.db.GetDB().First(&user, "id = ?", userID).Error; err != nil { -// return err -// } -// if err := s.db.GetDB().First(&friend, "id = ?", friendID).Error; err != nil { -// return err -// } -// return s.db.GetDB().Model(&user).Association("Friends").Append(&friend) -//}