diff --git a/.gitea/workflows/extension-build.yml b/.gitea/workflows/extension-build.yml index 17486ab..157fd81 100644 --- a/.gitea/workflows/extension-build.yml +++ b/.gitea/workflows/extension-build.yml @@ -6,7 +6,8 @@ on: paths: - 'extension/**' - '.gitea/workflows/extension-build.yml' - + - 'extension/.env' + jobs: build: runs-on: arm diff --git a/extension/.env b/extension/.env index 0c0e1f5..fa1cf39 100644 --- a/extension/.env +++ b/extension/.env @@ -1 +1 @@ -VITE_API_BASE_URL=http://localhost:8080/api/v1 +VITE_API_BASE_URL=https://insight.buildapp.eu.org/api/v1 diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 37c681a..0d5ab0d 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -60,6 +60,9 @@ func main() { userSvc := service.NewUserService(userRepo) userHandler := handler.NewUserHandler(userSvc) + authSvc := service.NewAuthService(userRepo) + authHandler := handler.NewAuthHandler(authSvc) + profileRepo := repository.NewProductProfileRepository(db) profileSvc := service.NewProductProfileService(profileRepo) profileHandler := handler.NewProductProfileHandler(profileSvc) @@ -120,6 +123,7 @@ func main() { r.Route("/api/v1", func(r chi.Router) { // Public routes r.Post("/users/register", userHandler.Register) + r.Post("/auth/login", authHandler.Login) // Protected routes r.Group(func(r chi.Router) { diff --git a/server/internal/handler/auth_handler.go b/server/internal/handler/auth_handler.go new file mode 100644 index 0000000..dcba737 --- /dev/null +++ b/server/internal/handler/auth_handler.go @@ -0,0 +1,44 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "github.com/zs/InsightReply/internal/service" +) + +type AuthHandler struct { + svc *service.AuthService +} + +func NewAuthHandler(svc *service.AuthService) *AuthHandler { + return &AuthHandler{svc: svc} +} + +// Login handles user authentication and returns a JWT token +func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { + var body struct { + Email string `json:"email"` + Password string `json:"password"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + SendError(w, http.StatusBadRequest, 4001, "Invalid request body") + return + } + + if body.Email == "" || body.Password == "" { + SendError(w, http.StatusBadRequest, 4001, "Email and Password are required") + return + } + + token, err := h.svc.Login(body.Email, body.Password) + if err != nil { + SendError(w, http.StatusUnauthorized, 4001, err.Error()) + return + } + + SendSuccess(w, map[string]string{ + "token": token, + }) +} diff --git a/server/internal/handler/user_handler.go b/server/internal/handler/user_handler.go index c4877e7..04ef79f 100644 --- a/server/internal/handler/user_handler.go +++ b/server/internal/handler/user_handler.go @@ -18,6 +18,7 @@ func NewUserHandler(svc *service.UserService) *UserHandler { func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { var body struct { Email string `json:"email"` + Password string `json:"password"` Identity string `json:"identity"` } @@ -26,7 +27,7 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { return } - user, err := h.svc.Register(body.Email, body.Identity) + user, err := h.svc.Register(body.Email, body.Password, body.Identity) if err != nil { SendError(w, http.StatusInternalServerError, 5001, "Failed to register user") return diff --git a/server/internal/service/auth_service.go b/server/internal/service/auth_service.go new file mode 100644 index 0000000..54ca790 --- /dev/null +++ b/server/internal/service/auth_service.go @@ -0,0 +1,53 @@ +package service + +import ( + "errors" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/zs/InsightReply/internal/repository" + "golang.org/x/crypto/bcrypt" +) + +type AuthService struct { + userRepo *repository.UserRepository +} + +func NewAuthService(userRepo *repository.UserRepository) *AuthService { + return &AuthService{userRepo: userRepo} +} + +func (s *AuthService) Login(email, password string) (string, error) { + // 1. Fetch user by email + user, err := s.userRepo.GetByEmail(email) + if err != nil { + return "", errors.New("invalid email or password") + } + + // 2. Compare password hash + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) + if err != nil { + return "", errors.New("invalid email or password") + } + + // 3. Generate JWT Token + secret := os.Getenv("JWT_SECRET") + if secret == "" { + secret = "fallback_secret_key_change_in_production" + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": user.ID.String(), + "email": user.Email, + "exp": time.Now().Add(time.Hour * 72).Unix(), // 3 days expiration + "iat": time.Now().Unix(), + }) + + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/server/internal/service/user_service.go b/server/internal/service/user_service.go index f52969b..192b329 100644 --- a/server/internal/service/user_service.go +++ b/server/internal/service/user_service.go @@ -3,6 +3,7 @@ package service import ( "github.com/zs/InsightReply/internal/model" "github.com/zs/InsightReply/internal/repository" + "golang.org/x/crypto/bcrypt" ) type UserService struct { @@ -13,12 +14,18 @@ func NewUserService(repo *repository.UserRepository) *UserService { return &UserService{repo: repo} } -func (s *UserService) Register(email string, identity string) (*model.User, error) { +func (s *UserService) Register(email string, password string, identity string) (*model.User, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + user := &model.User{ Email: email, + PasswordHash: string(hashedPassword), IdentityLabel: identity, } - err := s.repo.Create(user) + err = s.repo.Create(user) return user, err }