diff --git a/services/user/cmd/server/main.go b/services/user/cmd/server/main.go index d1c7b4d7..e158d727 100644 --- a/services/user/cmd/server/main.go +++ b/services/user/cmd/server/main.go @@ -4,43 +4,44 @@ import ( "log" "net/http" "os" + "peerprep/user/internal/handlers" + "peerprep/user/internal/repositories" + "peerprep/user/internal/routers" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "go.uber.org/zap" + "gorm.io/driver/postgres" + "gorm.io/gorm" ) -func registerRoutes(r *chi.Mux, logger *zap.Logger) { - // F1.* stubs - r.Post("/api/v1/auth/register", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"status":"registered_stub"}`)) - }) - r.Post("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"accessToken":"stub","refreshToken":"stub"}`)) - }) - r.Post("/api/v1/auth/logout", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"status":"logged_out_stub"}`)) - }) - r.Put("/api/v1/account", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"status":"account_updated_stub"}`)) - }) - r.Delete("/api/v1/account", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"status":"account_deleted_stub"}`)) - }) -} - func main() { logger, _ := zap.NewProduction() defer logger.Sync() + // Initialize database connection + dsn := "host=localhost user=your_user password=your_password dbname=your_db port=5432 sslmode=disable" + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + logger.Fatal("Failed to connect to the database", zap.Error(err)) + } + + // Initialize repository and handler + userRepo := &repositories.UserRepository{DB: db} + userHandler := &handlers.UserHandler{Repo: userRepo} + + // Set up router r := chi.NewRouter() r.Use(middleware.RequestID, middleware.RealIP, middleware.Logger, middleware.Recoverer, middleware.Timeout(60*time.Second)) + // Health check route r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) }) - registerRoutes(r, logger) + // Register user routes + routers.UserRoutes(r, userHandler) + + // Start server port := os.Getenv("PORT") if port == "" { port = "8080" diff --git a/services/user/go.mod b/services/user/go.mod index 18aa9ce2..96164df9 100644 --- a/services/user/go.mod +++ b/services/user/go.mod @@ -7,4 +7,17 @@ require ( go.uber.org/zap v1.27.0 ) -require go.uber.org/multierr v1.10.0 // indirect +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/gorm v1.31.0 // indirect +) diff --git a/services/user/go.sum b/services/user/go.sum index c0e9e80a..8128d44d 100644 --- a/services/user/go.sum +++ b/services/user/go.sum @@ -1,9 +1,25 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -12,5 +28,19 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/services/user/internal/handlers/auth_handler.go b/services/user/internal/handlers/auth_handler.go new file mode 100644 index 00000000..e69de29b diff --git a/services/user/internal/handlers/user_crud_handler.go b/services/user/internal/handlers/user_crud_handler.go new file mode 100644 index 00000000..05b7c609 --- /dev/null +++ b/services/user/internal/handlers/user_crud_handler.go @@ -0,0 +1,100 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "peerprep/user/internal/models" + "peerprep/user/internal/repositories" + + "github.com/go-chi/chi/v5" +) + +type UserHandler struct { + Repo *repositories.UserRepository +} + +// CreateUserHandler handles user creation +func (h *UserHandler) CreateUserHandler(w http.ResponseWriter, r *http.Request) { + var user models.User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + // Save user using the repository + if err := h.Repo.CreateUser(&user); err != nil { + http.Error(w, "Failed to create user", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(user) +} + +// GetUserHandler retrieves a user by ID +func (h *UserHandler) GetUserHandler(w http.ResponseWriter, r *http.Request) { + userID := chi.URLParam(r, "id") + if userID == "" { + http.Error(w, "User ID is required", http.StatusBadRequest) + return + } + + user, err := h.Repo.GetUserByID(userID) + if err != nil { + if err == repositories.ErrUserNotFound { + http.Error(w, "User not found", http.StatusNotFound) + } else { + http.Error(w, "Failed to retrieve user", http.StatusInternalServerError) + } + return + } + + json.NewEncoder(w).Encode(user) +} + +// UpdateUserHandler updates user details +func (h *UserHandler) UpdateUserHandler(w http.ResponseWriter, r *http.Request) { + userID := chi.URLParam(r, "id") + if userID == "" { + http.Error(w, "User ID is required", http.StatusBadRequest) + return + } + + var updates models.User + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + user, err := h.Repo.UpdateUser(userID, &updates) + if err != nil { + if err == repositories.ErrUserNotFound { + http.Error(w, "User not found", http.StatusNotFound) + } else { + http.Error(w, "Failed to update user", http.StatusInternalServerError) + } + return + } + + json.NewEncoder(w).Encode(user) +} + +// DeleteUserHandler deletes a user by ID +func (h *UserHandler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { + userID := chi.URLParam(r, "id") + if userID == "" { + http.Error(w, "User ID is required", http.StatusBadRequest) + return + } + + if err := h.Repo.DeleteUser(userID); err != nil { + if err == repositories.ErrUserNotFound { + http.Error(w, "User not found", http.StatusNotFound) + } else { + http.Error(w, "Failed to delete user", http.StatusInternalServerError) + } + return + } + + w.WriteHeader(http.StatusNoContent) +} \ No newline at end of file diff --git a/services/user/internal/models/user.go b/services/user/internal/models/user.go new file mode 100644 index 00000000..4c64023b --- /dev/null +++ b/services/user/internal/models/user.go @@ -0,0 +1,12 @@ +package models + +import ( + "gorm.io/gorm" +) + +// User represents a registered user in the system. +type User struct { + gorm.Model + Username string `gorm:"unique;not null" json:"username"` + Email string `gorm:"unique;not null" json:"email"` +} diff --git a/services/user/internal/repositories/user_repository.go b/services/user/internal/repositories/user_repository.go new file mode 100644 index 00000000..3d458382 --- /dev/null +++ b/services/user/internal/repositories/user_repository.go @@ -0,0 +1,62 @@ +package repositories + +import ( + "errors" + "peerprep/user/internal/models" + + "gorm.io/gorm" +) + +var ErrUserNotFound = errors.New("user not found") + +type UserRepository struct { + DB *gorm.DB +} + +func (r *UserRepository) CreateUser(user *models.User) error { + return r.DB.Create(user).Error +} + +func (r *UserRepository) GetUserByID(userID string) (*models.User, error) { + var user models.User + id, err := strconv.ParseUint(userID, 10, 64) + if err != nil { + return nil, err + } + err = r.DB.Where("id = ?", id).First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return &user, err +} + +func (r *UserRepository) UpdateUser(userID string, updates *models.User) (*models.User, error) { + var user models.User + id, err := strconv.ParseUint(userID, 10, 64) + if err != nil { + return nil, err + } + if err := r.DB.Where("id = ?", id).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, err + } + + if err := r.DB.Model(&user).Updates(updates).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepository) DeleteUser(userID string) error { + id, err := strconv.ParseUint(userID, 10, 64) + if err != nil { + return err + } + result := r.DB.Where("id = ?", id).Delete(&models.User{}) + if result.RowsAffected == 0 { + return ErrUserNotFound + } + return result.Error +} diff --git a/services/user/internal/routers/user_routes.go b/services/user/internal/routers/user_routes.go new file mode 100644 index 00000000..b98f2369 --- /dev/null +++ b/services/user/internal/routers/user_routes.go @@ -0,0 +1,16 @@ +package routers + +import ( + handlers "peerprep/user/internal/handlers" + + "github.com/go-chi/chi/v5" +) + +func UserRoutes(r *chi.Mux, userHandler *handlers.UserHandler) { + r.Route("/api/v1/users", func(r chi.Router) { + r.Post("/", userHandler.CreateUserHandler) // Create user + r.Get("/{id}", userHandler.GetUserHandler) // Get user by ID + r.Put("/{id}", userHandler.UpdateUserHandler) // Update user by ID + r.Delete("/{id}", userHandler.DeleteUserHandler) // Delete user by ID + }) +} \ No newline at end of file