refactor/consolidate-shared-go-modules #1
4 changed files with 1 additions and 302 deletions
|
|
@ -6,20 +6,10 @@ toolchain go1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
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/lib/pq v1.10.9
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0
|
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 (
|
require (
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
|
|
||||||
|
|
@ -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/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 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
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.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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
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 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue