diff --git a/.env b/.env deleted file mode 100644 index 9d6b86c..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -# .env -JWT_SECRET= "389f1f43c4ef9fceb0acf38e3a48859332798de1b13ca5fbfd0182ee0e413478" diff --git a/.gitignore b/.gitignore index d660137..4e232e8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ go.mod 2024-2-Zdes-budet-nazvanie-UykwHnIE.crt data/ +.env diff --git a/Makefile b/Makefile index c43e391..df84c53 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,23 @@ PKG=./... MOCKGEN=mockgen COVERAGE_FILE=coverage.out -MOCK_SRC_PLACES=internal/pkg/places/interfaces.go -MOCK_DST_PLACES=internal/pkg/places/mocks/mock.go +MOCK_SRC_PLACES=internal/pkg/attractions/interfaces.go +MOCK_DST_PLACES=internal/pkg/attractions/mocks/mock.go MOCK_SRC_USER=internal/pkg/user/interfaces.go MOCK_DST_USER=internal/pkg/user/mocks/mock.go +MOCK_SRC_TRIPS=internal/pkg/trips/interfaces.go +MOCK_DST_TRIPS=internal/pkg/trips/mocks/mock_trips.go +PACKAGE_NAME=mocks PACKAGE_NAME_USER=user -PACKAGE_NAME_PLACES=places - +PACKAGE_NAME_PLACES=attractions +PACKAGE_NAME_trips= all: test mocks: - $(MOCKGEN) -source=$(MOCK_SRC_PLACES) -destination=$(MOCK_DST_PLACES) -package=$(PACKAGE_NAME_PLACES) - $(MOCKGEN) -source=$(MOCK_SRC_USER) -destination=$(MOCK_DST_USER) -package=$(PACKAGE_NAME_USER) + $(MOCKGEN) -source=$(MOCK_SRC_PLACES) -destination=$(MOCK_DST_PLACES) -package=$(PACKAGE_NAME) + $(MOCKGEN) -source=$(MOCK_SRC_USER) -destination=$(MOCK_DST_USER) -package=$(PACKAGE_NAME) + $(MOCKGEN) -source=$(MOCK_SRC_TRIPS) -destination=$(MOCK_DST_TRIPS) -package=$(PACKAGE_NAME) test: mocks go test $(PKG) -coverprofile=$(COVERAGE_FILE) @@ -38,6 +42,4 @@ run: build_ .PHONY: lint lint: - golangci-lint run --config=.golangci.yaml - - + golangci-lint run --config=.golangci.yaml \ No newline at end of file diff --git a/build/attraction.Dockerfile b/build/attraction.Dockerfile new file mode 100644 index 0000000..da2a43f --- /dev/null +++ b/build/attraction.Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.23.1-alpine AS builder +COPY . /github.com/go-park-mail-ru/2024_2_ThereWillBeName/attractions +WORKDIR /github.com/go-park-mail-ru/2024_2_ThereWillBeName/attractions +RUN go mod download +RUN go clean --modcache +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o ./.bin ./cmd/attractions/main.go +FROM scratch AS runner +WORKDIR /build +COPY --from=builder /github.com/go-park-mail-ru/2024_2_ThereWillBeName/attractions/.bin . +EXPOSE 8081 +ENTRYPOINT ["./.bin"] diff --git a/build/gateway.Dockerfile b/build/gateway.Dockerfile new file mode 100644 index 0000000..7bbb61c --- /dev/null +++ b/build/gateway.Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.23.1-alpine AS builder +COPY . /github.com/go-park-mail-ru/2024_2_ThereWillBeName/gateway +WORKDIR /github.com/go-park-mail-ru/2024_2_ThereWillBeName/gateway +RUN go mod download +RUN go clean --modcache +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o ./.bin ./cmd/gateway/main.go +FROM scratch AS runner +WORKDIR /build +COPY --from=builder /github.com/go-park-mail-ru/2024_2_ThereWillBeName/gateway/.bin . +EXPOSE 8080 +ENTRYPOINT ["./.bin"] diff --git a/build/survey.Dockerfile b/build/survey.Dockerfile new file mode 100644 index 0000000..d99dce5 --- /dev/null +++ b/build/survey.Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.23.1-alpine AS builder +COPY . /github.com/go-park-mail-ru/2024_2_ThereWillBeName/survey +WORKDIR /github.com/go-park-mail-ru/2024_2_ThereWillBeName/survey +RUN go mod download +RUN go clean --modcache +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o ./.bin ./cmd/survey/main.go +FROM scratch AS runner +WORKDIR /build +COPY --from=builder /github.com/go-park-mail-ru/2024_2_ThereWillBeName/survey/.bin . +EXPOSE 50054 +ENTRYPOINT ["./.bin"] diff --git a/build/trips.Dockerfile b/build/trips.Dockerfile new file mode 100644 index 0000000..b6f7752 --- /dev/null +++ b/build/trips.Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.23.1-alpine AS builder +COPY . /github.com/go-park-mail-ru/2024_2_ThereWillBeName/trips +WORKDIR /github.com/go-park-mail-ru/2024_2_ThereWillBeName/trips +RUN go mod download +RUN go clean --modcache +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o ./.bin ./cmd/trips/main.go +FROM scratch AS runner +WORKDIR /build +COPY --from=builder /github.com/go-park-mail-ru/2024_2_ThereWillBeName/trips/.bin . +EXPOSE 50053 +ENTRYPOINT ["./.bin"] diff --git a/build/users.Dockerfile b/build/users.Dockerfile new file mode 100644 index 0000000..395cabd --- /dev/null +++ b/build/users.Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.23.1-alpine AS builder +COPY . /github.com/go-park-mail-ru/2024_2_ThereWillBeName/users +WORKDIR /github.com/go-park-mail-ru/2024_2_ThereWillBeName/users +RUN go mod download +RUN go clean --modcache +RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o ./.bin ./cmd/users/main.go +FROM scratch AS runner +WORKDIR /build +COPY --from=builder /github.com/go-park-mail-ru/2024_2_ThereWillBeName/users/.bin . +EXPOSE 50052 +ENTRYPOINT ["./.bin"] diff --git a/cmd/attractions/main.go b/cmd/attractions/main.go new file mode 100644 index 0000000..404e53b --- /dev/null +++ b/cmd/attractions/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "2024_2_ThereWillBeName/internal/models" + grpcAttractions "2024_2_ThereWillBeName/internal/pkg/attractions/delivery/grpc" + genPlaces "2024_2_ThereWillBeName/internal/pkg/attractions/delivery/grpc/gen" + placeRepo "2024_2_ThereWillBeName/internal/pkg/attractions/repo" + placeUsecase "2024_2_ThereWillBeName/internal/pkg/attractions/usecase" + grpcCategories "2024_2_ThereWillBeName/internal/pkg/categories/delivery/grpc" + genCategories "2024_2_ThereWillBeName/internal/pkg/categories/delivery/grpc/gen" + categoriesRepo "2024_2_ThereWillBeName/internal/pkg/categories/repo" + categoriesUsecase "2024_2_ThereWillBeName/internal/pkg/categories/usecase" + grpcCities "2024_2_ThereWillBeName/internal/pkg/cities/delivery/grpc" + genCities "2024_2_ThereWillBeName/internal/pkg/cities/delivery/grpc/gen" + citiesRepo "2024_2_ThereWillBeName/internal/pkg/cities/repo" + citiesUsecase "2024_2_ThereWillBeName/internal/pkg/cities/usecase" + "2024_2_ThereWillBeName/internal/pkg/dblogger" + "2024_2_ThereWillBeName/internal/pkg/logger" + grpcReviews "2024_2_ThereWillBeName/internal/pkg/reviews/delivery/grpc" + genReviews "2024_2_ThereWillBeName/internal/pkg/reviews/delivery/grpc/gen" + reviewRepo "2024_2_ThereWillBeName/internal/pkg/reviews/repo" + reviewUsecase "2024_2_ThereWillBeName/internal/pkg/reviews/usecase" + grpcSearch "2024_2_ThereWillBeName/internal/pkg/search/delivery/grpc" + genSearch "2024_2_ThereWillBeName/internal/pkg/search/delivery/grpc/gen" + searchRepo "2024_2_ThereWillBeName/internal/pkg/search/repo" + searchUsecase "2024_2_ThereWillBeName/internal/pkg/search/usecase" + "database/sql" + "flag" + "log" + "log/slog" + "net" + "os" + "os/signal" + "strconv" + "syscall" + + _ "github.com/lib/pq" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +func main() { + var cfg models.ConfigGrpc + flag.IntVar(&cfg.Port, "grpc-port", 50051, "gRPC server port") + flag.StringVar(&cfg.ConnStr, "connStr", "host=tripdb port=5432 user=service password=test dbname=trip sslmode=disable", "PostgreSQL connection string") + flag.Parse() + + logger := setupLogger() + + db, err := sql.Open("postgres", cfg.ConnStr) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer db.Close() + + wrappedDB := dblogger.NewDB(db, logger) + + reviewsRepo := reviewRepo.NewReviewRepository(wrappedDB) + reviewUsecase := reviewUsecase.NewReviewsUsecase(reviewsRepo) + placeRepo := placeRepo.NewPLaceRepository(wrappedDB) + placeUsecase := placeUsecase.NewPlaceUsecase(placeRepo) + citiesRepo := citiesRepo.NewCitiesRepository(wrappedDB) + citiesUsecase := citiesUsecase.NewCitiesUsecase(citiesRepo) + categoriesRepo := categoriesRepo.NewCategoriesRepo(wrappedDB) + categoriesUsecase := categoriesUsecase.NewCategoriesUsecase(categoriesRepo) + searchRepo := searchRepo.NewSearchRepository(wrappedDB) + searchUsecase := searchUsecase.NewSearchUsecase(searchRepo) + + grpcAttractionsServer := grpc.NewServer() + + attractionsHandler := grpcAttractions.NewGrpcAttractionsHandler(placeUsecase) + genPlaces.RegisterAttractionsServer(grpcAttractionsServer, attractionsHandler) + + citiesHandler := grpcCities.NewGrpcCitiesHandler(citiesUsecase) + genCities.RegisterCitiesServer(grpcAttractionsServer, citiesHandler) + + reviewsHandler := grpcReviews.NewGrpcReviewsHandler(reviewUsecase) + genReviews.RegisterReviewsServer(grpcAttractionsServer, reviewsHandler) + + categoriesHandler := grpcCategories.NewGrpcCategoriesHandler(categoriesUsecase) + genCategories.RegisterCategoriesServer(grpcAttractionsServer, categoriesHandler) + + searchHandler := grpcSearch.NewGrpcSearchHandler(searchUsecase, logger) + genSearch.RegisterSearchServer(grpcAttractionsServer, searchHandler) + + reflection.Register(grpcAttractionsServer) + + go func() { + listener, err := net.Listen("tcp", ":8081") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + log.Printf("gRPC server listening on :%d", cfg.Port) + if err := grpcAttractionsServer.Serve(listener); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + }() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + log.Println("Shutting down gRPC server...") + grpcAttractionsServer.GracefulStop() + log.Println("gRPC server gracefully stopped") +} + +func setupLogger() *slog.Logger { + + levelEnv := os.Getenv("LOG_LEVEL") + logLevel := slog.LevelDebug + if level, err := strconv.Atoi(levelEnv); err == nil { + logLevel = slog.Level(level) + } + + opts := logger.PrettyHandlerOptions{ + SlogOpts: slog.HandlerOptions{ + Level: logLevel, + }, + } + + handler := logger.NewPrettyHandler(os.Stdout, opts) + + return slog.New(handler) +} diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go new file mode 100644 index 0000000..a0894ce --- /dev/null +++ b/cmd/gateway/main.go @@ -0,0 +1,223 @@ +package main + +import ( + "2024_2_ThereWillBeName/internal/models" + genAttractions "2024_2_ThereWillBeName/internal/pkg/attractions/delivery/grpc/gen" + httpPlaces "2024_2_ThereWillBeName/internal/pkg/attractions/delivery/http" + genCategories "2024_2_ThereWillBeName/internal/pkg/categories/delivery/grpc/gen" + httpCategories "2024_2_ThereWillBeName/internal/pkg/categories/delivery/http" + genCities "2024_2_ThereWillBeName/internal/pkg/cities/delivery/grpc/gen" + httpCities "2024_2_ThereWillBeName/internal/pkg/cities/delivery/http" + "2024_2_ThereWillBeName/internal/pkg/httpresponses" + httpresponse "2024_2_ThereWillBeName/internal/pkg/httpresponses" + "2024_2_ThereWillBeName/internal/pkg/jwt" + "2024_2_ThereWillBeName/internal/pkg/logger" + "2024_2_ThereWillBeName/internal/pkg/middleware" + genReviews "2024_2_ThereWillBeName/internal/pkg/reviews/delivery/grpc/gen" + httpReviews "2024_2_ThereWillBeName/internal/pkg/reviews/delivery/http" + genSearch "2024_2_ThereWillBeName/internal/pkg/search/delivery/grpc/gen" + httpSearch "2024_2_ThereWillBeName/internal/pkg/search/delivery/http" + genSurvey "2024_2_ThereWillBeName/internal/pkg/survey/delivery/grpc/gen" + httpSurvey "2024_2_ThereWillBeName/internal/pkg/survey/delivery/http" + genTrips "2024_2_ThereWillBeName/internal/pkg/trips/delivery/grpc/gen" + httpTrips "2024_2_ThereWillBeName/internal/pkg/trips/delivery/http" + genUsers "2024_2_ThereWillBeName/internal/pkg/user/delivery/grpc/gen" + httpUsers "2024_2_ThereWillBeName/internal/pkg/user/delivery/http" + "context" + "errors" + "flag" + "fmt" + "log" + "log/slog" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + + _ "github.com/lib/pq" + + "github.com/gorilla/mux" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func main() { + var cfg models.Config + flag.IntVar(&cfg.Port, "port", 8080, "API server port") + flag.StringVar(&cfg.Env, "env", "production", "Environment") + flag.StringVar(&cfg.AllowedOrigin, "allowed-origin", "*", "Allowed origin") + flag.Parse() + + logger := setupLogger() + + jwtSecret := os.Getenv("JWT_SECRET") + jwtHandler := jwt.NewJWT(jwtSecret, logger) + + attractionsConn, err := grpc.Dial("attractions:8081", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect to attractions service: %v", err) + } + defer attractionsConn.Close() + + attractionsClient := genAttractions.NewAttractionsClient(attractionsConn) + categoriesClient := genCategories.NewCategoriesClient(attractionsConn) + citiesClient := genCities.NewCitiesClient(attractionsConn) + reviewsClient := genReviews.NewReviewsClient(attractionsConn) + searchClient := genSearch.NewSearchClient(attractionsConn) + + usersConn, err := grpc.NewClient("users:50052", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect to users service: %v", err) + } + defer usersConn.Close() + usersClient := genUsers.NewUserServiceClient(usersConn) + + tripsConn, err := grpc.NewClient("trips:50053", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect to trips service: %v", err) + } + defer tripsConn.Close() + tripsClient := genTrips.NewTripsClient(tripsConn) + + surveyConn, err := grpc.NewClient("survey:50054", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("did not connect to survey service: %v", err) + } + defer surveyConn.Close() + surveyClient := genSurvey.NewSurveyServiceClient(surveyConn) + + // Инициализация HTTP сервера + corsMiddleware := middleware.NewCORSMiddleware([]string{cfg.AllowedOrigin}) + r := mux.NewRouter().PathPrefix("/api/v1").Subrouter() + r.Use(corsMiddleware.CorsMiddleware) + + r.Use(middleware.RequestLoggerMiddleware(logger)) + + // Обработка ненайденных маршрутов + r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := httpresponses.ErrorResponse{ + Message: "Not found", + } + httpresponses.SendJSONResponse(w, response, http.StatusNotFound, logger) + }) + + // Маршрут для healthcheck + r.HandleFunc("/healthcheck", healthcheckHandler).Methods(http.MethodGet) + + // Маршруты для attractions + placesHandler := httpPlaces.NewPlacesHandler(attractionsClient, logger) + places := r.PathPrefix("/places").Subrouter() + places.HandleFunc("", placesHandler.GetPlacesHandler).Methods(http.MethodGet) + places.HandleFunc("/search", placesHandler.SearchPlacesHandler).Methods(http.MethodGet) + places.HandleFunc("/{id}", placesHandler.GetPlaceHandler).Methods(http.MethodGet) + places.HandleFunc("/category/{categoryName}", placesHandler.GetPlacesByCategoryHandler).Methods(http.MethodGet) + + categoriesHandler := httpCategories.NewCategoriesHandler(categoriesClient, logger) + categories := r.PathPrefix("/categories").Subrouter() + categories.HandleFunc("", categoriesHandler.GetCategoriesHandler).Methods(http.MethodGet) + + reviewsHandler := httpReviews.NewReviewHandler(reviewsClient, logger) + reviews := places.PathPrefix("/{placeID}/reviews").Subrouter() + reviews.Handle("", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(reviewsHandler.CreateReviewHandler), logger)).Methods(http.MethodPost) + reviews.Handle("/{reviewID}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(reviewsHandler.UpdateReviewHandler), logger)).Methods(http.MethodPut) + reviews.Handle("/{reviewID}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(reviewsHandler.DeleteReviewHandler), logger)).Methods(http.MethodDelete) + reviews.HandleFunc("/{reviewID}", reviewsHandler.GetReviewHandler).Methods(http.MethodGet) + reviews.HandleFunc("", reviewsHandler.GetReviewsByPlaceIDHandler).Methods(http.MethodGet) + + citiesHandler := httpCities.NewCitiesHandler(citiesClient, logger) + cities := r.PathPrefix("/cities").Subrouter() + cities.HandleFunc("/search", citiesHandler.SearchCitiesByNameHandler).Methods(http.MethodGet) + cities.HandleFunc("/{id}", citiesHandler.SearchCityByIDHandler).Methods(http.MethodGet) + + searchHandler := httpSearch.NewSearchHandler(searchClient, logger) + search := r.PathPrefix("/search").Subrouter() + search.HandleFunc("", searchHandler.Search).Methods(http.MethodGet) + + //Маршруты для Users + usersHandler := httpUsers.NewUserHandler(usersClient, jwtHandler, logger) + auth := r.PathPrefix("/auth").Subrouter() + auth.HandleFunc("/signup", usersHandler.SignUp).Methods(http.MethodPost) + auth.HandleFunc("/login", usersHandler.Login).Methods(http.MethodPost) + auth.Handle("/logout", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(usersHandler.Logout), logger)).Methods(http.MethodPost) + + users := r.PathPrefix("/users").Subrouter() + users.Handle("/me", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(usersHandler.CurrentUser), logger)).Methods(http.MethodGet) + + user := users.PathPrefix("/{userID}").Subrouter() + + user.Handle("/avatars", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(usersHandler.UploadAvatar), logger)).Methods(http.MethodPut) + user.Handle("/profile", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(usersHandler.GetProfile), logger)).Methods(http.MethodGet) + user.Handle("/update/password", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(usersHandler.UpdatePassword), logger)).Methods(http.MethodPut) + user.Handle("/profile", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(usersHandler.UpdateProfile), logger)).Methods(http.MethodPut) + + tripsHandler := httpTrips.NewTripHandler(tripsClient, logger) + trips := r.PathPrefix("/trips").Subrouter() + trips.Handle("/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.GetTripHandler), logger)).Methods(http.MethodGet) + trips.Handle("", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.CreateTripHandler), logger)).Methods(http.MethodPost) + trips.Handle("/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.UpdateTripHandler), logger)).Methods(http.MethodPut) + trips.Handle("/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.DeleteTripHandler), logger)).Methods(http.MethodDelete) + trips.Handle("/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.AddPlaceToTripHandler), logger)).Methods(http.MethodPost) + user.Handle("/trips", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.GetTripsByUserIDHandler), logger)).Methods(http.MethodGet) + trips.Handle("/{id}/photos", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.AddPhotosToTripHandler), logger)).Methods(http.MethodPut) + trips.Handle("/{id}/photos", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(tripsHandler.DeletePhotoHandler), logger)).Methods(http.MethodDelete) + + surveyHandler := httpSurvey.NewSurveyHandler(surveyClient, logger) + survey := r.PathPrefix("/survey").Subrouter() + survey.Handle("/stats/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(surveyHandler.GetSurveyStatsBySurveyId), logger)).Methods(http.MethodGet) + survey.Handle("/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(surveyHandler.GetSurveyById), logger)).Methods(http.MethodGet) + survey.Handle("/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(surveyHandler.CreateSurveyResponse), logger)).Methods(http.MethodPost) + survey.Handle("/users/{id}", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(surveyHandler.GetSurveyStatsByUserId), logger)).Methods(http.MethodGet) + + httpSrv := &http.Server{Handler: r, Addr: fmt.Sprintf(":%d", cfg.Port)} + go func() { + logger.Info("HTTP server listening on :%d", cfg.Port) + if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error("failed to serve HTTP: %d", err) + os.Exit(1) + } + }() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + logger.Info("Shutting down HTTP server...") + if err := httpSrv.Shutdown(context.Background()); err != nil { + logger.Error("HTTP server shutdown failed: %v", err) + os.Exit(1) + } + logger.Info("HTTP server gracefully stopped") +} + +func healthcheckHandler(w http.ResponseWriter, r *http.Request) { + logger := setupLogger() + + _, err := fmt.Fprintf(w, "STATUS: OK") + if err != nil { + logger.Error("Failed to write healthcheck response", slog.Any("error", err)) + response := httpresponse.ErrorResponse{ + Message: "Invalid request", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, logger) + } +} + +func setupLogger() *slog.Logger { + + levelEnv := os.Getenv("LOG_LEVEL") + logLevel := slog.LevelDebug + if level, err := strconv.Atoi(levelEnv); err == nil { + logLevel = slog.Level(level) + } + + opts := logger.PrettyHandlerOptions{ + SlogOpts: slog.HandlerOptions{ + Level: logLevel, + }, + } + + handler := logger.NewPrettyHandler(os.Stdout, opts) + + return slog.New(handler) +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index e9cae24..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,201 +0,0 @@ -package main - -import ( - "2024_2_ThereWillBeName/internal/models" - httpresponse "2024_2_ThereWillBeName/internal/pkg/httpresponses" - "2024_2_ThereWillBeName/internal/pkg/jwt" - "2024_2_ThereWillBeName/internal/pkg/logger" - "2024_2_ThereWillBeName/internal/pkg/middleware" - httpHandler "2024_2_ThereWillBeName/internal/pkg/user/delivery/http" - userRepo "2024_2_ThereWillBeName/internal/pkg/user/repo" - userUsecase "2024_2_ThereWillBeName/internal/pkg/user/usecase" - "log/slog" - "strconv" - - citieshandler "2024_2_ThereWillBeName/internal/pkg/cities/delivery/http" - citiesrepo "2024_2_ThereWillBeName/internal/pkg/cities/repo" - citiesusecase "2024_2_ThereWillBeName/internal/pkg/cities/usecase" - delivery "2024_2_ThereWillBeName/internal/pkg/places/delivery/http" - placeRepo "2024_2_ThereWillBeName/internal/pkg/places/repo" - placeUsecase "2024_2_ThereWillBeName/internal/pkg/places/usecase" - reviewhandler "2024_2_ThereWillBeName/internal/pkg/reviews/delivery/http" - reviewrepo "2024_2_ThereWillBeName/internal/pkg/reviews/repo" - reviewusecase "2024_2_ThereWillBeName/internal/pkg/reviews/usecase" - triphandler "2024_2_ThereWillBeName/internal/pkg/trips/delivery/http" - triprepo "2024_2_ThereWillBeName/internal/pkg/trips/repo" - tripusecase "2024_2_ThereWillBeName/internal/pkg/trips/usecase" - - "database/sql" - "flag" - "fmt" - "net/http" - "os" - "time" - - _ "2024_2_ThereWillBeName/docs" - - "github.com/gorilla/mux" - _ "github.com/lib/pq" - httpSwagger "github.com/swaggo/http-swagger" -) - -func main() { - var cfg models.Config - flag.IntVar(&cfg.Port, "port", 8080, "API server port") - flag.StringVar(&cfg.Env, "env", "production", "Environment") - flag.StringVar(&cfg.AllowedOrigin, "allowed-origin", "*", "Allowed origin") - flag.StringVar(&cfg.ConnStr, "connStr", "host=tripdb port=5432 user=service password=test dbname=trip sslmode=disable", "PostgreSQL connection string") - flag.Parse() - - logger := setupLogger() - defer logger.Info("Server stopped") - - db, err := openDB(cfg.ConnStr) - if err != nil { - logger.Error("Failed to open database", slog.Any("error", err)) - } - logger.Info("Connected to database successfully") - defer db.Close() - - jwtSecret := os.Getenv("JWT_SECRET") - storagePath := os.Getenv("AVATAR_STORAGE_PATH") - - userRepo := userRepo.NewAuthRepository(db) - jwtHandler := jwt.NewJWT(string(jwtSecret), logger) - userUseCase := userUsecase.NewUserUsecase(userRepo, storagePath) - h := httpHandler.NewUserHandler(userUseCase, jwtHandler, logger) - - reviewsRepo := reviewrepo.NewReviewRepository(db) - reviewUsecase := reviewusecase.NewReviewsUsecase(reviewsRepo) - reviewHandler := reviewhandler.NewReviewHandler(reviewUsecase, logger) - placeRepo := placeRepo.NewPLaceRepository(db) - placeUsecase := placeUsecase.NewPlaceUsecase(placeRepo) - placeHandler := delivery.NewPlacesHandler(placeUsecase, logger) - tripsRepo := triprepo.NewTripRepository(db) - tripUsecase := tripusecase.NewTripsUsecase(tripsRepo) - tripHandler := triphandler.NewTripHandler(tripUsecase, logger) - citiesRepo := citiesrepo.NewCitiesRepository(db) - citiesUsecase := citiesusecase.NewCitiesUsecase(citiesRepo) - citiesHandler := citieshandler.NewCitiesHandler(citiesUsecase, logger) - - corsMiddleware := middleware.NewCORSMiddleware([]string{cfg.AllowedOrigin}) - - r := mux.NewRouter().PathPrefix("/api/v1").Subrouter() - r.Use(corsMiddleware.CorsMiddleware) - r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - response := httpresponse.ErrorResponse{ - Message: "Not found", - } - httpresponse.SendJSONResponse(w, response, http.StatusNotFound, logger) - }) - r.HandleFunc("/healthcheck", healthcheckHandler).Methods(http.MethodGet) - - auth := r.PathPrefix("/auth").Subrouter() - auth.HandleFunc("/signup", h.SignUp).Methods(http.MethodPost) - auth.HandleFunc("/login", h.Login).Methods(http.MethodPost) - auth.HandleFunc("/logout", h.Logout).Methods(http.MethodPost) - users := r.PathPrefix("/users").Subrouter() - users.Handle("/me", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(h.CurrentUser), logger)).Methods(http.MethodGet) - - user := users.PathPrefix("/{userID}").Subrouter() - - user.Handle("/avatars", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(h.UploadAvatar), logger)).Methods(http.MethodPut) - user.Handle("/profile", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(h.GetProfile), logger)).Methods(http.MethodGet) - - places := r.PathPrefix("/places").Subrouter() - places.HandleFunc("", placeHandler.GetPlacesHandler).Methods(http.MethodGet) - places.HandleFunc("", placeHandler.PostPlaceHandler).Methods(http.MethodPost) - places.HandleFunc("/search/{placeName}", placeHandler.SearchPlacesHandler).Methods(http.MethodGet) - places.HandleFunc("/{id}", placeHandler.GetPlaceHandler).Methods(http.MethodGet) - places.HandleFunc("/{id}", placeHandler.PutPlaceHandler).Methods(http.MethodPut) - places.HandleFunc("/{id}", placeHandler.DeletePlaceHandler).Methods(http.MethodDelete) - - r.PathPrefix("/swagger").Handler(httpSwagger.WrapHandler) - - reviews := places.PathPrefix("/{placeID}/reviews").Subrouter() - reviews.HandleFunc("", reviewHandler.CreateReviewHandler).Methods(http.MethodPost) - reviews.HandleFunc("/{reviewID}", reviewHandler.UpdateReviewHandler).Methods(http.MethodPut) - reviews.HandleFunc("/{reviewID}", reviewHandler.DeleteReviewHandler).Methods(http.MethodDelete) - reviews.HandleFunc("/{reviewID}", reviewHandler.GetReviewHandler).Methods(http.MethodGet) - reviews.HandleFunc("", reviewHandler.GetReviewsByPlaceIDHandler).Methods(http.MethodGet) - - trips := r.PathPrefix("/trips").Subrouter() - trips.HandleFunc("", tripHandler.CreateTripHandler).Methods(http.MethodPost) - trips.HandleFunc("/{id}", tripHandler.UpdateTripHandler).Methods(http.MethodPut) - trips.HandleFunc("/{id}", tripHandler.DeleteTripHandler).Methods(http.MethodDelete) - trips.HandleFunc("/{id}", tripHandler.GetTripHandler).Methods(http.MethodGet) - trips.HandleFunc("/{id}", tripHandler.AddPlaceToTripHandler).Methods(http.MethodPost) - user.HandleFunc("/trips", tripHandler.GetTripsByUserIDHandler).Methods(http.MethodGet) - - cities := r.PathPrefix("/cities").Subrouter() - cities.HandleFunc("/search", citiesHandler.SearchCitiesByNameHandler).Methods(http.MethodGet) - cities.HandleFunc("/{id}", citiesHandler.SearchCityByIDHandler).Methods(http.MethodGet) - - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", cfg.Port), - Handler: r, - IdleTimeout: time.Minute, - ReadTimeout: 10 * time.Second, - WriteTimeout: 30 * time.Second, - } - - logger.Info("starting server", "environment", cfg.Env, "address", srv.Addr) - err = srv.ListenAndServe() - if err != nil { - logger.Error("Failed to start server", slog.Any("error", err)) - os.Exit(1) - } -} - -// healthcheckHandler godoc -// @Summary Health check -// @Description Check the health status of the service -// @Produce text/plain -// @Success 200 {string} string "STATUS: OK" -// @Failure 400 {object} httpresponses.ErrorResponse "Bad Request" -// @Router /healthcheck [get] -func healthcheckHandler(w http.ResponseWriter, r *http.Request) { - logger := setupLogger() - - _, err := fmt.Fprintf(w, "STATUS: OK") - if err != nil { - logger.Error("Failed to write healthcheck response", slog.Any("error", err)) - response := httpresponse.ErrorResponse{ - Message: "Invalid request", - } - httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, logger) - } -} - -func openDB(connStr string) (*sql.DB, error) { - db, err := sql.Open("postgres", connStr) - if err != nil { - return nil, err - } - - err = db.Ping() - if err != nil { - return nil, err - } - - return db, nil -} - -func setupLogger() *slog.Logger { - - levelEnv := os.Getenv("LOG_LEVEL") - logLevel := slog.LevelDebug - if level, err := strconv.Atoi(levelEnv); err == nil { - logLevel = slog.Level(level) - } - - opts := logger.PrettyHandlerOptions{ - SlogOpts: slog.HandlerOptions{ - Level: logLevel, - }, - } - - handler := logger.NewPrettyHandler(os.Stdout, opts) - - return slog.New(handler) -} diff --git a/cmd/survey/main.go b/cmd/survey/main.go new file mode 100644 index 0000000..d0ffc19 --- /dev/null +++ b/cmd/survey/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "2024_2_ThereWillBeName/internal/models" + "2024_2_ThereWillBeName/internal/pkg/logger" + grpcSurvey "2024_2_ThereWillBeName/internal/pkg/survey/delivery/grpc" + "2024_2_ThereWillBeName/internal/pkg/survey/delivery/grpc/gen" + surveyRepo "2024_2_ThereWillBeName/internal/pkg/survey/repo" + surveyUsecase "2024_2_ThereWillBeName/internal/pkg/survey/usecase" + "database/sql" + "flag" + "fmt" + _ "github.com/lib/pq" + "log" + "log/slog" + "net" + "os" + "os/signal" + "strconv" + "syscall" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +func main() { + var cfg models.ConfigGrpc + flag.IntVar(&cfg.Port, "grpc-port", 50054, "gRPC server port") + flag.StringVar(&cfg.ConnStr, "connStr", "host=tripdb port=5432 user=service password=test dbname=trip sslmode=disable", "PostgreSQL connection string") + flag.Parse() + + logger := setupLogger() + + db, err := sql.Open("postgres", cfg.ConnStr) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer db.Close() + + surveyRepoImpl := surveyRepo.NewPLaceRepository(db) + surveyUsecaseImpl := surveyUsecase.NewSurveysUsecase(surveyRepoImpl) + + grpcSurveyServer := grpc.NewServer() + surveyHandler := grpcSurvey.NewGrpcSurveyHandler(surveyUsecaseImpl, logger) + gen.RegisterSurveyServiceServer(grpcSurveyServer, surveyHandler) + reflection.Register(grpcSurveyServer) + + go func() { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + log.Printf("gRPC server listening on :%d", cfg.Port) + if err := grpcSurveyServer.Serve(listener); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + }() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + log.Println("Shutting down gRPC server...") + grpcSurveyServer.GracefulStop() + log.Println("gRPC server gracefully stopped") +} +func setupLogger() *slog.Logger { + + levelEnv := os.Getenv("LOG_LEVEL") + logLevel := slog.LevelDebug + if level, err := strconv.Atoi(levelEnv); err == nil { + logLevel = slog.Level(level) + } + + opts := logger.PrettyHandlerOptions{ + SlogOpts: slog.HandlerOptions{ + Level: logLevel, + }, + } + + handler := logger.NewPrettyHandler(os.Stdout, opts) + + return slog.New(handler) +} diff --git a/cmd/trips/main.go b/cmd/trips/main.go new file mode 100644 index 0000000..6ea5a46 --- /dev/null +++ b/cmd/trips/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "2024_2_ThereWillBeName/internal/models" + "2024_2_ThereWillBeName/internal/pkg/dblogger" + "2024_2_ThereWillBeName/internal/pkg/logger" + grpcTrips "2024_2_ThereWillBeName/internal/pkg/trips/delivery/grpc" + "2024_2_ThereWillBeName/internal/pkg/trips/delivery/grpc/gen" + tripRepo "2024_2_ThereWillBeName/internal/pkg/trips/repo" + tripUsecase "2024_2_ThereWillBeName/internal/pkg/trips/usecase" + "database/sql" + "flag" + "fmt" + "log" + "log/slog" + "net" + "os" + "os/signal" + "strconv" + "syscall" + + _ "github.com/lib/pq" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +func main() { + var cfg models.ConfigGrpc + flag.IntVar(&cfg.Port, "grpc-port", 50053, "gRPC server port") + flag.StringVar(&cfg.ConnStr, "connStr", "host=tripdb port=5432 user=service password=test dbname=trip sslmode=disable", "PostgreSQL connection string") + flag.Parse() + + logger := setupLogger() + + db, err := sql.Open("postgres", cfg.ConnStr) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer db.Close() + + wrappedDB := dblogger.NewDB(db, logger) + + tripRepo := tripRepo.NewTripRepository(wrappedDB) + tripUsecase := tripUsecase.NewTripsUsecase(tripRepo) + + grpcTripsServer := grpc.NewServer() + tripsHandler := grpcTrips.NewGrpcTripHandler(tripUsecase, logger) + gen.RegisterTripsServer(grpcTripsServer, tripsHandler) + reflection.Register(grpcTripsServer) + + go func() { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + log.Printf("gRPC server listening on :%d", cfg.Port) + if err := grpcTripsServer.Serve(listener); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + }() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + log.Println("Shutting down gRPC server...") + grpcTripsServer.GracefulStop() + log.Println("gRPC server gracefully stopped") +} +func setupLogger() *slog.Logger { + + levelEnv := os.Getenv("LOG_LEVEL") + logLevel := slog.LevelDebug + if level, err := strconv.Atoi(levelEnv); err == nil { + logLevel = slog.Level(level) + } + + opts := logger.PrettyHandlerOptions{ + SlogOpts: slog.HandlerOptions{ + Level: logLevel, + }, + } + + handler := logger.NewPrettyHandler(os.Stdout, opts) + + return slog.New(handler) +} diff --git a/cmd/users/main.go b/cmd/users/main.go new file mode 100644 index 0000000..b047f3f --- /dev/null +++ b/cmd/users/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "2024_2_ThereWillBeName/internal/models" + "2024_2_ThereWillBeName/internal/pkg/dblogger" + "2024_2_ThereWillBeName/internal/pkg/logger" + grpcUsers "2024_2_ThereWillBeName/internal/pkg/user/delivery/grpc" + "2024_2_ThereWillBeName/internal/pkg/user/delivery/grpc/gen" + userRepo "2024_2_ThereWillBeName/internal/pkg/user/repo" + userUsecase "2024_2_ThereWillBeName/internal/pkg/user/usecase" + "database/sql" + "flag" + "fmt" + "log" + "log/slog" + "net" + "os" + "os/signal" + "strconv" + "syscall" + + _ "github.com/lib/pq" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +func main() { + var cfg models.ConfigGrpc + flag.IntVar(&cfg.Port, "grpc-port", 50052, "gRPC server port") + flag.StringVar(&cfg.ConnStr, "connStr", "host=tripdb port=5432 user=service password=test dbname=trip sslmode=disable", "PostgreSQL connection string") + flag.Parse() + + logger := setupLogger() + + storagePath := os.Getenv("AVATAR_STORAGE_PATH") + + db, err := sql.Open("postgres", cfg.ConnStr) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer db.Close() + + wrappedDB := dblogger.NewDB(db, logger) + + userRepo := userRepo.NewAuthRepository(wrappedDB) + userUsecase := userUsecase.NewUserUsecase(userRepo, storagePath) + + grpcUsersServer := grpc.NewServer() + usersHandler := grpcUsers.NewGrpcUserHandler(userUsecase, logger) + gen.RegisterUserServiceServer(grpcUsersServer, usersHandler) + reflection.Register(grpcUsersServer) + + go func() { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + log.Printf("gRPC server listening on :%d", cfg.Port) + if err := grpcUsersServer.Serve(listener); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } + }() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + log.Println("Shutting down gRPC server...") + grpcUsersServer.GracefulStop() + log.Println("gRPC server gracefully stopped") +} + +func setupLogger() *slog.Logger { + + levelEnv := os.Getenv("LOG_LEVEL") + logLevel := slog.LevelDebug + if level, err := strconv.Atoi(levelEnv); err == nil { + logLevel = slog.Level(level) + } + + opts := logger.PrettyHandlerOptions{ + SlogOpts: slog.HandlerOptions{ + Level: logLevel, + }, + } + + handler := logger.NewPrettyHandler(os.Stdout, opts) + + return slog.New(handler) +} diff --git a/coverage.html b/coverage.html index d95c845..7038965 100644 --- a/coverage.html +++ b/coverage.html @@ -3,7 +3,7 @@
-