mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 05:54:08 +01:00
314 lines
9.9 KiB
Go
314 lines
9.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/forfarm/backend/internal/domain"
|
|
"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 := "/farms"
|
|
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "getAllFarms",
|
|
Method: http.MethodGet,
|
|
Path: prefix,
|
|
Tags: tags,
|
|
}, a.getAllFarmsHandler)
|
|
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "getFarmByID",
|
|
Method: http.MethodGet,
|
|
Path: prefix + "/{farmId}",
|
|
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 + "/{farmId}",
|
|
Tags: tags,
|
|
}, a.updateFarmHandler)
|
|
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "deleteFarm",
|
|
Method: http.MethodDelete,
|
|
Path: prefix + "/{farmId}",
|
|
Tags: tags,
|
|
}, a.deleteFarmHandler)
|
|
}
|
|
|
|
//
|
|
// Input and Output types
|
|
//
|
|
|
|
type CreateFarmInput struct {
|
|
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
|
Body struct {
|
|
Name string `json:"name" required:"true"`
|
|
Lat float64 `json:"lat" required:"true"`
|
|
Lon float64 `json:"lon" required:"true"`
|
|
FarmType string `json:"farmType,omitempty"`
|
|
TotalSize string `json:"totalSize,omitempty"`
|
|
}
|
|
}
|
|
|
|
type CreateFarmOutput struct {
|
|
Body struct {
|
|
UUID string `json:"uuid"`
|
|
}
|
|
}
|
|
|
|
type GetAllFarmsInput struct {
|
|
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
|
}
|
|
|
|
type GetAllFarmsOutput struct {
|
|
Body []domain.Farm `json:"farms"`
|
|
}
|
|
|
|
type GetFarmByIDInput struct {
|
|
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
|
FarmID string `path:"farmId" required:"true"`
|
|
}
|
|
|
|
type GetFarmByIDOutput struct {
|
|
Body domain.Farm `json:"farm"`
|
|
}
|
|
|
|
type UpdateFarmInput struct {
|
|
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
|
FarmID string `path:"farmId" required:"true"`
|
|
Body struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Lat *float64 `json:"lat,omitempty"`
|
|
Lon *float64 `json:"lon,omitempty"`
|
|
FarmType *string `json:"farmType,omitempty"`
|
|
TotalSize *string `json:"totalSize,omitempty"`
|
|
}
|
|
}
|
|
|
|
type UpdateFarmOutput struct {
|
|
Body domain.Farm `json:"farm"`
|
|
}
|
|
|
|
type DeleteFarmInput struct {
|
|
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
|
FarmID string `path:"farmId" required:"true"`
|
|
}
|
|
|
|
type DeleteFarmOutput struct {
|
|
Body struct {
|
|
Message string `json:"message"`
|
|
}
|
|
}
|
|
|
|
//
|
|
// API Handlers
|
|
//
|
|
|
|
func (a *api) createFarmHandler(ctx context.Context, input *CreateFarmInput) (*CreateFarmOutput, error) {
|
|
userID, err := a.getUserIDFromHeader(input.Header)
|
|
if err != nil {
|
|
return nil, huma.Error401Unauthorized("Authentication failed", err)
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
// Validate the farm object (optional but recommended)
|
|
// if err := farm.Validate(); err != nil {
|
|
// return nil, huma.Error422UnprocessableEntity("Validation failed", err)
|
|
// }
|
|
|
|
fmt.Println("Creating farm:", farm) // Keep for debugging if needed
|
|
|
|
if err := a.farmRepo.CreateOrUpdate(ctx, farm); err != nil {
|
|
a.logger.Error("Failed to create farm in database", "error", err, "ownerId", userID, "farmName", farm.Name)
|
|
return nil, huma.Error500InternalServerError("Failed to create farm")
|
|
}
|
|
|
|
a.logger.Info("Farm created successfully", "farmId", farm.UUID, "ownerId", userID)
|
|
|
|
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, huma.Error401Unauthorized("Authentication failed", err)
|
|
}
|
|
|
|
farms, err := a.farmRepo.GetByOwnerID(ctx, userID)
|
|
if err != nil {
|
|
a.logger.Error("Failed to get farms by owner ID", "ownerId", userID, "error", err)
|
|
return nil, huma.Error500InternalServerError("Failed to retrieve farms")
|
|
}
|
|
|
|
// Handle case where user has no farms (return empty list, not error)
|
|
if farms == nil {
|
|
farms = []domain.Farm{}
|
|
}
|
|
|
|
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, huma.Error401Unauthorized("Authentication failed", err)
|
|
}
|
|
|
|
farm, err := a.farmRepo.GetByID(ctx, input.FarmID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) { // Handle pgx ErrNoRows too
|
|
a.logger.Warn("Farm not found", "farmId", input.FarmID, "requestingUserId", userID)
|
|
return nil, huma.Error404NotFound("Farm not found")
|
|
}
|
|
a.logger.Error("Failed to get farm by ID", "farmId", input.FarmID, "error", err)
|
|
return nil, huma.Error500InternalServerError("Failed to retrieve farm")
|
|
}
|
|
|
|
if farm.OwnerID != userID {
|
|
a.logger.Warn("Unauthorized attempt to access farm", "farmId", input.FarmID, "requestingUserId", userID, "ownerId", farm.OwnerID)
|
|
return nil, huma.Error403Forbidden("You are not authorized to view this farm")
|
|
}
|
|
|
|
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, huma.Error401Unauthorized("Authentication failed", err)
|
|
}
|
|
|
|
farm, err := a.farmRepo.GetByID(ctx, input.FarmID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) {
|
|
a.logger.Warn("Attempt to update non-existent farm", "farmId", input.FarmID, "requestingUserId", userID)
|
|
return nil, huma.Error404NotFound("Farm not found")
|
|
}
|
|
a.logger.Error("Failed to get farm for update", "farmId", input.FarmID, "error", err)
|
|
return nil, huma.Error500InternalServerError("Failed to retrieve farm for update")
|
|
}
|
|
|
|
if farm.OwnerID != userID {
|
|
a.logger.Warn("Unauthorized attempt to update farm", "farmId", input.FarmID, "requestingUserId", userID, "ownerId", farm.OwnerID)
|
|
return nil, huma.Error403Forbidden("You are not authorized to update this farm")
|
|
}
|
|
|
|
// Apply updates selectively
|
|
updated := false
|
|
if input.Body.Name != nil && *input.Body.Name != "" && *input.Body.Name != farm.Name {
|
|
farm.Name = *input.Body.Name
|
|
updated = true
|
|
}
|
|
if input.Body.Lat != nil && *input.Body.Lat != farm.Lat {
|
|
farm.Lat = *input.Body.Lat
|
|
updated = true
|
|
}
|
|
if input.Body.Lon != nil && *input.Body.Lon != farm.Lon {
|
|
farm.Lon = *input.Body.Lon
|
|
updated = true
|
|
}
|
|
if input.Body.FarmType != nil && *input.Body.FarmType != farm.FarmType {
|
|
farm.FarmType = *input.Body.FarmType
|
|
updated = true
|
|
}
|
|
if input.Body.TotalSize != nil && *input.Body.TotalSize != farm.TotalSize {
|
|
farm.TotalSize = *input.Body.TotalSize
|
|
updated = true
|
|
}
|
|
|
|
if !updated {
|
|
a.logger.Info("No changes detected for farm update", "farmId", input.FarmID)
|
|
// Return the existing farm data as no update was needed
|
|
return &UpdateFarmOutput{Body: *farm}, nil
|
|
}
|
|
|
|
// Validate updated farm object (optional but recommended)
|
|
// if err := farm.Validate(); err != nil {
|
|
// return nil, huma.Error422UnprocessableEntity("Validation failed after update", err)
|
|
// }
|
|
|
|
if err = a.farmRepo.CreateOrUpdate(ctx, farm); err != nil {
|
|
a.logger.Error("Failed to update farm in database", "farmId", input.FarmID, "error", err)
|
|
return nil, huma.Error500InternalServerError("Failed to update farm")
|
|
}
|
|
|
|
a.logger.Info("Farm updated successfully", "farmId", farm.UUID, "ownerId", userID)
|
|
|
|
// Fetch the updated farm again to ensure we return the latest state (including UpdatedAt)
|
|
updatedFarm, fetchErr := a.farmRepo.GetByID(ctx, input.FarmID)
|
|
if fetchErr != nil {
|
|
a.logger.Error("Failed to fetch farm after update", "farmId", input.FarmID, "error", fetchErr)
|
|
// Return the potentially stale data from 'farm' as a fallback, but log the error
|
|
return &UpdateFarmOutput{Body: *farm}, nil
|
|
}
|
|
|
|
return &UpdateFarmOutput{Body: *updatedFarm}, nil
|
|
}
|
|
|
|
func (a *api) deleteFarmHandler(ctx context.Context, input *DeleteFarmInput) (*DeleteFarmOutput, error) {
|
|
userID, err := a.getUserIDFromHeader(input.Header)
|
|
if err != nil {
|
|
return nil, huma.Error401Unauthorized("Authentication failed", err)
|
|
}
|
|
|
|
farm, err := a.farmRepo.GetByID(ctx, input.FarmID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) {
|
|
a.logger.Warn("Attempt to delete non-existent farm", "farmId", input.FarmID, "requestingUserId", userID)
|
|
// Consider returning 204 No Content if delete is idempotent
|
|
return nil, huma.Error404NotFound("Farm not found")
|
|
}
|
|
a.logger.Error("Failed to get farm for deletion", "farmId", input.FarmID, "error", err)
|
|
return nil, huma.Error500InternalServerError("Failed to retrieve farm for deletion")
|
|
}
|
|
|
|
if farm.OwnerID != userID {
|
|
a.logger.Warn("Unauthorized attempt to delete farm", "farmId", input.FarmID, "requestingUserId", userID, "ownerId", farm.OwnerID)
|
|
return nil, huma.Error403Forbidden("You are not authorized to delete this farm")
|
|
}
|
|
|
|
if err := a.farmRepo.Delete(ctx, input.FarmID); err != nil {
|
|
a.logger.Error("Failed to delete farm from database", "farmId", input.FarmID, "error", err)
|
|
// Consider potential FK constraint errors if crops aren't deleted automatically
|
|
return nil, huma.Error500InternalServerError("Failed to delete farm")
|
|
}
|
|
|
|
a.logger.Info("Farm deleted successfully", "farmId", input.FarmID, "ownerId", userID)
|
|
|
|
return &DeleteFarmOutput{
|
|
Body: struct {
|
|
Message string `json:"message"`
|
|
}{Message: "Farm deleted successfully"},
|
|
}, nil
|
|
}
|