diff --git a/services/authentication/generate_code.go b/services/authentication/generate_code.go new file mode 100644 index 0000000..2c4ef79 --- /dev/null +++ b/services/authentication/generate_code.go @@ -0,0 +1,93 @@ +package authentication + +import ( + "errors" + "fmt" + "math/rand" + "netina/models" + "time" + + "netina/validation" + + "gorm.io/gorm" +) + +type GenerateCode struct { + DB *gorm.DB +} + +func NewGenerateCode(db *gorm.DB) *GenerateCode { + return &GenerateCode{DB: db} +} + +// Generate a random 6-digit code +func generateCode() string { + rand.Seed(time.Now().UnixNano()) + return fmt.Sprintf("%06d", rand.Intn(1000000)) +} + +// CreateLoginCode generates a login code and stores it in the database +func (s *GenerateCode) CreateLoginCode(phoneNumber string) (string, error) { + var code string + var isUnique bool + + // Try to generate a unique code + for !isUnique { + code = generateCode() + isUnique, _ = s.isCodeUnique(code) + } + + now := time.Now() + expiresAt := now.Add(2 * time.Minute) + + loginCode := &models.LoginCode{ + PhoneNumber: phoneNumber, + Code: code, + CreatedAt: now, + ExpiresAt: expiresAt, + } + + if err := validation.ValidateStruct(loginCode); err != nil { + return "" , err + } + + if err := s.DB.Create(loginCode).Error; err != nil { + return "", err + } + + return code, nil +} + +// Check if the generated code is unique +func (s *GenerateCode) isCodeUnique(code string) (bool, error) { + var loginCode models.LoginCode + if err := s.DB.Where("code = ?", code).First(&loginCode).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, nil + } + return false, err + } + return false, nil +} + +// VerifyLoginCode verifies the login code and checks if it is still valid +func (s *GenerateCode) VerifyLoginCode(phoneNumber, code string) error { + var loginCode models.LoginCode + + // Find the login code + if err := s.DB.Where("phone_number = ? AND code = ?", phoneNumber, code).First(&loginCode).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("code not found") + } + return err + } + + // Check if the code has expired + if time.Now().After(loginCode.ExpiresAt) { + return errors.New("code expired") + } + + return nil +} + + diff --git a/services/authentication/jwt.go b/services/authentication/jwt.go new file mode 100644 index 0000000..40d237e --- /dev/null +++ b/services/authentication/jwt.go @@ -0,0 +1,99 @@ +package authentication + +import ( + "errors" + "netina/database" + Role_repository "netina/repositories/role" + User_repository "netina/repositories/user" + "os" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/joho/godotenv" +) + +// JWTClaims defines the structure of JWT claims. +type JWTClaims struct { + ID uint `json:"id"` + Role string `json:"role"` + jwt.StandardClaims +} + +// Load the environment variables from the .env file +func loadEnv() { + err := godotenv.Load("./config/.env") + if err != nil { + panic("Error loading .env file") + } +} + +// GenerateJWT generates a new JWT token. +func GenerateJWT(claims *JWTClaims) (string, error) { + loadEnv() + secretKey := os.Getenv("SECRET_KEY") + if secretKey == "" { + return "", errors.New("SECRET_KEY is not set in the environment variables") + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(secretKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// ValidateJWT validates a given JWT token. +func ValidateJWT(tokenString string) (*JWTClaims, error) { + loadEnv() + secretKey := os.Getenv("SECRET_KEY") + if secretKey == "" { + return nil, errors.New("SECRET_KEY is not set in the environment variables") + } + + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(secretKey), nil + }) + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} + +// GenerateClaims creates JWT claims. +func GenerateClaims(userID uint) (*JWTClaims , error) { + db := database.Db() + role_query := Role_repository.RoleQueryRepository{DB: &db} + user_query := User_repository.UserQueryRepository{DB: &db} + + user , err := user_query.GetUser(userID) + if err != nil { + return nil , err + } + + role , err := role_query.GetRole(user.Role_id) + if err != nil { + return nil , err + } + + cliams := &JWTClaims{ + ID: user.User_id, + Role: role.Name, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Hour * 2).Unix(), + + }, + } + + return cliams , nil + +} diff --git a/services/authentication/login.go b/services/authentication/login.go new file mode 100644 index 0000000..f57f58e --- /dev/null +++ b/services/authentication/login.go @@ -0,0 +1,73 @@ +package authentication + +import ( + "net/http" + "netina/database" + "netina/models" + u "netina/queries" + user_repository "netina/repositories/user" + sms "netina/services" + "netina/validation" + + "github.com/labstack/echo/v4" +) + +func RequestCode(c echo.Context) error { + db := database.Db() + + gc := NewGenerateCode(&db) + + phoneNumber := new(models.CreatePhoneNumber) + if err := c.Bind(phoneNumber); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request payload"}) + } + + if err := validation.ValidateStruct(phoneNumber); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + loginCode ,err := gc.CreateLoginCode(phoneNumber.PhoneNumber) + if err != nil { + return err + } + sms.Sms(phoneNumber.PhoneNumber , loginCode) + return nil +} + + +func Login(c echo.Context) error { + db := database.Db() + code := new(models.LoginCode) + if err := c.Bind(code); err != nil { + return err + } + + if err := validation.ValidateStruct(code); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + gc := NewGenerateCode(&db) + + if gc.VerifyLoginCode(code.PhoneNumber , code.Code) == nil { + ur := user_repository.UserQueryRepository{DB: &db} + userHandller := u.GetUserByPhoneNumberHandler{Repository: ur} + user , err := userHandller.Handle(code.PhoneNumber) + if err != nil { + return c.JSON(http.StatusNotFound , "user not found") + } + + token , err:= GenerateTokenForUser(user.User_id) + if err != nil { + return err + } + + return c.JSON(http.StatusAccepted , token) + + } else { + return c.JSON(http.StatusBadRequest , "invalid Request") + } + +} + + + diff --git a/services/authentication/user_jwt.go b/services/authentication/user_jwt.go new file mode 100644 index 0000000..4c25582 --- /dev/null +++ b/services/authentication/user_jwt.go @@ -0,0 +1,21 @@ +package authentication + +func GenerateTokenForUser(userID uint) (string, error) { + claims, err := GenerateClaims(userID) + if err != nil { + return "", err + } + token, err := GenerateJWT(claims) + if err != nil { + return "", err + } + return token, nil +} + +func ValidateUserToken(tokenString string) (*JWTClaims, error) { + claims, err := ValidateJWT(tokenString) + if err != nil { + return nil, err + } + return claims, nil +}