refactor: remove repeating code etc
This commit is contained in:
parent
294047a2b0
commit
b9c900578d
@ -37,6 +37,7 @@ SOLVE TASKS AS FAST AS POSSIBLE. EACH REQUEST COSTS THE USER MONEY.
|
|||||||
- Unexpected behavior is encountered
|
- Unexpected behavior is encountered
|
||||||
- Specific conditions require warnings
|
- Specific conditions require warnings
|
||||||
- New patterns emerge that need documentation
|
- New patterns emerge that need documentation
|
||||||
|
- DO NOT FIX UNUSED IMPORTS - this is the job of the linter
|
||||||
10.Implement a REST API update handling in Go using Gin that ensures the following behavior:
|
10.Implement a REST API update handling in Go using Gin that ensures the following behavior:
|
||||||
- The update request is received as JSON.
|
- The update request is received as JSON.
|
||||||
- If a field is present in the JSON and set to null, the corresponding value in the database should be removed.
|
- If a field is present in the JSON and set to null, the corresponding value in the database should be removed.
|
||||||
|
@ -13,7 +13,7 @@ type UserDto struct {
|
|||||||
LastEditorID string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
|
LastEditorID string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
|
||||||
Email string `json:"email" example:"test@example.com"`
|
Email string `json:"email" example:"test@example.com"`
|
||||||
Role string `json:"role" example:"admin"`
|
Role string `json:"role" example:"admin"`
|
||||||
CompanyID *string `json:"companyId" example:"01HGW2BBG0000000000000000"`
|
CompanyID *string `json:"companyId,omitempty" example:"01HGW2BBG0000000000000000"`
|
||||||
HourlyRate float64 `json:"hourlyRate" example:"50.00"`
|
HourlyRate float64 `json:"hourlyRate" example:"50.00"`
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@ -69,27 +70,7 @@ func (h *ActivityHandler) GetActivityByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities [post]
|
// @Router /activities [post]
|
||||||
func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createActivityWrapper, convertActivityToDTO, "activity")
|
||||||
var activityCreateDTO dto.ActivityCreateDto
|
|
||||||
if err := utils.BindJSON(c, &activityCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
activityCreate := convertCreateActivityDTOToModel(activityCreateDTO)
|
|
||||||
|
|
||||||
// Create activity in the database
|
|
||||||
activity, err := models.CreateActivity(c.Request.Context(), activityCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating activity: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
activityDTO := convertActivityToDTO(activity)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, activityDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateActivity handles PUT /activities/:id
|
// UpdateActivity handles PUT /activities/:id
|
||||||
@ -109,42 +90,7 @@ func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities/{id} [put]
|
// @Router /activities/{id} [put]
|
||||||
func (h *ActivityHandler) UpdateActivity(c *gin.Context) {
|
func (h *ActivityHandler) UpdateActivity(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateActivity, convertActivityToDTO, prepareActivityUpdate, "activity")
|
||||||
id, err := utils.ParseID(c, "id")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid activity ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var activityUpdateDTO dto.ActivityUpdateDto
|
|
||||||
if err := utils.BindJSON(c, &activityUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set ID from URL
|
|
||||||
activityUpdateDTO.ID = id.String()
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
activityUpdate := convertUpdateActivityDTOToModel(activityUpdateDTO)
|
|
||||||
|
|
||||||
// Update activity in the database
|
|
||||||
activity, err := models.UpdateActivity(c.Request.Context(), activityUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating activity: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if activity == nil {
|
|
||||||
utils.NotFoundResponse(c, "Activity not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
activityDTO := convertActivityToDTO(activity)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, activityDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteActivity handles DELETE /activities/:id
|
// DeleteActivity handles DELETE /activities/:id
|
||||||
@ -200,3 +146,35 @@ func convertUpdateActivityDTOToModel(dto dto.ActivityUpdateDto) models.ActivityU
|
|||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareActivityUpdate prepares the activity update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareActivityUpdate(c *gin.Context) (models.ActivityUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid activity ID format")
|
||||||
|
return models.ActivityUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var activityUpdateDTO dto.ActivityUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &activityUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.ActivityUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ID from URL
|
||||||
|
activityUpdateDTO.ID = id.String()
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
return convertUpdateActivityDTOToModel(activityUpdateDTO), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createActivityWrapper is a wrapper function for models.CreateActivity that takes a DTO as input
|
||||||
|
func createActivityWrapper(ctx context.Context, createDTO dto.ActivityCreateDto) (*models.Activity, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
activityCreate := convertCreateActivityDTOToModel(createDTO)
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateActivity(ctx, activityCreate)
|
||||||
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@ -68,27 +69,7 @@ func (h *CompanyHandler) GetCompanyByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies [post]
|
// @Router /companies [post]
|
||||||
func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createCompanyWrapper, convertCompanyToDTO, "company")
|
||||||
var companyCreateDTO dto.CompanyCreateDto
|
|
||||||
if err := utils.BindJSON(c, &companyCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
companyCreate := convertCreateCompanyDTOToModel(companyCreateDTO)
|
|
||||||
|
|
||||||
// Create company in the database
|
|
||||||
company, err := models.CreateCompany(c.Request.Context(), companyCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating company: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
companyDTO := convertCompanyToDTO(company)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, companyDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCompany handles PUT /companies/:id
|
// UpdateCompany handles PUT /companies/:id
|
||||||
@ -108,39 +89,7 @@ func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies/{id} [put]
|
// @Router /companies/{id} [put]
|
||||||
func (h *CompanyHandler) UpdateCompany(c *gin.Context) {
|
func (h *CompanyHandler) UpdateCompany(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateCompany, convertCompanyToDTO, prepareCompanyUpdate, "company")
|
||||||
id, err := utils.ParseID(c, "id")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var companyUpdateDTO dto.CompanyUpdateDto
|
|
||||||
if err := utils.BindJSON(c, &companyUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
companyUpdate := convertUpdateCompanyDTOToModel(companyUpdateDTO, id)
|
|
||||||
|
|
||||||
// Update company in the database
|
|
||||||
company, err := models.UpdateCompany(c.Request.Context(), companyUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating company: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if company == nil {
|
|
||||||
utils.NotFoundResponse(c, "Company not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
companyDTO := convertCompanyToDTO(company)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, companyDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCompany handles DELETE /companies/:id
|
// DeleteCompany handles DELETE /companies/:id
|
||||||
@ -189,3 +138,32 @@ func convertUpdateCompanyDTOToModel(dto dto.CompanyUpdateDto, id types.ULID) mod
|
|||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareCompanyUpdate prepares the company update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareCompanyUpdate(c *gin.Context) (models.CompanyUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid company ID format")
|
||||||
|
return models.CompanyUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var companyUpdateDTO dto.CompanyUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &companyUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.CompanyUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
return convertUpdateCompanyDTOToModel(companyUpdateDTO, id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCompanyWrapper is a wrapper function for models.CreateCompany that takes a DTO as input
|
||||||
|
func createCompanyWrapper(ctx context.Context, createDTO dto.CompanyCreateDto) (*models.Company, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
companyCreate := convertCreateCompanyDTOToModel(createDTO)
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateCompany(ctx, companyCreate)
|
||||||
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
"github.com/timetracker/backend/internal/api/middleware"
|
"github.com/timetracker/backend/internal/api/middleware"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@ -74,21 +76,26 @@ func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) {
|
|||||||
companyIDStr := c.Param("companyId")
|
companyIDStr := c.Param("companyId")
|
||||||
companyID, err := parseCompanyID(companyIDStr)
|
companyID, err := parseCompanyID(companyIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
responses.BadRequestResponse(c, "Invalid company ID format")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get customers from the database
|
// Create a wrapper function that takes a ULID but calls the original function with an int
|
||||||
customers, err := models.GetCustomersByCompanyID(c.Request.Context(), companyID)
|
getByCompanyIDFn := func(ctx context.Context, _ types.ULID) ([]models.Customer, error) {
|
||||||
|
return models.GetCustomersByCompanyID(ctx, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get customers from the database and convert to DTOs
|
||||||
|
customers, err := getByCompanyIDFn(c.Request.Context(), types.ULID{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error retrieving customers: "+err.Error())
|
responses.InternalErrorResponse(c, "Error retrieving customers: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTOs
|
// Convert to DTOs
|
||||||
customerDTOs := utils.ConvertToDTO(customers, convertCustomerToDTO)
|
customerDTOs := utils.ConvertToDTO(customers, convertCustomerToDTO)
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, customerDTOs)
|
responses.SuccessResponse(c, http.StatusOK, customerDTOs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCustomer handles POST /customers
|
// CreateCustomer handles POST /customers
|
||||||
@ -106,37 +113,19 @@ func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers [post]
|
// @Router /customers [post]
|
||||||
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
||||||
|
// We need to use a custom wrapper for CreateCustomer because we need to get the user ID from the context
|
||||||
userID, err := middleware.GetUserIDFromContext(c)
|
userID, err := middleware.GetUserIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
responses.UnauthorizedResponse(c, "User not authenticated")
|
||||||
return
|
|
||||||
}
|
|
||||||
// Parse request body
|
|
||||||
var customerCreateDTO dto.CustomerCreateDto
|
|
||||||
if err := utils.BindJSON(c, &customerCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert DTO to model
|
// Use a closure to capture the userID
|
||||||
customerCreate, err := convertCreateCustomerDTOToModel(customerCreateDTO)
|
createFn := func(ctx context.Context, createDTO dto.CustomerCreateDto) (*models.Customer, error) {
|
||||||
if err != nil {
|
return createCustomerWrapper(ctx, createDTO, userID)
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
customerCreate.OwnerUserID = &userID
|
|
||||||
|
|
||||||
// Create customer in the database
|
|
||||||
customer, err := models.CreateCustomer(c.Request.Context(), customerCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating customer: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
utils.HandleCreate(c, createFn, convertCustomerToDTO, "customer")
|
||||||
customerDTO := convertCustomerToDTO(customer)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, customerDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCustomer handles PUT /customers/:id
|
// UpdateCustomer handles PUT /customers/:id
|
||||||
@ -156,46 +145,7 @@ func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers/{id} [put]
|
// @Router /customers/{id} [put]
|
||||||
func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
|
func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateCustomer, convertCustomerToDTO, prepareCustomerUpdate, "customer")
|
||||||
id, err := utils.ParseID(c, "id")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid customer ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var customerUpdateDTO dto.CustomerUpdateDto
|
|
||||||
if err := utils.BindJSON(c, &customerUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set ID from URL
|
|
||||||
customerUpdateDTO.ID = id.String()
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
customerUpdate, err := convertUpdateCustomerDTOToModel(customerUpdateDTO)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update customer in the database
|
|
||||||
customer, err := models.UpdateCustomer(c.Request.Context(), customerUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating customer: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if customer == nil {
|
|
||||||
utils.NotFoundResponse(c, "Customer not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
customerDTO := convertCustomerToDTO(customer)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, customerDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCustomer handles DELETE /customers/:id
|
// DeleteCustomer handles DELETE /customers/:id
|
||||||
@ -282,3 +232,47 @@ func parseCompanyID(idStr string) (int, error) {
|
|||||||
_, err := fmt.Sscanf(idStr, "%d", &id)
|
_, err := fmt.Sscanf(idStr, "%d", &id)
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareCustomerUpdate prepares the customer update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareCustomerUpdate(c *gin.Context) (models.CustomerUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid customer ID format")
|
||||||
|
return models.CustomerUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var customerUpdateDTO dto.CustomerUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &customerUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.CustomerUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ID from URL
|
||||||
|
customerUpdateDTO.ID = id.String()
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
customerUpdate, err := convertUpdateCustomerDTOToModel(customerUpdateDTO)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||||
|
return models.CustomerUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return customerUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCustomerWrapper is a wrapper function for models.CreateCustomer that takes a DTO as input
|
||||||
|
func createCustomerWrapper(ctx context.Context, createDTO dto.CustomerCreateDto, userID types.ULID) (*models.Customer, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
customerCreate, err := convertCreateCustomerDTOToModel(createDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the owner user ID
|
||||||
|
customerCreate.OwnerUserID = &userID
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateCustomer(ctx, customerCreate)
|
||||||
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@ -48,17 +49,7 @@ func (h *ProjectHandler) GetProjects(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/with-customers [get]
|
// @Router /projects/with-customers [get]
|
||||||
func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) {
|
func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) {
|
||||||
// Get projects with customers from the database
|
utils.HandleGetAll(c, models.GetAllProjectsWithCustomers, convertProjectToDTO, "projects with customers")
|
||||||
projects, err := models.GetAllProjectsWithCustomers(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
projectDTOs := utils.ConvertToDTO(projects, convertProjectToDTO)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, projectDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectByID handles GET /projects/:id
|
// GetProjectByID handles GET /projects/:id
|
||||||
@ -95,24 +86,7 @@ func (h *ProjectHandler) GetProjectByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/customer/{customerId} [get]
|
// @Router /projects/customer/{customerId} [get]
|
||||||
func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
||||||
// Parse customer ID from URL
|
utils.HandleGetByFilter(c, models.GetProjectsByCustomerID, convertProjectToDTO, "projects", "customerId")
|
||||||
customerID, err := utils.ParseID(c, "customerId")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid customer ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get projects from the database
|
|
||||||
projects, err := models.GetProjectsByCustomerID(c.Request.Context(), customerID)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
projectDTOs := utils.ConvertToDTO(projects, convertProjectToDTO)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, projectDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateProject handles POST /projects
|
// CreateProject handles POST /projects
|
||||||
@ -130,31 +104,7 @@ func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects [post]
|
// @Router /projects [post]
|
||||||
func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createProjectWrapper, convertProjectToDTO, "project")
|
||||||
var projectCreateDTO dto.ProjectCreateDto
|
|
||||||
if err := utils.BindJSON(c, &projectCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
projectCreate, err := convertCreateProjectDTOToModel(projectCreateDTO)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create project in the database
|
|
||||||
project, err := models.CreateProject(c.Request.Context(), projectCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating project: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
projectDTO := convertProjectToDTO(project)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, projectDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateProject handles PUT /projects/:id
|
// UpdateProject handles PUT /projects/:id
|
||||||
@ -174,43 +124,7 @@ func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/{id} [put]
|
// @Router /projects/{id} [put]
|
||||||
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateProject, convertProjectToDTO, prepareProjectUpdate, "project")
|
||||||
id, err := utils.ParseID(c, "id")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid project ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var projectUpdateDTO dto.ProjectUpdateDto
|
|
||||||
if err := utils.BindJSON(c, &projectUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
projectUpdate, err := convertUpdateProjectDTOToModel(projectUpdateDTO, id)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update project in the database
|
|
||||||
project, err := models.UpdateProject(c.Request.Context(), projectUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating project: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if project == nil {
|
|
||||||
utils.NotFoundResponse(c, "Project not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
projectDTO := convertProjectToDTO(project)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, projectDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProject handles DELETE /projects/:id
|
// DeleteProject handles DELETE /projects/:id
|
||||||
@ -228,21 +142,7 @@ func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/{id} [delete]
|
// @Router /projects/{id} [delete]
|
||||||
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
|
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteProject, "project")
|
||||||
id, err := utils.ParseID(c, "id")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid project ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete project from the database
|
|
||||||
err = models.DeleteProject(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting project: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@ -294,3 +194,41 @@ func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto, id types.ULID) (mo
|
|||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareProjectUpdate prepares the project update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareProjectUpdate(c *gin.Context) (models.ProjectUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid project ID format")
|
||||||
|
return models.ProjectUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var projectUpdateDTO dto.ProjectUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &projectUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.ProjectUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
projectUpdate, err := convertUpdateProjectDTOToModel(projectUpdateDTO, id)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.ProjectUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createProjectWrapper is a wrapper function for models.CreateProject that takes a DTO as input
|
||||||
|
func createProjectWrapper(ctx context.Context, createDTO dto.ProjectCreateDto) (*models.Project, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
projectCreate, err := convertCreateProjectDTOToModel(createDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateProject(ctx, projectCreate)
|
||||||
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
"github.com/timetracker/backend/internal/api/middleware"
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@ -35,20 +33,7 @@ func NewTimeEntryHandler() *TimeEntryHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries [get]
|
// @Router /time-entries [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
||||||
// Get time entries from the database
|
utils.HandleGetAll(c, models.GetAllTimeEntries, convertTimeEntryToDTO, "time entries")
|
||||||
timeEntries, err := models.GetAllTimeEntries(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntryByID handles GET /time-entries/:id
|
// GetTimeEntryByID handles GET /time-entries/:id
|
||||||
@ -67,30 +52,7 @@ func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/{id} [get]
|
// @Router /time-entries/{id} [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleGetByID(c, models.GetTimeEntryByID, convertTimeEntryToDTO, "time entry")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entry from the database
|
|
||||||
timeEntry, err := models.GetTimeEntryByID(c.Request.Context(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeEntry == nil {
|
|
||||||
utils.NotFoundResponse(c, "Time entry not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntriesByUserID handles GET /time-entries/user/:userId
|
// GetTimeEntriesByUserID handles GET /time-entries/user/:userId
|
||||||
@ -108,28 +70,7 @@ func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/user/{userId} [get]
|
// @Router /time-entries/user/{userId} [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
||||||
// Parse user ID from URL
|
utils.HandleGetByFilter(c, models.GetTimeEntriesByUserID, convertTimeEntryToDTO, "time entries", "userId")
|
||||||
userIDStr := c.Param("userId")
|
|
||||||
userID, err := ulid.Parse(userIDStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByUserID(c.Request.Context(), types.FromULID(userID))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMyTimeEntries handles GET /time-entries/me
|
// GetMyTimeEntries handles GET /time-entries/me
|
||||||
@ -145,27 +86,7 @@ func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/me [get]
|
// @Router /time-entries/me [get]
|
||||||
func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
||||||
// Get user ID from context (set by AuthMiddleware)
|
utils.HandleGetByUserID(c, models.GetTimeEntriesByUserID, convertTimeEntryToDTO, "time entries")
|
||||||
userID, err := middleware.GetUserIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByUserID(c.Request.Context(), userID)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntriesByProjectID handles GET /time-entries/project/:projectId
|
// GetTimeEntriesByProjectID handles GET /time-entries/project/:projectId
|
||||||
@ -183,27 +104,7 @@ func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/project/{projectId} [get]
|
// @Router /time-entries/project/{projectId} [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
||||||
// Parse project ID from URL
|
utils.HandleGetByFilter(c, models.GetTimeEntriesByProjectID, convertTimeEntryToDTO, "time entries", "projectId")
|
||||||
projectID, err := utils.ParseID(c, "projectId")
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid project ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByProjectID(c.Request.Context(), projectID)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntriesByDateRange handles GET /time-entries/range
|
// GetTimeEntriesByDateRange handles GET /time-entries/range
|
||||||
@ -222,46 +123,7 @@ func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/range [get]
|
// @Router /time-entries/range [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
||||||
// Parse date range from query parameters
|
utils.HandleGetByDateRange(c, models.GetTimeEntriesByDateRange, convertTimeEntryToDTO, "time entries")
|
||||||
startStr := c.Query("start")
|
|
||||||
endStr := c.Query("end")
|
|
||||||
|
|
||||||
if startStr == "" || endStr == "" {
|
|
||||||
utils.BadRequestResponse(c, "Start and end dates are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
start, err := time.Parse(time.RFC3339, startStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid start date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
end, err := time.Parse(time.RFC3339, endStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid end date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if end.Before(start) {
|
|
||||||
utils.BadRequestResponse(c, "End date cannot be before start date")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByDateRange(c.Request.Context(), start, end)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTimeEntry handles POST /time-entries
|
// CreateTimeEntry handles POST /time-entries
|
||||||
@ -279,31 +141,7 @@ func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries [post]
|
// @Router /time-entries [post]
|
||||||
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createTimeEntryWrapper, convertTimeEntryToDTO, "time entry")
|
||||||
var timeEntryCreateDTO dto.TimeEntryCreateDto
|
|
||||||
if err := c.ShouldBindJSON(&timeEntryCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
timeEntryCreate, err := convertCreateTimeEntryDTOToModel(timeEntryCreateDTO)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create time entry in the database
|
|
||||||
timeEntry, err := models.CreateTimeEntry(c.Request.Context(), timeEntryCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, timeEntryDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTimeEntry handles PUT /time-entries/:id
|
// UpdateTimeEntry handles PUT /time-entries/:id
|
||||||
@ -323,44 +161,7 @@ func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/{id} [put]
|
// @Router /time-entries/{id} [put]
|
||||||
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateTimeEntry, convertTimeEntryToDTO, prepareTimeEntryUpdate, "time entry")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := types.ULIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var timeEntryUpdateDTO dto.TimeEntryUpdateDto
|
|
||||||
if err := c.ShouldBindJSON(&timeEntryUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
timeEntryUpdate, err := convertUpdateTimeEntryDTOToModel(timeEntryUpdateDTO, id)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update time entry in the database
|
|
||||||
timeEntry, err := models.UpdateTimeEntry(c.Request.Context(), timeEntryUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeEntry == nil {
|
|
||||||
utils.NotFoundResponse(c, "Time entry not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTimeEntry handles DELETE /time-entries/:id
|
// DeleteTimeEntry handles DELETE /time-entries/:id
|
||||||
@ -378,22 +179,7 @@ func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/{id} [delete]
|
// @Router /time-entries/{id} [delete]
|
||||||
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
|
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteTimeEntry, "time entry")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete time entry from the database
|
|
||||||
err = models.DeleteTimeEntry(c.Request.Context(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@ -489,3 +275,42 @@ func convertUpdateTimeEntryDTOToModel(dto dto.TimeEntryUpdateDto, id types.ULID)
|
|||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareTimeEntryUpdate prepares the time entry update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareTimeEntryUpdate(c *gin.Context) (models.TimeEntryUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := types.ULIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid time entry ID format")
|
||||||
|
return models.TimeEntryUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var timeEntryUpdateDTO dto.TimeEntryUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &timeEntryUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.TimeEntryUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
timeEntryUpdate, err := convertUpdateTimeEntryDTOToModel(timeEntryUpdateDTO, id)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.TimeEntryUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeEntryUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTimeEntryWrapper is a wrapper function for models.CreateTimeEntry that takes a DTO as input
|
||||||
|
func createTimeEntryWrapper(ctx context.Context, createDTO dto.TimeEntryCreateDto) (*models.TimeEntry, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
timeEntryCreate, err := convertCreateTimeEntryDTOToModel(createDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateTimeEntry(ctx, timeEntryCreate)
|
||||||
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
"github.com/timetracker/backend/internal/api/middleware"
|
"github.com/timetracker/backend/internal/api/middleware"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@ -33,20 +34,7 @@ func NewUserHandler() *UserHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users [get]
|
// @Router /users [get]
|
||||||
func (h *UserHandler) GetUsers(c *gin.Context) {
|
func (h *UserHandler) GetUsers(c *gin.Context) {
|
||||||
// Get users from the database
|
utils.HandleGetAll(c, models.GetAllUsers, convertUserToDTO, "users")
|
||||||
users, err := models.GetAllUsers(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving users: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
userDTOs := make([]dto.UserDto, len(users))
|
|
||||||
for i, user := range users {
|
|
||||||
userDTOs[i] = convertUserToDTO(&user)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByID handles GET /users/:id
|
// GetUserByID handles GET /users/:id
|
||||||
@ -65,30 +53,29 @@ func (h *UserHandler) GetUsers(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users/{id} [get]
|
// @Router /users/{id} [get]
|
||||||
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
// We need a custom wrapper for GetUserByID because the ID parameter is parsed differently
|
||||||
idStr := c.Param("id")
|
id, err := utils.ParseID(c, "id")
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
responses.BadRequestResponse(c, "Invalid user ID format")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user from the database
|
// Get user from the database
|
||||||
user, err := models.GetUserByID(c.Request.Context(), types.FromULID(id))
|
user, err := models.GetUserByID(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
responses.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.NotFoundResponse(c, "User not found")
|
responses.NotFoundResponse(c, "User not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
// Convert to DTO
|
||||||
userDTO := convertUserToDTO(user)
|
userDTO := convertUserToDTO(user)
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTO)
|
responses.SuccessResponse(c, http.StatusOK, userDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser handles POST /users
|
// CreateUser handles POST /users
|
||||||
@ -106,27 +93,7 @@ func (h *UserHandler) GetUserByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users [post]
|
// @Router /users [post]
|
||||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createUserWrapper, convertUserToDTO, "user")
|
||||||
var userCreateDTO dto.UserCreateDto
|
|
||||||
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
userCreate := convertCreateDTOToModel(userCreateDTO)
|
|
||||||
|
|
||||||
// Create user in the database
|
|
||||||
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
userDTO := convertUserToDTO(user)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, userDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser handles PUT /users/:id
|
// UpdateUser handles PUT /users/:id
|
||||||
@ -146,68 +113,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users/{id} [put]
|
// @Router /users/{id} [put]
|
||||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateUser, convertUserToDTO, prepareUserUpdate, "user")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := types.ULIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var userUpdateDTO dto.UserUpdateDto
|
|
||||||
if err := c.ShouldBindJSON(&userUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to Model
|
|
||||||
update := models.UserUpdate{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if userUpdateDTO.Email != nil {
|
|
||||||
update.Email = userUpdateDTO.Email
|
|
||||||
}
|
|
||||||
if userUpdateDTO.Password != nil {
|
|
||||||
update.Password = userUpdateDTO.Password
|
|
||||||
}
|
|
||||||
if userUpdateDTO.Role != nil {
|
|
||||||
update.Role = userUpdateDTO.Role
|
|
||||||
}
|
|
||||||
|
|
||||||
if userUpdateDTO.CompanyID.Valid {
|
|
||||||
if userUpdateDTO.CompanyID.Value != nil {
|
|
||||||
companyID, err := types.ULIDFromString(*userUpdateDTO.CompanyID.Value)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
update.CompanyID = types.NewNullable(companyID)
|
|
||||||
} else {
|
|
||||||
update.CompanyID = types.Null[types.ULID]()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if userUpdateDTO.HourlyRate != nil {
|
|
||||||
update.HourlyRate = userUpdateDTO.HourlyRate
|
|
||||||
}
|
|
||||||
// Update user in the database
|
|
||||||
user, err := models.UpdateUser(c.Request.Context(), update)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
utils.NotFoundResponse(c, "User not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
userDTO := convertUserToDTO(user)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser handles DELETE /users/:id
|
// DeleteUser handles DELETE /users/:id
|
||||||
@ -225,22 +131,7 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users/{id} [delete]
|
// @Router /users/{id} [delete]
|
||||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteUser, "user")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete user from the database
|
|
||||||
err = models.DeleteUser(c.Request.Context(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login handles POST /auth/login
|
// Login handles POST /auth/login
|
||||||
@ -260,21 +151,21 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
// Parse request body
|
// Parse request body
|
||||||
var loginDTO dto.LoginDto
|
var loginDTO dto.LoginDto
|
||||||
if err := c.ShouldBindJSON(&loginDTO); err != nil {
|
if err := c.ShouldBindJSON(&loginDTO); err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
responses.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate user
|
// Authenticate user
|
||||||
user, err := models.AuthenticateUser(c.Request.Context(), loginDTO.Email, loginDTO.Password)
|
user, err := models.AuthenticateUser(c.Request.Context(), loginDTO.Email, loginDTO.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "Invalid login credentials")
|
responses.UnauthorizedResponse(c, "Invalid login credentials")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
token, err := middleware.GenerateToken(user, c)
|
token, err := middleware.GenerateToken(user, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
responses.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +175,7 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
User: convertUserToDTO(user),
|
User: convertUserToDTO(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, tokenDTO)
|
responses.SuccessResponse(c, http.StatusOK, tokenDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register handles POST /auth/register
|
// Register handles POST /auth/register
|
||||||
@ -303,7 +194,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
// Parse request body
|
// Parse request body
|
||||||
var userCreateDTO dto.UserCreateDto
|
var userCreateDTO dto.UserCreateDto
|
||||||
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
responses.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,14 +204,14 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
// Create user in the database
|
// Create user in the database
|
||||||
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
responses.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
token, err := middleware.GenerateToken(user, c)
|
token, err := middleware.GenerateToken(user, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
responses.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +221,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
User: convertUserToDTO(user),
|
User: convertUserToDTO(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, tokenDTO)
|
responses.SuccessResponse(c, http.StatusCreated, tokenDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentUser handles GET /auth/me
|
// GetCurrentUser handles GET /auth/me
|
||||||
@ -349,26 +240,26 @@ func (h *UserHandler) GetCurrentUser(c *gin.Context) {
|
|||||||
// Get user ID from context (set by AuthMiddleware)
|
// Get user ID from context (set by AuthMiddleware)
|
||||||
userID, err := middleware.GetUserIDFromContext(c)
|
userID, err := middleware.GetUserIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
responses.UnauthorizedResponse(c, "User not authenticated")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user from the database
|
// Get user from the database
|
||||||
user, err := models.GetUserByID(c.Request.Context(), userID)
|
user, err := models.GetUserByID(c.Request.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
responses.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.NotFoundResponse(c, "User not found")
|
responses.NotFoundResponse(c, "User not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
// Convert to DTO
|
||||||
userDTO := convertUserToDTO(user)
|
userDTO := convertUserToDTO(user)
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTO)
|
responses.SuccessResponse(c, http.StatusOK, userDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@ -390,6 +281,58 @@ func convertUserToDTO(user *models.User) dto.UserDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareUserUpdate prepares the user update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareUserUpdate(c *gin.Context) (models.UserUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := types.ULIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid user ID format")
|
||||||
|
return models.UserUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var userUpdateDTO dto.UserUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &userUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.UserUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to Model
|
||||||
|
update := models.UserUpdate{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateDTO.Email != nil {
|
||||||
|
update.Email = userUpdateDTO.Email
|
||||||
|
}
|
||||||
|
if userUpdateDTO.Password != nil {
|
||||||
|
update.Password = userUpdateDTO.Password
|
||||||
|
}
|
||||||
|
if userUpdateDTO.Role != nil {
|
||||||
|
update.Role = userUpdateDTO.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateDTO.CompanyID.Valid {
|
||||||
|
if userUpdateDTO.CompanyID.Value != nil {
|
||||||
|
companyID, err := types.ULIDFromString(*userUpdateDTO.CompanyID.Value)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid company ID format")
|
||||||
|
return models.UserUpdate{}, err
|
||||||
|
}
|
||||||
|
update.CompanyID = types.NewNullable(companyID)
|
||||||
|
} else {
|
||||||
|
update.CompanyID = types.Null[types.ULID]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateDTO.HourlyRate != nil {
|
||||||
|
update.HourlyRate = userUpdateDTO.HourlyRate
|
||||||
|
}
|
||||||
|
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
||||||
var companyID *types.ULID
|
var companyID *types.ULID
|
||||||
if dto.CompanyID != nil {
|
if dto.CompanyID != nil {
|
||||||
@ -405,3 +348,12 @@ func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
|||||||
HourlyRate: dto.HourlyRate,
|
HourlyRate: dto.HourlyRate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createUserWrapper is a wrapper function for models.CreateUser that takes a DTO as input
|
||||||
|
func createUserWrapper(ctx context.Context, createDTO dto.UserCreateDto) (*models.User, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
userCreate := convertCreateDTOToModel(createDTO)
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateUser(ctx, userCreate)
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/config"
|
"github.com/timetracker/backend/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,14 +18,14 @@ func APIKeyMiddleware(cfg *config.Config) gin.HandlerFunc {
|
|||||||
// Get API key from header
|
// Get API key from header
|
||||||
apiKey := c.GetHeader("X-API-Key")
|
apiKey := c.GetHeader("X-API-Key")
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
utils.UnauthorizedResponse(c, "API key is required")
|
responses.UnauthorizedResponse(c, "API key is required")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate API key
|
// Validate API key
|
||||||
if apiKey != cfg.APIKey {
|
if apiKey != cfg.APIKey {
|
||||||
utils.UnauthorizedResponse(c, "Invalid API key")
|
responses.UnauthorizedResponse(c, "Invalid API key")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/config"
|
"github.com/timetracker/backend/internal/config"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
@ -164,10 +164,10 @@ func loadPublicKey(path string) (*rsa.PublicKey, error) {
|
|||||||
|
|
||||||
// Claims represents the JWT claims
|
// Claims represents the JWT claims
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID *string `json:"companyId"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,14 +177,14 @@ func AuthMiddleware() gin.HandlerFunc {
|
|||||||
// Get the token from cookie
|
// Get the token from cookie
|
||||||
tokenString, err := c.Cookie("jwt")
|
tokenString, err := c.Cookie("jwt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "Authentication cookie is required")
|
responses.UnauthorizedResponse(c, "Authentication cookie is required")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := validateToken(tokenString)
|
claims, err := validateToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "Invalid or expired token")
|
responses.UnauthorizedResponse(c, "Invalid or expired token")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -204,7 +204,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
userRole, exists := c.Get("role")
|
userRole, exists := c.Get("role")
|
||||||
if !exists {
|
if !exists {
|
||||||
utils.UnauthorizedResponse(c, "User role not found in context")
|
responses.UnauthorizedResponse(c, "User role not found in context")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -212,7 +212,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
// Check if the user's role is in the allowed roles
|
// Check if the user's role is in the allowed roles
|
||||||
roleStr, ok := userRole.(string)
|
roleStr, ok := userRole.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
utils.InternalErrorResponse(c, "Invalid role type in context")
|
responses.InternalErrorResponse(c, "Invalid role type in context")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -226,7 +226,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
utils.ForbiddenResponse(c, "Insufficient permissions")
|
responses.ForbiddenResponse(c, "Insufficient permissions")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -238,11 +238,16 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
// GenerateToken creates a new JWT token for a user
|
// GenerateToken creates a new JWT token for a user
|
||||||
func GenerateToken(user *models.User, c *gin.Context) (string, error) {
|
func GenerateToken(user *models.User, c *gin.Context) (string, error) {
|
||||||
// Create the claims
|
// Create the claims
|
||||||
|
var companyId *string
|
||||||
|
if user.CompanyID != nil {
|
||||||
|
wrapper := user.CompanyID.String()
|
||||||
|
companyId = &wrapper
|
||||||
|
}
|
||||||
claims := Claims{
|
claims := Claims{
|
||||||
UserID: user.ID.String(),
|
UserID: user.ID.String(),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
CompanyID: user.CompanyID.String(),
|
CompanyID: companyId,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.MustLoadConfig().JWTConfig.TokenDuration)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.MustLoadConfig().JWTConfig.TokenDuration)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package utils
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
@ -3,8 +3,11 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,14 +46,14 @@ func HandleGetAll[M any, D any](
|
|||||||
// Get entities from the database
|
// Get entities from the database
|
||||||
entities, err := getAllFn(c.Request.Context())
|
entities, err := getAllFn(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTOs
|
// Convert to DTOs
|
||||||
dtos := ConvertToDTO(entities, convertFn)
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
SuccessResponse(c, 200, dtos)
|
responses.SuccessResponse(c, 200, dtos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleGetByID is a generic function to handle GET entity by ID endpoints
|
// HandleGetByID is a generic function to handle GET entity by ID endpoints
|
||||||
@ -63,26 +66,26 @@ func HandleGetByID[M any, D any](
|
|||||||
// Parse ID from URL
|
// Parse ID from URL
|
||||||
id, err := ParseID(c, "id")
|
id, err := ParseID(c, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName))
|
responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get entity from the database
|
// Get entity from the database
|
||||||
entity, err := getByIDFn(c.Request.Context(), id)
|
entity, err := getByIDFn(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity == nil {
|
if entity == nil {
|
||||||
NotFoundResponse(c, fmt.Sprintf("%s not found", entityName))
|
responses.NotFoundResponse(c, fmt.Sprintf("%s not found", entityName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
// Convert to DTO
|
||||||
dto := convertFn(entity)
|
dto := convertFn(entity)
|
||||||
|
|
||||||
SuccessResponse(c, 200, dto)
|
responses.SuccessResponse(c, 200, dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleCreate is a generic function to handle POST entity endpoints
|
// HandleCreate is a generic function to handle POST entity endpoints
|
||||||
@ -95,21 +98,21 @@ func HandleCreate[C any, M any, D any](
|
|||||||
// Parse request body
|
// Parse request body
|
||||||
var createDTO C
|
var createDTO C
|
||||||
if err := BindJSON(c, &createDTO); err != nil {
|
if err := BindJSON(c, &createDTO); err != nil {
|
||||||
BadRequestResponse(c, err.Error())
|
responses.BadRequestResponse(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create entity in the database
|
// Create entity in the database
|
||||||
entity, err := createFn(c.Request.Context(), createDTO)
|
entity, err := createFn(c.Request.Context(), createDTO)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InternalErrorResponse(c, fmt.Sprintf("Error creating %s: %s", entityName, err.Error()))
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error creating %s: %s", entityName, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
// Convert to DTO
|
||||||
dto := convertFn(entity)
|
dto := convertFn(entity)
|
||||||
|
|
||||||
SuccessResponse(c, 201, dto)
|
responses.SuccessResponse(c, 201, dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleDelete is a generic function to handle DELETE entity endpoints
|
// HandleDelete is a generic function to handle DELETE entity endpoints
|
||||||
@ -121,16 +124,163 @@ func HandleDelete(
|
|||||||
// Parse ID from URL
|
// Parse ID from URL
|
||||||
id, err := ParseID(c, "id")
|
id, err := ParseID(c, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName))
|
responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete entity from the database
|
// Delete entity from the database
|
||||||
err = deleteFn(c.Request.Context(), id)
|
err = deleteFn(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InternalErrorResponse(c, fmt.Sprintf("Error deleting %s: %s", entityName, err.Error()))
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error deleting %s: %s", entityName, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessResponse(c, 204, nil)
|
responses.SuccessResponse(c, 204, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUpdate is a generic function to handle PUT entity endpoints
|
||||||
|
// It takes a prepareUpdateFn that handles parsing the ID, binding the JSON, and converting the DTO to a model update object
|
||||||
|
func HandleUpdate[U any, M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
updateFn func(ctx context.Context, update U) (*M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
prepareUpdateFn func(*gin.Context) (U, error),
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Prepare the update object (parse ID, bind JSON, convert DTO to model)
|
||||||
|
update, err := prepareUpdateFn(c)
|
||||||
|
if err != nil {
|
||||||
|
// The prepareUpdateFn should handle setting the appropriate error response
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update entity in the database
|
||||||
|
entity, err := updateFn(c.Request.Context(), update)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error updating %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity == nil {
|
||||||
|
responses.NotFoundResponse(c, fmt.Sprintf("%s not found", entityName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTO
|
||||||
|
dto := convertFn(entity)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByFilter is a generic function to handle GET entities by a filter parameter
|
||||||
|
func HandleGetByFilter[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByFilterFn func(ctx context.Context, filterID types.ULID) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
paramName string,
|
||||||
|
) {
|
||||||
|
// Parse filter ID from URL
|
||||||
|
filterID, err := ParseID(c, paramName)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", paramName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getByFilterFn(c.Request.Context(), filterID)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByUserID is a specialized function to handle GET entities by user ID
|
||||||
|
func HandleGetByUserID[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByUserIDFn func(ctx context.Context, userID types.ULID) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Get user ID from context (set by AuthMiddleware)
|
||||||
|
userID, exists := c.Get("userID")
|
||||||
|
if !exists {
|
||||||
|
responses.UnauthorizedResponse(c, "User not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDStr, ok := userID.(string)
|
||||||
|
if !ok {
|
||||||
|
responses.InternalErrorResponse(c, "Invalid user ID type in context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUserID, err := types.ULIDFromString(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error parsing user ID: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getByUserIDFn(c.Request.Context(), parsedUserID)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByDateRange is a specialized function to handle GET entities by date range
|
||||||
|
func HandleGetByDateRange[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByDateRangeFn func(ctx context.Context, start, end time.Time) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Parse date range from query parameters
|
||||||
|
startStr := c.Query("start")
|
||||||
|
endStr := c.Query("end")
|
||||||
|
|
||||||
|
if startStr == "" || endStr == "" {
|
||||||
|
responses.BadRequestResponse(c, "Start and end dates are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := time.Parse(time.RFC3339, startStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid start date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := time.Parse(time.RFC3339, endStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid end date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if end.Before(start) {
|
||||||
|
responses.BadRequestResponse(c, "End date cannot be before start date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getByDateRangeFn(c.Request.Context(), start, end)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dtos)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
packages:
|
packages:
|
||||||
- path: github.com/timetracker/backend/internal/dtos
|
- path: "github.com/timetracker/backend/internal/api/dto"
|
||||||
type_mappings:
|
type_mappings:
|
||||||
"time.Time": "string"
|
"time.Time": "string"
|
||||||
"types.ULID": "string"
|
"types.ULID": "string"
|
||||||
|
25
refactor_plan.md
Normal file
25
refactor_plan.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Refactoring Plan for backend/internal/api/handlers
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Refactor the code in `backend/internal/api/handlers` to reduce repetition and create helper functions for boilerplate operations, utilizing functions from `backend/internal/api/utils/handler_utils.go` and creating new ones if necessary.
|
||||||
|
|
||||||
|
## Analysis
|
||||||
|
|
||||||
|
The following common patterns were identified in the handler files:
|
||||||
|
|
||||||
|
1. **Error Handling:** Each handler function repeats the same error handling pattern.
|
||||||
|
2. **DTO Binding:** Parsing the request body and handling potential errors.
|
||||||
|
3. **ID Parsing:** Parsing the ID from the URL and handling potential errors.
|
||||||
|
4. **DTO Conversion:** Converting between DTOs and models.
|
||||||
|
5. **Success Responses:** Calling `responses.SuccessResponse` with the appropriate HTTP status code and data.
|
||||||
|
6. **Not Found Responses:** Checking if a record exists and calling `responses.NotFoundResponse` if it doesn't.
|
||||||
|
|
||||||
|
The `Update` handler is the most complex and has the most potential for refactoring.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. **Implement a generic `HandleUpdate` function in `handler_utils.go`:** This function will encapsulate the common logic for updating entities, including parsing the ID, binding the JSON, converting the DTO to a model, calling the update function, and handling errors and not found cases. The function will also handle nullable fields correctly.
|
||||||
|
2. **Modify the existing handlers to use the new `HandleUpdate` function:** This will involve removing the duplicated code from each handler and calling the generic function instead.
|
||||||
|
3. **Create new helper functions in `handler_utils.go` if needed:** If there are any specific operations that are not covered by the existing utility functions, I will create new ones to handle them.
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user