diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index 3f9b560..aa5754a 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -2,9 +2,11 @@ package api import ( "context" + "errors" "fmt" "log/slog" "net/http" + "strings" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" @@ -16,6 +18,7 @@ import ( "github.com/forfarm/backend/internal/domain" m "github.com/forfarm/backend/internal/middlewares" "github.com/forfarm/backend/internal/repository" + "github.com/forfarm/backend/internal/utilities" ) type api struct { @@ -48,6 +51,15 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { } } +func (a *api) getUserIDFromHeader(authHeader string) (string, error) { + const bearerPrefix = "Bearer " + if !strings.HasPrefix(authHeader, bearerPrefix) { + return "", errors.New("invalid authorization header") + } + tokenString := strings.TrimPrefix(authHeader, bearerPrefix) + return utilities.ExtractUUIDFromToken(tokenString) +} + func (a *api) Server(port int) *http.Server { return &http.Server{ Addr: fmt.Sprintf(":%d", port), diff --git a/backend/internal/api/farm.go b/backend/internal/api/farm.go index 4820e11..23568a1 100644 --- a/backend/internal/api/farm.go +++ b/backend/internal/api/farm.go @@ -9,23 +9,17 @@ import ( "github.com/go-chi/chi/v5" ) +// registerFarmRoutes defines endpoints for farm operations. func (a *api) registerFarmRoutes(_ chi.Router, api huma.API) { tags := []string{"farm"} - prefix := "/farm" + prefix := "/farms" huma.Register(api, huma.Operation{ - OperationID: "createFarm", - Method: http.MethodPost, + OperationID: "getAllFarms", + Method: http.MethodGet, Path: prefix, Tags: tags, - }, a.createFarmHandler) - - huma.Register(api, huma.Operation{ - OperationID: "getFarmsByOwner", - Method: http.MethodGet, - Path: prefix + "/owner/{owner_id}", - Tags: tags, - }, a.getFarmsByOwnerHandler) + }, a.getAllFarmsHandler) huma.Register(api, huma.Operation{ OperationID: "getFarmByID", @@ -34,6 +28,20 @@ func (a *api) registerFarmRoutes(_ chi.Router, api huma.API) { Tags: tags, }, a.getFarmByIDHandler) + huma.Register(api, huma.Operation{ + OperationID: "createFarm", + Method: http.MethodPost, + Path: prefix, + Tags: tags, + }, a.createFarmHandler) + + huma.Register(api, huma.Operation{ + OperationID: "updateFarm", + Method: http.MethodPut, + Path: prefix + "/{farm_id}", + Tags: tags, + }, a.updateFarmHandler) + huma.Register(api, huma.Operation{ OperationID: "deleteFarm", Method: http.MethodDelete, @@ -42,13 +50,20 @@ func (a *api) registerFarmRoutes(_ chi.Router, api huma.API) { }, a.deleteFarmHandler) } +// +// Input and Output types +// + +// CreateFarmInput contains the request data for creating a new farm. type CreateFarmInput struct { Header string `header:"Authorization" required:"true" example:"Bearer token"` Body struct { - Name string `json:"name"` - Lat []float64 `json:"lat"` - Lon []float64 `json:"lon"` - OwnerID string `json:"owner_id"` + Name string `json:"name"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + OwnerID string `json:"owner_id"` + FarmType string `json:"farm_type,omitempty"` + TotalSize string `json:"total_size,omitempty"` } } @@ -58,42 +73,14 @@ type CreateFarmOutput struct { } } -func (a *api) createFarmHandler(ctx context.Context, input *CreateFarmInput) (*CreateFarmOutput, error) { - farm := &domain.Farm{ - Name: input.Body.Name, - Lat: input.Body.Lat, - Lon: input.Body.Lon, - OwnerID: input.Body.OwnerID, - } - - err := a.farmRepo.CreateOrUpdate(ctx, farm) - if err != nil { - return nil, err - } - - return &CreateFarmOutput{Body: struct { - UUID string `json:"uuid"` - }{UUID: farm.UUID}}, nil +type GetAllFarmsInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` } -type GetFarmsByOwnerInput struct { - Header string `header:"Authorization" required:"true" example:"Bearer token"` - OwnerID string `path:"owner_id"` -} - -type GetFarmsByOwnerOutput struct { +type GetAllFarmsOutput struct { Body []domain.Farm } -func (a *api) getFarmsByOwnerHandler(ctx context.Context, input *GetFarmsByOwnerInput) (*GetFarmsByOwnerOutput, error) { - farms, err := a.farmRepo.GetByOwnerID(ctx, input.OwnerID) - if err != nil { - return nil, err - } - - return &GetFarmsByOwnerOutput{Body: farms}, nil -} - type GetFarmByIDInput struct { Header string `header:"Authorization" required:"true" example:"Bearer token"` FarmID string `path:"farm_id"` @@ -103,13 +90,22 @@ type GetFarmByIDOutput struct { Body domain.Farm } -func (a *api) getFarmByIDHandler(ctx context.Context, input *GetFarmByIDInput) (*GetFarmByIDOutput, error) { - farm, err := a.farmRepo.GetByID(ctx, input.FarmID) - if err != nil { - return nil, err +// UpdateFarmInput uses pointer types for optional/nullable fields. +type UpdateFarmInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` + FarmID string `path:"farm_id"` + Body struct { + Name string `json:"name,omitempty"` + Lat *float64 `json:"lat,omitempty"` + Lon *float64 `json:"lon,omitempty"` + FarmType *string `json:"farm_type,omitempty"` + TotalSize *string `json:"total_size,omitempty"` + OwnerID string `json:"owner_id,omitempty"` } +} - return &GetFarmByIDOutput{Body: farm}, nil +type UpdateFarmOutput struct { + Body domain.Farm } type DeleteFarmInput struct { @@ -123,13 +119,134 @@ type DeleteFarmOutput struct { } } -func (a *api) deleteFarmHandler(ctx context.Context, input *DeleteFarmInput) (*DeleteFarmOutput, error) { - err := a.farmRepo.Delete(ctx, input.FarmID) +// +// API Handlers +// + +func (a *api) createFarmHandler(ctx context.Context, input *CreateFarmInput) (*CreateFarmOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) if err != nil { return nil, err } - return &DeleteFarmOutput{Body: struct { - Message string `json:"message"` - }{Message: "Farm deleted successfully"}}, nil + if input.Body.OwnerID != "" && input.Body.OwnerID != userID { + return nil, huma.Error401Unauthorized("unauthorized: cannot create a farm for another owner") + } + + farm := &domain.Farm{ + Name: input.Body.Name, + Lat: input.Body.Lat, + Lon: input.Body.Lon, + FarmType: input.Body.FarmType, + TotalSize: input.Body.TotalSize, + OwnerID: userID, + } + + if err := a.farmRepo.CreateOrUpdate(ctx, farm); err != nil { + return nil, err + } + + return &CreateFarmOutput{ + Body: struct { + UUID string `json:"uuid"` + }{UUID: farm.UUID}, + }, nil +} + +func (a *api) getAllFarmsHandler(ctx context.Context, input *GetAllFarmsInput) (*GetAllFarmsOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) + if err != nil { + return nil, err + } + + farms, err := a.farmRepo.GetByOwnerID(ctx, userID) + if err != nil { + return nil, err + } + return &GetAllFarmsOutput{Body: farms}, nil +} + +func (a *api) getFarmByIDHandler(ctx context.Context, input *GetFarmByIDInput) (*GetFarmByIDOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) + if err != nil { + return nil, err + } + + farm, err := a.farmRepo.GetByID(ctx, input.FarmID) + if err != nil { + return nil, err + } + + if farm.OwnerID != userID { + return nil, huma.Error401Unauthorized("unauthorized") + } + + return &GetFarmByIDOutput{Body: *farm}, nil +} + +func (a *api) updateFarmHandler(ctx context.Context, input *UpdateFarmInput) (*UpdateFarmOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) + if err != nil { + return nil, err + } + + farm, err := a.farmRepo.GetByID(ctx, input.FarmID) + if err != nil { + return nil, err + } + + if farm.OwnerID != userID { + return nil, huma.Error401Unauthorized("unauthorized") + } + + if input.Body.Name != "" { + farm.Name = input.Body.Name + } + if input.Body.Lat != nil { + farm.Lat = *input.Body.Lat + } + if input.Body.Lon != nil { + farm.Lon = *input.Body.Lon + } + if input.Body.FarmType != nil { + farm.FarmType = *input.Body.FarmType + } + if input.Body.TotalSize != nil { + farm.TotalSize = *input.Body.TotalSize + } + if input.Body.OwnerID != "" && input.Body.OwnerID != userID { + return nil, huma.Error401Unauthorized("unauthorized: cannot change owner") + } + + if err = a.farmRepo.CreateOrUpdate(ctx, farm); err != nil { + return nil, err + } + + return &UpdateFarmOutput{Body: *farm}, nil +} + +func (a *api) deleteFarmHandler(ctx context.Context, input *DeleteFarmInput) (*DeleteFarmOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) + if err != nil { + return nil, err + } + + farm, err := a.farmRepo.GetByID(ctx, input.FarmID) + if err != nil { + return nil, err + } + + if farm.OwnerID != userID { + return nil, huma.Error401Unauthorized("unauthorized") + } + + if err := a.farmRepo.Delete(ctx, input.FarmID); err != nil { + return nil, err + } + + return &DeleteFarmOutput{ + Body: struct { + Message string `json:"message"` + }{Message: "Farm deleted successfully"}, + }, nil } diff --git a/backend/internal/domain/farm.go b/backend/internal/domain/farm.go index 6422735..6de0cac 100644 --- a/backend/internal/domain/farm.go +++ b/backend/internal/domain/farm.go @@ -10,8 +10,10 @@ import ( type Farm struct { UUID string Name string - Lat []float64 - Lon []float64 + Lat float64 // single latitude value + Lon float64 // single longitude value + FarmType string // e.g., "Durian", "mango", "mixed-crop", "others" + TotalSize string // e.g., "10 Rai" (optional) CreatedAt time.Time UpdatedAt time.Time OwnerID string @@ -27,7 +29,7 @@ func (f *Farm) Validate() error { } type FarmRepository interface { - GetByID(context.Context, string) (Farm, error) + GetByID(context.Context, string) (*Farm, error) GetByOwnerID(context.Context, string) ([]Farm, error) CreateOrUpdate(context.Context, *Farm) error Delete(context.Context, string) error diff --git a/backend/internal/repository/postgres_farm.go b/backend/internal/repository/postgres_farm.go index 5cd9439..8910b08 100644 --- a/backend/internal/repository/postgres_farm.go +++ b/backend/internal/repository/postgres_farm.go @@ -26,45 +26,53 @@ func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args . var farms []domain.Farm for rows.Next() { var f domain.Farm + // Order: uuid, name, lat, lon, farm_type, total_size, created_at, updated_at, owner_id if err := rows.Scan( &f.UUID, &f.Name, &f.Lat, &f.Lon, + &f.FarmType, + &f.TotalSize, &f.CreatedAt, &f.UpdatedAt, &f.OwnerID, ); err != nil { return nil, err } - farms = append(farms, f) } return farms, nil } -func (p *postgresFarmRepository) GetByID(ctx context.Context, uuid string) (domain.Farm, error) { +func (p *postgresFarmRepository) GetByID(ctx context.Context, farmId string) (*domain.Farm, error) { query := ` - SELECT uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types + SELECT uuid, name, lat, lon, farm_type, total_size, created_at, updated_at, owner_id FROM farms WHERE uuid = $1` - - farms, err := p.fetch(ctx, query, uuid) + var f domain.Farm + err := p.conn.QueryRow(ctx, query, farmId).Scan( + &f.UUID, + &f.Name, + &f.Lat, + &f.Lon, + &f.FarmType, + &f.TotalSize, + &f.CreatedAt, + &f.UpdatedAt, + &f.OwnerID, + ) if err != nil { - return domain.Farm{}, err + return nil, err } - if len(farms) == 0 { - return domain.Farm{}, domain.ErrNotFound - } - return farms[0], nil + return &f, nil } func (p *postgresFarmRepository) GetByOwnerID(ctx context.Context, ownerID string) ([]domain.Farm, error) { query := ` - SELECT uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types + SELECT uuid, name, lat, lon, farm_type, total_size, created_at, updated_at, owner_id FROM farms WHERE owner_id = $1` - return p.fetch(ctx, query, ownerID) } @@ -73,27 +81,20 @@ func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.F f.UUID = uuid.New().String() } - query := ` - INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types) - VALUES ($1, $2, $3, $4, NOW(), NOW(), $5, $6) + query := ` + INSERT INTO farms (uuid, name, lat, lon, farm_type, total_size, created_at, updated_at, owner_id) + VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW(), $7) ON CONFLICT (uuid) DO UPDATE SET name = EXCLUDED.name, lat = EXCLUDED.lat, lon = EXCLUDED.lon, + farm_type = EXCLUDED.farm_type, + total_size = EXCLUDED.total_size, updated_at = NOW(), - owner_id = EXCLUDED.owner_id, - plant_types = EXCLUDED.plant_types + owner_id = EXCLUDED.owner_id RETURNING uuid, created_at, updated_at` - - return p.conn.QueryRow( - ctx, - query, - f.UUID, - f.Name, - f.Lat, - f.Lon, - f.OwnerID, - ).Scan(&f.UUID, &f.CreatedAt, &f.UpdatedAt) + return p.conn.QueryRow(ctx, query, f.UUID, f.Name, f.Lat, f.Lon, f.FarmType, f.TotalSize, f.OwnerID). + Scan(&f.UUID, &f.CreatedAt, &f.UpdatedAt) } func (p *postgresFarmRepository) Delete(ctx context.Context, uuid string) error {