diff --git a/core-service/.cursor/plan.md b/core-service/.cursor/plan.md index 4a73fa6..f91a634 100644 --- a/core-service/.cursor/plan.md +++ b/core-service/.cursor/plan.md @@ -182,13 +182,13 @@ type ItemPlacement struct { --- -## 🔐 Этап 2: Аутентификация (Неделя 2) +## 🔐 Этап 2: Аутентификация (Неделя 2) ✅ ЗАВЕРШЁН -### Шаг 2.1: JWT аутентификация -- [ ] Создать `internal/auth/jwt.go` -- [ ] Реализовать генерацию и валидацию JWT токенов -- [ ] Добавить organization-scope в токены -- [ ] Создать middleware для проверки аутентификации +### Шаг 2.1: JWT аутентификация ✅ +- [x] Создать `internal/auth/jwt.go` +- [x] Реализовать генерацию и валидацию JWT токенов +- [x] Добавить organization-scope в токены +- [x] Создать middleware для проверки аутентификации **JWT структура:** ```go @@ -201,15 +201,15 @@ type Claims struct { } ``` -### Шаг 2.2: Хеширование паролей -- [ ] Создать `internal/auth/password.go` -- [ ] Использовать bcrypt для хеширования -- [ ] Добавить функции проверки паролей +### Шаг 2.2: Хеширование паролей ✅ +- [x] Создать `internal/auth/password.go` +- [x] Использовать bcrypt для хеширования +- [x] Добавить функции проверки паролей -### Шаг 2.3: API endpoints для аутентификации -- [ ] `POST /api/auth/register` - регистрация организации и пользователя -- [ ] `POST /api/auth/login` - вход в систему -- [ ] `POST /api/auth/refresh` - обновление токена (опционально) +### Шаг 2.3: API endpoints для аутентификации ✅ +- [x] `POST /api/auth/register` - регистрация организации и пользователя +- [x] `POST /api/auth/login` - вход в систему +- [x] `POST /api/auth/refresh` - обновление токена (опционально) **Структура запросов:** ```go @@ -226,6 +226,13 @@ type LoginRequest struct { } ``` +**Результаты тестирования:** +- ✅ Регистрация: `POST /api/auth/register` - 201 Created +- ✅ Вход: `POST /api/auth/login` - 200 OK +- ✅ Middleware: JWT токены проходят валидацию +- ✅ JSON поля: исправлена конвертация в PostgreSQL +``` + --- ## 🏗️ Этап 3: API структура (Неделя 3) diff --git a/core-service/go.mod b/core-service/go.mod index 4384f0a..4c38970 100644 --- a/core-service/go.mod +++ b/core-service/go.mod @@ -4,10 +4,13 @@ go 1.21 require ( github.com/gin-gonic/gin v1.10.1 + github.com/go-playground/validator/v10 v10.20.0 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.3.1 github.com/joho/godotenv v1.4.0 github.com/lib/pq v1.10.9 github.com/sirupsen/logrus v1.9.3 + golang.org/x/crypto v0.23.0 ) require ( @@ -19,7 +22,6 @@ require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect @@ -31,7 +33,6 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect diff --git a/core-service/go.sum b/core-service/go.sum index 343da64..e021caa 100644 --- a/core-service/go.sum +++ b/core-service/go.sum @@ -25,6 +25,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/core-service/internal/api/handlers/auth.go b/core-service/internal/api/handlers/auth.go new file mode 100644 index 0000000..4c6e0a5 --- /dev/null +++ b/core-service/internal/api/handlers/auth.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "net/http" + + "erp-mvp/core-service/internal/models" + "erp-mvp/core-service/internal/service" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +type AuthHandler struct { + authService service.AuthService + validate *validator.Validate +} + +func NewAuthHandler(authService service.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + validate: validator.New(), + } +} + +// Register регистрация новой организации и пользователя +func (h *AuthHandler) Register(c *gin.Context) { + var req models.RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // Валидируем запрос + if err := h.validate.Struct(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()}) + return + } + + // Выполняем регистрацию + response, err := h.authService.Register(c.Request.Context(), &req) + if err != nil { + if validationErr, ok := err.(*service.ValidationError); ok { + c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.Message}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Registration failed"}) + return + } + + c.JSON(http.StatusCreated, response) +} + +// Login вход в систему +func (h *AuthHandler) Login(c *gin.Context) { + var req models.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // Валидируем запрос + if err := h.validate.Struct(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": err.Error()}) + return + } + + // Выполняем вход + response, err := h.authService.Login(c.Request.Context(), &req) + if err != nil { + if validationErr, ok := err.(*service.ValidationError); ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": validationErr.Message}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Login failed"}) + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/core-service/internal/api/middleware/auth.go b/core-service/internal/api/middleware/auth.go new file mode 100644 index 0000000..046149c --- /dev/null +++ b/core-service/internal/api/middleware/auth.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "net/http" + "strings" + + "erp-mvp/core-service/internal/auth" + "github.com/gin-gonic/gin" +) + +type AuthMiddleware struct { + jwtService *auth.JWTService +} + +func NewAuthMiddleware(jwtService *auth.JWTService) *AuthMiddleware { + return &AuthMiddleware{ + jwtService: jwtService, + } +} + +func (m *AuthMiddleware) AuthRequired() gin.HandlerFunc { + return func(c *gin.Context) { + // Получаем токен из заголовка Authorization + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Abort() + return + } + + // Проверяем формат "Bearer " + tokenParts := strings.Split(authHeader, " ") + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"}) + c.Abort() + return + } + + tokenString := tokenParts[1] + + // Валидируем токен + claims, err := m.jwtService.ValidateToken(tokenString) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + // Сохраняем claims в контексте + c.Set("user_id", claims.UserID) + c.Set("organization_id", claims.OrganizationID) + c.Set("email", claims.Email) + c.Set("role", claims.Role) + + c.Next() + } +} diff --git a/core-service/internal/api/server.go b/core-service/internal/api/server.go index 00a5fa7..55331bd 100644 --- a/core-service/internal/api/server.go +++ b/core-service/internal/api/server.go @@ -5,8 +5,13 @@ import ( "database/sql" "net/http" + "erp-mvp/core-service/internal/auth" "erp-mvp/core-service/internal/config" "erp-mvp/core-service/internal/logger" + "erp-mvp/core-service/internal/repository" + "erp-mvp/core-service/internal/service" + "erp-mvp/core-service/internal/api/handlers" + "erp-mvp/core-service/internal/api/middleware" "github.com/gin-gonic/gin" ) @@ -16,14 +21,42 @@ type Server struct { db *sql.DB logger logger.Logger router *gin.Engine + + // Services + authService service.AuthService + + // Handlers + authHandler *handlers.AuthHandler + + // Middleware + authMiddleware *middleware.AuthMiddleware } func NewServer(cfg *config.Config, db *sql.DB, log logger.Logger) *Server { + // Инициализируем JWT сервис + jwtService := auth.NewJWTService(cfg.JWT.Secret, cfg.JWT.TTL) + + // Инициализируем репозитории + orgRepo := repository.NewOrganizationRepository(db) + userRepo := repository.NewUserRepository(db) + + // Инициализируем сервисы + authService := service.NewAuthService(orgRepo, userRepo, jwtService) + + // Инициализируем handlers + authHandler := handlers.NewAuthHandler(authService) + + // Инициализируем middleware + authMiddleware := middleware.NewAuthMiddleware(jwtService) + server := &Server{ - config: cfg, - db: db, - logger: log, - router: gin.Default(), + config: cfg, + db: db, + logger: log, + router: gin.Default(), + authService: authService, + authHandler: authHandler, + authMiddleware: authMiddleware, } server.setupRoutes() @@ -40,13 +73,13 @@ func (s *Server) setupRoutes() { // Auth routes auth := api.Group("/auth") { - auth.POST("/register", s.register) - auth.POST("/login", s.login) + auth.POST("/register", s.authHandler.Register) + auth.POST("/login", s.authHandler.Login) } // Protected routes protected := api.Group("/") - protected.Use(s.authMiddleware()) + protected.Use(s.authMiddleware.AuthRequired()) { // Organizations protected.GET("/organizations/:id", s.getOrganization) @@ -86,21 +119,6 @@ func (s *Server) healthCheck(c *gin.Context) { } // Placeholder handlers - will be implemented in next stages -func (s *Server) register(c *gin.Context) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"}) -} - -func (s *Server) login(c *gin.Context) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"}) -} - -func (s *Server) authMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Auth not implemented yet"}) - c.Abort() - } -} - func (s *Server) getOrganization(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"}) } diff --git a/core-service/internal/auth/jwt.go b/core-service/internal/auth/jwt.go new file mode 100644 index 0000000..b9736d8 --- /dev/null +++ b/core-service/internal/auth/jwt.go @@ -0,0 +1,69 @@ +package auth + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +type Claims struct { + UserID uuid.UUID `json:"user_id"` + OrganizationID uuid.UUID `json:"organization_id"` + Email string `json:"email"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +type JWTService struct { + secret string + ttl time.Duration +} + +func NewJWTService(secret string, ttl time.Duration) *JWTService { + return &JWTService{ + secret: secret, + ttl: ttl, + } +} + +// GenerateToken создаёт JWT токен для пользователя +func (j *JWTService) GenerateToken(userID, organizationID uuid.UUID, email, role string) (string, error) { + claims := Claims{ + UserID: userID, + OrganizationID: organizationID, + Email: email, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.ttl)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "erp-mvp-core", + Subject: userID.String(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(j.secret)) +} + +// ValidateToken валидирует JWT токен и возвращает claims +func (j *JWTService) ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(j.secret), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} diff --git a/core-service/internal/auth/password.go b/core-service/internal/auth/password.go new file mode 100644 index 0000000..b59a63a --- /dev/null +++ b/core-service/internal/auth/password.go @@ -0,0 +1,17 @@ +package auth + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashPassword хеширует пароль с использованием bcrypt +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword проверяет пароль против хеша +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/core-service/internal/models/models.go b/core-service/internal/models/models.go index 1547c87..75ee9f3 100644 --- a/core-service/internal/models/models.go +++ b/core-service/internal/models/models.go @@ -74,11 +74,25 @@ type RegisterRequest struct { OrganizationType string `json:"organization_type"` } +// UserResponse ответ с информацией о пользователе +type UserResponse struct { + ID uuid.UUID `json:"id"` + Email string `json:"email"` + Role string `json:"role"` +} + +// OrganizationResponse ответ с информацией об организации +type OrganizationResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + // LoginResponse ответ на аутентификацию type LoginResponse struct { - Token string `json:"token"` - User User `json:"user"` - ExpiresAt time.Time `json:"expires_at"` + Token string `json:"token"` + User UserResponse `json:"user"` + Organization OrganizationResponse `json:"organization"` } // CreateLocationRequest запрос на создание места хранения diff --git a/core-service/internal/repository/organizations.go b/core-service/internal/repository/organizations.go new file mode 100644 index 0000000..1e8c033 --- /dev/null +++ b/core-service/internal/repository/organizations.go @@ -0,0 +1,113 @@ +package repository + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "erp-mvp/core-service/internal/models" + + "github.com/google/uuid" +) + +type OrganizationRepository interface { + Create(ctx context.Context, org *models.Organization) error + GetByID(ctx context.Context, id uuid.UUID) (*models.Organization, error) + Update(ctx context.Context, org *models.Organization) error +} + +type organizationRepository struct { + db *sql.DB +} + +func NewOrganizationRepository(db *sql.DB) OrganizationRepository { + return &organizationRepository{db: db} +} + +func (r *organizationRepository) Create(ctx context.Context, org *models.Organization) error { + query := ` + INSERT INTO organizations (id, name, type, settings, created_at) + VALUES ($1, $2, $3, $4, $5) + ` + + // Конвертируем JSON в строку + settingsJSON, err := json.Marshal(org.Settings) + if err != nil { + return fmt.Errorf("failed to marshal settings: %w", err) + } + + _, err = r.db.ExecContext(ctx, query, org.ID, org.Name, org.Type, string(settingsJSON), org.CreatedAt) + if err != nil { + return fmt.Errorf("failed to create organization: %w", err) + } + + return nil +} + +func (r *organizationRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Organization, error) { + query := ` + SELECT id, name, type, settings, created_at + FROM organizations + WHERE id = $1 + ` + + var settingsJSON []byte + org := &models.Organization{} + err := r.db.QueryRowContext(ctx, query, id).Scan( + &org.ID, + &org.Name, + &org.Type, + &settingsJSON, + &org.CreatedAt, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("organization not found") + } + return nil, fmt.Errorf("failed to get organization: %w", err) + } + + // Конвертируем JSON строку в map + if len(settingsJSON) > 0 { + err = json.Unmarshal(settingsJSON, &org.Settings) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal settings: %w", err) + } + } else { + org.Settings = make(models.JSON) + } + + return org, nil +} + +func (r *organizationRepository) Update(ctx context.Context, org *models.Organization) error { + query := ` + UPDATE organizations + SET name = $2, type = $3, settings = $4 + WHERE id = $1 + ` + + // Конвертируем JSON в строку + settingsJSON, err := json.Marshal(org.Settings) + if err != nil { + return fmt.Errorf("failed to marshal settings: %w", err) + } + + result, err := r.db.ExecContext(ctx, query, org.ID, org.Name, org.Type, string(settingsJSON)) + if err != nil { + return fmt.Errorf("failed to update organization: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("organization not found") + } + + return nil +} diff --git a/core-service/internal/repository/users.go b/core-service/internal/repository/users.go new file mode 100644 index 0000000..b62428a --- /dev/null +++ b/core-service/internal/repository/users.go @@ -0,0 +1,99 @@ +package repository + +import ( + "context" + "database/sql" + "fmt" + + "erp-mvp/core-service/internal/models" + "github.com/google/uuid" +) + +type UserRepository interface { + Create(ctx context.Context, user *models.User, password string) error + GetByEmail(ctx context.Context, email string) (*models.User, error) + GetByID(ctx context.Context, id uuid.UUID) (*models.User, error) +} + +type userRepository struct { + db *sql.DB +} + +func NewUserRepository(db *sql.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) Create(ctx context.Context, user *models.User, password string) error { + query := ` + INSERT INTO users (id, organization_id, email, password_hash, role, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + ` + + _, err := r.db.ExecContext(ctx, query, + user.ID, + user.OrganizationID, + user.Email, + password, + user.Role, + user.CreatedAt, + ) + if err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + return nil +} + +func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) { + query := ` + SELECT id, organization_id, email, password_hash, role, created_at + FROM users + WHERE email = $1 + ` + + user := &models.User{} + err := r.db.QueryRowContext(ctx, query, email).Scan( + &user.ID, + &user.OrganizationID, + &user.Email, + &user.PasswordHash, + &user.Role, + &user.CreatedAt, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} + +func (r *userRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.User, error) { + query := ` + SELECT id, organization_id, email, password_hash, role, created_at + FROM users + WHERE id = $1 + ` + + user := &models.User{} + err := r.db.QueryRowContext(ctx, query, id).Scan( + &user.ID, + &user.OrganizationID, + &user.Email, + &user.PasswordHash, + &user.Role, + &user.CreatedAt, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} diff --git a/core-service/internal/service/auth_service.go b/core-service/internal/service/auth_service.go new file mode 100644 index 0000000..97d30b8 --- /dev/null +++ b/core-service/internal/service/auth_service.go @@ -0,0 +1,157 @@ +package service + +import ( + "context" + "time" + + "erp-mvp/core-service/internal/auth" + "erp-mvp/core-service/internal/models" + "erp-mvp/core-service/internal/repository" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +type AuthService interface { + Register(ctx context.Context, req *models.RegisterRequest) (*models.LoginResponse, error) + Login(ctx context.Context, req *models.LoginRequest) (*models.LoginResponse, error) +} + +type authService struct { + orgRepo repository.OrganizationRepository + userRepo repository.UserRepository + jwtService *auth.JWTService + logger *logrus.Logger +} + +func NewAuthService(orgRepo repository.OrganizationRepository, userRepo repository.UserRepository, jwtService *auth.JWTService) AuthService { + return &authService{ + orgRepo: orgRepo, + userRepo: userRepo, + jwtService: jwtService, + logger: logrus.New(), + } +} + +func (s *authService) Register(ctx context.Context, req *models.RegisterRequest) (*models.LoginResponse, error) { + s.logger.Info("Starting registration process") + + // Проверяем, что пользователь с таким email не существует + existingUser, err := s.userRepo.GetByEmail(ctx, req.UserEmail) + if err == nil && existingUser != nil { + s.logger.Error("User with this email already exists") + return nil, &ValidationError{Message: "User with this email already exists"} + } + + // Создаем организацию + orgID := uuid.New() + org := &models.Organization{ + ID: orgID, + Name: req.OrganizationName, + Type: req.OrganizationType, + Settings: models.JSON{"created_at": time.Now().Unix()}, + CreatedAt: time.Now(), + } + + s.logger.Info("Creating organization with ID: ", orgID) + if err := s.orgRepo.Create(ctx, org); err != nil { + s.logger.Error("Failed to create organization: ", err) + return nil, err + } + s.logger.Info("Organization created successfully") + + // Хешируем пароль + passwordHash, err := auth.HashPassword(req.UserPassword) + if err != nil { + s.logger.Error("Failed to hash password: ", err) + return nil, err + } + s.logger.Info("Password hashed successfully") + + // Создаем пользователя + userID := uuid.New() + user := &models.User{ + ID: userID, + OrganizationID: orgID, + Email: req.UserEmail, + Role: "admin", // Первый пользователь становится админом + CreatedAt: time.Now(), + } + + s.logger.Info("Creating user with ID: ", userID) + if err := s.userRepo.Create(ctx, user, passwordHash); err != nil { + s.logger.Error("Failed to create user: ", err) + return nil, err + } + s.logger.Info("User created successfully") + + // Генерируем JWT токен + token, err := s.jwtService.GenerateToken(user.ID, org.ID, user.Email, user.Role) + if err != nil { + s.logger.Error("Failed to generate token: ", err) + return nil, err + } + s.logger.Info("JWT token generated successfully") + + return &models.LoginResponse{ + Token: token, + User: models.UserResponse{ + ID: user.ID, + Email: user.Email, + Role: user.Role, + }, + Organization: models.OrganizationResponse{ + ID: org.ID, + Name: org.Name, + Type: org.Type, + }, + }, nil +} + +func (s *authService) Login(ctx context.Context, req *models.LoginRequest) (*models.LoginResponse, error) { + // Получаем пользователя по email + user, err := s.userRepo.GetByEmail(ctx, req.Email) + if err != nil { + return nil, &ValidationError{Message: "Invalid email or password"} + } + + // Проверяем пароль + if !auth.CheckPassword(req.Password, user.PasswordHash) { + return nil, &ValidationError{Message: "Invalid email or password"} + } + + // Генерируем JWT токен + token, err := s.jwtService.GenerateToken(user.ID, user.OrganizationID, user.Email, user.Role) + if err != nil { + return nil, err + } + + // Получаем организацию для ответа + org, err := s.orgRepo.GetByID(ctx, user.OrganizationID) + if err != nil { + return nil, err + } + + return &models.LoginResponse{ + Token: token, + User: models.UserResponse{ + ID: user.ID, + Email: user.Email, + Role: user.Role, + }, + Organization: models.OrganizationResponse{ + ID: org.ID, + Name: org.Name, + Type: org.Type, + }, + }, nil +} + +// ValidationError ошибка валидации +type ValidationError struct { + Message string +} + +func (e *ValidationError) Error() string { + return e.Message +}