diff --git a/internal/infrastructure/go.mod b/internal/infrastructure/go.mod index f1e7218..4d124c7 100644 --- a/internal/infrastructure/go.mod +++ b/internal/infrastructure/go.mod @@ -6,20 +6,10 @@ toolchain go1.24.7 require ( github.com/golang-migrate/migrate/v4 v4.19.0 - github.com/gorilla/mux v1.8.1 - github.com/jobs-scraper/libs/ports v0.0.0 - github.com/jobs-scraper/libs/repo v0.0.0 - github.com/jobs-scraper/internal/utils v0.0.0 github.com/lib/pq v1.10.9 github.com/rabbitmq/amqp091-go v1.10.0 ) -replace github.com/jobs-scraper/libs/repo => ../../libs/repo - -replace github.com/jobs-scraper/internal/utils => ../utils - -replace github.com/jobs-scraper/libs/ports => ../../libs/ports - require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/internal/infrastructure/go.sum b/internal/infrastructure/go.sum index 4880d04..92379c3 100644 --- a/internal/infrastructure/go.sum +++ b/internal/infrastructure/go.sum @@ -28,7 +28,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -67,5 +66,6 @@ go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXe go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/infrastructure/http/job.go b/internal/infrastructure/http/job.go deleted file mode 100644 index f6618b3..0000000 --- a/internal/infrastructure/http/job.go +++ /dev/null @@ -1,148 +0,0 @@ -package http - -import ( - "encoding/json" - "fmt" - "log/slog" - "net/http" - "strconv" - - "github.com/jobs-scraper/libs/ports" - "github.com/jobs-scraper/libs/repo" - "github.com/jobs-scraper/internal/utils" - - "github.com/gorilla/mux" -) - -// JobHandler handles HTTP requests related to jobs -type JobHandler struct { - jobCommands *ports.JobCommands - jobAnalysisResultRepo *repo.JobAnalysisResultRepository -} - -// NewJobHandler creates a new instance of JobHandler -func NewJobHandler(jobCommands *ports.JobCommands, jobAnalysisResultRepo *repo.JobAnalysisResultRepository) *JobHandler { - return &JobHandler{ - jobCommands: jobCommands, - jobAnalysisResultRepo: jobAnalysisResultRepo, - } -} - -// RegisterRoutes registers all routes to the router -func (h *JobHandler) RegisterRoutes(router *mux.Router) { - jobs := router.PathPrefix("/jobs").Subrouter() - - //jobs.Use(AuthMiddleware) - - jobs.HandleFunc("", utils.Make(h.CreateJob)).Methods("POST") - jobs.HandleFunc("/analysis", utils.Make(h.GetAllAnalysisResults)).Methods("GET") - jobs.HandleFunc("/{id}/analysis", utils.Make(h.GetJobAnalysisResult)).Methods("GET") - jobs.HandleFunc("/analysis/top-matches", utils.Make(h.GetTopMatches)).Methods("GET") -} - -// CreateJob handles the creation of a new job -// @Summary Create a new job -// @Description Creates a new job with the provided specifications -// @Tags jobs -// @Accept json -// @Produce json -// @Param job body ports.CreateJobCommand true "Job creation data" -// @Success 201 {object} map[string]string "Successfully created job" -// @Failure 400 {object} map[string]string "Invalid request data" -// @Failure 500 {object} map[string]string "Internal server error" -// @Router /jobs [post] -func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) error { - var cmd ports.CreateJobCommand - if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil { - slog.Error(err.Error()) - return utils.NewAPIError(http.StatusBadRequest, utils.InvalidJSON()) - } - - err := h.jobCommands.CreateJob.Handle(cmd) - if err != nil { - slog.Error(err.Error()) - return utils.NewAPIError(http.StatusInternalServerError, err) - } - - utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"}) - return nil -} - -// GetAllAnalysisResults handles retrieving all job analysis results -// @Summary Get all job analysis results -// @Description Retrieves all job analysis results from the database -// @Tags jobs -// @Produce json -// @Success 200 {array} domain.JobAnalysisResult "List of job analysis results" -// @Failure 500 {object} map[string]string "Internal server error" -// @Router /jobs/analysis [get] -func (h *JobHandler) GetAllAnalysisResults(w http.ResponseWriter, r *http.Request) error { - results, err := h.jobAnalysisResultRepo.GetAllAnalysisResults() - if err != nil { - slog.Error("Failed to get analysis results", "error", err) - return utils.NewAPIError(http.StatusInternalServerError, err) - } - - utils.WriteJSON(w, http.StatusOK, results) - return nil -} - -// GetJobAnalysisResult handles retrieving analysis result for a specific job -// @Summary Get job analysis result by job ID -// @Description Retrieves the analysis result for a specific job -// @Tags jobs -// @Produce json -// @Param id path int true "Job ID" -// @Success 200 {object} domain.JobAnalysisResult "Job analysis result" -// @Failure 400 {object} map[string]string "Invalid job ID" -// @Failure 404 {object} map[string]string "Analysis result not found" -// @Failure 500 {object} map[string]string "Internal server error" -// @Router /jobs/{id}/analysis [get] -func (h *JobHandler) GetJobAnalysisResult(w http.ResponseWriter, r *http.Request) error { - vars := mux.Vars(r) - jobID, err := strconv.ParseInt(vars["id"], 10, 64) - if err != nil { - return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID")) - } - - result, err := h.jobAnalysisResultRepo.GetAnalysisResultByJobID(jobID) - if err != nil { - slog.Error("Failed to get analysis result", "jobID", jobID, "error", err) - return utils.NewAPIError(http.StatusNotFound, fmt.Errorf("analysis result not found")) - } - - utils.WriteJSON(w, http.StatusOK, result) - return nil -} - -// GetTopMatches handles retrieving top matching jobs based on analysis score -// @Summary Get top matching jobs -// @Description Retrieves jobs with high analysis match scores -// @Tags jobs -// @Produce json -// @Param min_score query int false "Minimum match score (default: 70)" -// @Success 200 {array} domain.JobAnalysisResult "List of top matching job analysis results" -// @Failure 400 {object} map[string]string "Invalid minimum score" -// @Failure 500 {object} map[string]string "Internal server error" -// @Router /jobs/analysis/top-matches [get] -func (h *JobHandler) GetTopMatches(w http.ResponseWriter, r *http.Request) error { - minScoreStr := r.URL.Query().Get("min_score") - minScore := 70 // default minimum score - - if minScoreStr != "" { - var err error - minScore, err = strconv.Atoi(minScoreStr) - if err != nil { - return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid minimum score")) - } - } - - results, err := h.jobAnalysisResultRepo.GetAnalysisResultsByMatchScore(minScore) - if err != nil { - slog.Error("Failed to get top matches", "minScore", minScore, "error", err) - return utils.NewAPIError(http.StatusInternalServerError, err) - } - - utils.WriteJSON(w, http.StatusOK, results) - return nil -} diff --git a/internal/infrastructure/http/middlewares.go b/internal/infrastructure/http/middlewares.go deleted file mode 100644 index 3b6e0da..0000000 --- a/internal/infrastructure/http/middlewares.go +++ /dev/null @@ -1,143 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "log" - "net/http" - "strings" - "time" -) - -// CORSMiddleware handles Cross-Origin Resource Sharing -func CORSMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Access-Control-Max-Age", "86400") - - // Handle preflight OPTIONS requests - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - next.ServeHTTP(w, r) - }) -} - -// LogsMiddleware logs HTTP requests with timing information -func LogsMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - next.ServeHTTP(w, r) - log.Printf("%s %s %s", r.Method, r.RequestURI, time.Since(start)) - }) -} - -// AuthMiddleware handles JWT token validation, API keys, and basic auth -func AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip auth for swagger endpoints and health checks - if strings.HasPrefix(r.URL.Path, "/swagger/") || r.URL.Path == "/health" { - next.ServeHTTP(w, r) - return - } - - // Try Bearer token authentication first - authHeader := r.Header.Get("Authorization") - if authHeader != "" { - if strings.HasPrefix(authHeader, "Bearer ") { - token := strings.TrimPrefix(authHeader, "Bearer ") - if token != "" { - userID, err := validateJWTToken(token) - if err == nil { - ctx := context.WithValue(r.Context(), "user_id", userID) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - } - } - } - - // Try API Key authentication - apiKey := r.Header.Get("X-API-Key") - if apiKey != "" { - if isValidAPIKey(apiKey) { - ctx := context.WithValue(r.Context(), "user_id", getUserIDFromAPIKey(apiKey)) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - } - - // Try Basic authentication - username, password, ok := r.BasicAuth() - if ok { - if isValidCredentials(username, password) { - ctx := context.WithValue(r.Context(), "username", username) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - } - - // Fallback to cookie-based auth (existing behavior) - token, err := r.Cookie("token") - if err == nil && token.Value == "123" { - next.ServeHTTP(w, r) - return - } - - // No valid authentication found - response := map[string]interface{}{ - "error": "Unauthorized", - "message": "Valid authentication required. Use Bearer token, API key, or basic auth.", - "code": http.StatusUnauthorized, - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(response) - }) -} - -// Helper functions for authentication validation -func validateJWTToken(token string) (string, error) { - // TODO: Implement JWT validation logic with your preferred JWT library - // For demonstration, accept a demo token - if token == "valid-jwt-token" { - return "user-123", nil - } - return "", http.ErrAbortHandler -} - -func isValidAPIKey(apiKey string) bool { - // TODO: Implement API key validation logic - // For demonstration, accept a demo API key - return apiKey == "demo-api-key-123" -} - -func getUserIDFromAPIKey(apiKey string) string { - // TODO: Implement logic to get user ID from API key - // For demonstration, return a mock user ID - return "api-user-123" -} - -func isValidCredentials(username, password string) bool { - // TODO: Implement credential validation logic - // For demonstration, accept demo credentials - return username == "admin" && password == "password" -} - -// GetUserIDFromContext extracts user ID from request context -func GetUserIDFromContext(ctx context.Context) (string, bool) { - userID, ok := ctx.Value("user_id").(string) - return userID, ok -} - -// GetUsernameFromContext extracts username from request context -func GetUsernameFromContext(ctx context.Context) (string, bool) { - username, ok := ctx.Value("username").(string) - return username, ok -}