diff --git a/backend/go.mod b/backend/go.mod index c211bb4..f81da6e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,37 +5,27 @@ go 1.23.5 require ( github.com/danielgtaylor/huma/v2 v2.28.0 github.com/go-chi/chi/v5 v5.2.1 - github.com/go-chi/jwtauth/v5 v5.3.2 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 + github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.2 github.com/joho/godotenv v1.5.1 - github.com/lestrrat-go/jwx/v2 v2.1.3 github.com/pressly/goose/v3 v3.24.1 github.com/spf13/cobra v1.8.1 + golang.org/x/crypto v0.31.0 ) require ( github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/goccy/go-json v0.10.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/lestrrat-go/blackmagic v1.0.2 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.6 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/segmentio/asm v1.2.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/pflag v1.0.6 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.31.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index c1e8c76..312f534 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -6,18 +6,14 @@ github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wX 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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-chi/jwtauth/v5 v5.3.2 h1:s+ON3ATyyMs3Me0kqyuua6Rwu+2zqIIkL0GCaMarwvs= -github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -36,18 +32,6 @@ 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= -github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo= -github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= @@ -61,8 +45,6 @@ github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -73,9 +55,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index efc9818..fd10aa1 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -22,19 +22,23 @@ type api struct { httpClient *http.Client userRepo domain.UserRepository + cropRepo domain.CroplandRepository } func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { client := &http.Client{} + // Initialize repositories for users and croplands userRepository := repository.NewPostgresUser(pool) + croplandRepository := repository.NewPostgresCropland(pool) return &api{ logger: logger, httpClient: client, userRepo: userRepository, + cropRepo: croplandRepository, } } @@ -51,13 +55,16 @@ func (a *api) Routes() *chi.Mux { config := huma.DefaultConfig("ForFarm Public API", "v1.0.0") api := humachi.New(router, config) + // Register Authentication Routes router.Group(func(r chi.Router) { a.registerAuthRoutes(r, api) + a.registerCropRoutes(r, api) }) + // Register Cropland Routes, including Auth Middleware if required router.Group(func(r chi.Router) { + // Apply Authentication middleware to the Cropland routes api.UseMiddleware(m.AuthMiddleware(api)) - a.registerHelloRoutes(r, api) }) return router diff --git a/backend/internal/api/crop.go b/backend/internal/api/crop.go new file mode 100644 index 0000000..8ff12c7 --- /dev/null +++ b/backend/internal/api/crop.go @@ -0,0 +1,209 @@ +package api + +import ( + "context" + "errors" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/forfarm/backend/internal/domain" + "github.com/go-chi/chi/v5" + "github.com/gofrs/uuid" +) + +// Register the crop routes +func (a *api) registerCropRoutes(_ chi.Router, api huma.API) { + tags := []string{"crop"} + + prefix := "/crop" + + // Register GET /crop + huma.Register(api, huma.Operation{ + OperationID: "getAllCroplands", + Method: http.MethodGet, + Path: prefix, + Tags: tags, + }, a.getAllCroplandsHandler) + + // Register GET /crop/{uuid} + huma.Register(api, huma.Operation{ + OperationID: "getCroplandByID", + Method: http.MethodGet, + Path: prefix + "/{uuid}", + Tags: tags, + }, a.getCroplandByIDHandler) + + // Register GET /crop/farm/{farm_id} + huma.Register(api, huma.Operation{ + OperationID: "getAllCroplandsByFarmID", + Method: http.MethodGet, + Path: prefix + "/farm/{farm_id}", + Tags: tags, + }, a.getAllCroplandsByFarmIDHandler) + + // Register POST /crop (Create or Update) + huma.Register(api, huma.Operation{ + OperationID: "createOrUpdateCropland", + Method: http.MethodPost, + Path: prefix, + Tags: tags, + }, a.createOrUpdateCroplandHandler) +} + +// Response structure for all croplands +type GetCroplandsOutput struct { + Body struct { + Croplands []domain.Cropland `json:"croplands"` + } `json:"body"` +} + +// Response structure for single cropland by ID +type GetCroplandByIDOutput struct { + Body struct { + Cropland domain.Cropland `json:"cropland"` + } `json:"body"` +} + +// Request structure for creating or updating a cropland +type CreateOrUpdateCroplandInput struct { + Body struct { + UUID string `json:"uuid,omitempty"` // Optional for create, required for update + Name string `json:"name"` + Status string `json:"status"` + Priority int `json:"priority"` + LandSize float64 `json:"land_size"` + GrowthStage string `json:"growth_stage"` + PlantID string `json:"plant_id"` + FarmID string `json:"farm_id"` + } `json:"body"` +} + +// Response structure for creating or updating a cropland +type CreateOrUpdateCroplandOutput struct { + Body struct { + Cropland domain.Cropland `json:"cropland"` + } `json:"body"` +} + +// GetAllCroplands handles GET /crop endpoint +func (a *api) getAllCroplandsHandler(ctx context.Context, input *struct{}) (*GetCroplandsOutput, error) { + resp := &GetCroplandsOutput{} + + // Fetch all croplands without filtering by farmID + croplands, err := a.cropRepo.GetAll(ctx) // Use the GetAll method + if err != nil { + return nil, err + } + + resp.Body.Croplands = croplands + return resp, nil +} + +// GetCroplandByID handles GET /crop/{uuid} endpoint +func (a *api) getCroplandByIDHandler(ctx context.Context, input *struct { + UUID string `path:"uuid" example:"550e8400-e29b-41d4-a716-446655440000"` +}) (*GetCroplandByIDOutput, error) { + resp := &GetCroplandByIDOutput{} + + // Validate the UUID format + if input.UUID == "" { + return nil, huma.Error400BadRequest("UUID parameter is required") + } + + // Check if the UUID is in a valid format + _, err := uuid.FromString(input.UUID) + if err != nil { + return nil, huma.Error400BadRequest("invalid UUID format") + } + + // Fetch cropland by ID + cropland, err := a.cropRepo.GetByID(ctx, input.UUID) + if err != nil { + if errors.Is(err, domain.ErrNotFound) { + return nil, huma.Error404NotFound("cropland not found") + } + return nil, err + } + + resp.Body.Cropland = cropland + return resp, nil +} + +// GetAllCroplandsByFarmID handles GET /crop/farm/{farm_id} endpoint +func (a *api) getAllCroplandsByFarmIDHandler(ctx context.Context, input *struct { + FarmID string `path:"farm_id" example:"550e8400-e29b-41d4-a716-446655440000"` +}) (*GetCroplandsOutput, error) { + resp := &GetCroplandsOutput{} + + // Validate the FarmID format + if input.FarmID == "" { + return nil, huma.Error400BadRequest("FarmID parameter is required") + } + + // Check if the FarmID is in a valid format + _, err := uuid.FromString(input.FarmID) + if err != nil { + return nil, huma.Error400BadRequest("invalid FarmID format") + } + + // Fetch croplands by FarmID + croplands, err := a.cropRepo.GetByFarmID(ctx, input.FarmID) + if err != nil { + return nil, err + } + + resp.Body.Croplands = croplands + return resp, nil +} + +// CreateOrUpdateCropland handles POST /crop endpoint +func (a *api) createOrUpdateCroplandHandler(ctx context.Context, input *CreateOrUpdateCroplandInput) (*CreateOrUpdateCroplandOutput, error) { + resp := &CreateOrUpdateCroplandOutput{} + + // Validate required fields + if input.Body.Name == "" { + return nil, huma.Error400BadRequest("name is required") + } + if input.Body.Status == "" { + return nil, huma.Error400BadRequest("status is required") + } + if input.Body.GrowthStage == "" { + return nil, huma.Error400BadRequest("growth_stage is required") + } + if input.Body.PlantID == "" { + return nil, huma.Error400BadRequest("plant_id is required") + } + if input.Body.FarmID == "" { + return nil, huma.Error400BadRequest("farm_id is required") + } + + // Validate UUID if provided + if input.Body.UUID != "" { + _, err := uuid.FromString(input.Body.UUID) + if err != nil { + return nil, huma.Error400BadRequest("invalid UUID format") + } + } + + // Map input to domain.Cropland + cropland := &domain.Cropland{ + UUID: input.Body.UUID, + Name: input.Body.Name, + Status: input.Body.Status, + Priority: input.Body.Priority, + LandSize: input.Body.LandSize, + GrowthStage: input.Body.GrowthStage, + PlantID: input.Body.PlantID, + FarmID: input.Body.FarmID, + } + + // Create or update the cropland + err := a.cropRepo.CreateOrUpdate(ctx, cropland) + if err != nil { + return nil, err + } + + // Return the created/updated cropland + resp.Body.Cropland = *cropland + return resp, nil +} diff --git a/backend/internal/domain/cropland.go b/backend/internal/domain/cropland.go index 984e6e4..2da364d 100644 --- a/backend/internal/domain/cropland.go +++ b/backend/internal/domain/cropland.go @@ -31,6 +31,8 @@ func (c *Cropland) Validate() error { type CroplandRepository interface { GetByID(context.Context, string) (Cropland, error) + GetByFarmID(ctx context.Context, farmID string) ([]Cropland, error) + GetAll(ctx context.Context) ([]Cropland, error) // Add this method CreateOrUpdate(context.Context, *Cropland) error Delete(context.Context, string) error } diff --git a/backend/internal/repository/postgres_cropland.go b/backend/internal/repository/postgres_cropland.go index 7cc48f8..5eb0ff7 100644 --- a/backend/internal/repository/postgres_cropland.go +++ b/backend/internal/repository/postgres_cropland.go @@ -77,18 +77,18 @@ func (p *postgresCroplandRepository) CreateOrUpdate(ctx context.Context, c *doma } query := ` - INSERT INTO croplands (uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()) - ON CONFLICT (uuid) DO UPDATE - SET name = EXCLUDED.name, - status = EXCLUDED.status, - priority = EXCLUDED.priority, - land_size = EXCLUDED.land_size, - growth_stage = EXCLUDED.growth_stage, - plant_id = EXCLUDED.plant_id, - farm_id = EXCLUDED.farm_id, - updated_at = NOW() - RETURNING uuid, created_at, updated_at` + INSERT INTO croplands (uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW()) + ON CONFLICT (uuid) DO UPDATE + SET name = EXCLUDED.name, + status = EXCLUDED.status, + priority = EXCLUDED.priority, + land_size = EXCLUDED.land_size, + growth_stage = EXCLUDED.growth_stage, + plant_id = EXCLUDED.plant_id, + farm_id = EXCLUDED.farm_id, + updated_at = NOW() + RETURNING uuid, created_at, updated_at` return p.conn.QueryRow( ctx, @@ -101,11 +101,18 @@ func (p *postgresCroplandRepository) CreateOrUpdate(ctx context.Context, c *doma c.GrowthStage, c.PlantID, c.FarmID, - ).Scan(&c.CreatedAt, &c.UpdatedAt) + ).Scan(&c.UUID, &c.CreatedAt, &c.UpdatedAt) // Fixed Scan call } - func (p *postgresCroplandRepository) Delete(ctx context.Context, uuid string) error { query := `DELETE FROM croplands WHERE uuid = $1` _, err := p.conn.Exec(ctx, query, uuid) return err } + +func (p *postgresCroplandRepository) GetAll(ctx context.Context) ([]domain.Cropland, error) { + query := ` + SELECT uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at + FROM croplands` + + return p.fetch(ctx, query) +}