Compare commits
3 Commits
aa5c7e77fc
...
8785b86bfc
Author | SHA1 | Date | |
---|---|---|---|
8785b86bfc | |||
58173b436c | |||
558ee70c21 |
@ -9,47 +9,62 @@ import (
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
_ "github.com/timetracker/backend/docs" // This line is important for swag to work
|
||||
"github.com/timetracker/backend/internal/api/routes"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
_ "gorm.io/driver/postgres"
|
||||
// GORM IMPORTS MARKER
|
||||
)
|
||||
|
||||
// @title Time Tracker API
|
||||
// @version 1.0
|
||||
// @description This is a simple time tracker API.
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
// @title Time Tracker API
|
||||
// @version 1.0
|
||||
// @description This is a simple time tracker API.
|
||||
// @host localhost:8080
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @Summary Say hello
|
||||
// @Description Get a hello message
|
||||
// @ID hello
|
||||
// @Produce plain
|
||||
// @Success 200 {string} string "Hello from the Time Tracker Backend!"
|
||||
// @Router / [get]
|
||||
// @x-extension ulid.ULID string
|
||||
|
||||
// @Summary Say hello
|
||||
// @Description Get a hello message
|
||||
// @ID hello
|
||||
// @Produce plain
|
||||
// @Success 200 {string} string "Hello from the Time Tracker Backend!"
|
||||
// @Router / [get]
|
||||
func helloHandler(c *gin.Context) {
|
||||
c.String(http.StatusOK, "Hello from the Time Tracker Backend!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// Configure database
|
||||
dbConfig := models.DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Password: "password",
|
||||
DBName: "mydatabase",
|
||||
SSLMode: "disable", // Für Entwicklungsumgebung
|
||||
SSLMode: "disable", // For development environment
|
||||
}
|
||||
|
||||
// Datenbank initialisieren
|
||||
// Initialize database
|
||||
if err := models.InitDB(dbConfig); err != nil {
|
||||
log.Fatalf("Fehler bei der DB-Initialisierung: %v", err)
|
||||
log.Fatalf("Error initializing database: %v", err)
|
||||
}
|
||||
|
||||
// Create Gin router
|
||||
r := gin.Default()
|
||||
|
||||
// Basic route for health check
|
||||
r.GET("/", helloHandler)
|
||||
|
||||
// Setup API routes
|
||||
routes.SetupRouter(r)
|
||||
|
||||
// Swagger documentation
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// Start server
|
||||
fmt.Println("Server listening on port 8080")
|
||||
r.Run(":8080") // Use Gin's Run method
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
|
@ -37,6 +37,8 @@ github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0
|
||||
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
253
backend/internal/api/handlers/activity_handler.go
Normal file
253
backend/internal/api/handlers/activity_handler.go
Normal file
@ -0,0 +1,253 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// ActivityHandler handles activity-related API endpoints
|
||||
type ActivityHandler struct{}
|
||||
|
||||
// NewActivityHandler creates a new ActivityHandler
|
||||
func NewActivityHandler() *ActivityHandler {
|
||||
return &ActivityHandler{}
|
||||
}
|
||||
|
||||
// GetActivities handles GET /activities
|
||||
//
|
||||
// @Summary Get all activities
|
||||
// @Description Get a list of all activities
|
||||
// @Tags activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]utils.ActivityResponse}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /activities [get]
|
||||
func (h *ActivityHandler) GetActivities(c *gin.Context) {
|
||||
// Get activities from the database
|
||||
activities, err := models.GetAllActivities(c.Request.Context())
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving activities: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
activityDTOs := make([]dto.ActivityDto, len(activities))
|
||||
for i, activity := range activities {
|
||||
activityDTOs[i] = convertActivityToDTO(&activity)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, activityDTOs)
|
||||
}
|
||||
|
||||
// GetActivityByID handles GET /activities/:id
|
||||
//
|
||||
// @Summary Get activity by ID
|
||||
// @Description Get an activity by its ID
|
||||
// @Tags activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Activity ID"
|
||||
// @Success 200 {object} utils.Response{data=utils.ActivityResponse}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /activities/{id} [get]
|
||||
func (h *ActivityHandler) GetActivityByID(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid activity ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get activity from the database
|
||||
activity, err := models.GetActivityByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving 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)
|
||||
}
|
||||
|
||||
// CreateActivity handles POST /activities
|
||||
//
|
||||
// @Summary Create a new activity
|
||||
// @Description Create a new activity
|
||||
// @Tags activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param activity body dto.ActivityCreateDto true "Activity data"
|
||||
// @Success 201 {object} utils.Response{data=dto.ActivityDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /activities [post]
|
||||
func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
||||
// Parse request body
|
||||
var activityCreateDTO dto.ActivityCreateDto
|
||||
if err := c.ShouldBindJSON(&activityCreateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+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
|
||||
//
|
||||
// @Summary Update an activity
|
||||
// @Description Update an existing activity
|
||||
// @Tags activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Activity ID"
|
||||
// @Param activity body dto.ActivityUpdateDto true "Activity data"
|
||||
// @Success 200 {object} utils.Response{data=dto.ActivityDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /activities/{id} [put]
|
||||
func (h *ActivityHandler) UpdateActivity(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid activity ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var activityUpdateDTO dto.ActivityUpdateDto
|
||||
if err := c.ShouldBindJSON(&activityUpdateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+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
|
||||
//
|
||||
// @Summary Delete an activity
|
||||
// @Description Delete an activity by its ID
|
||||
// @Tags activities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Activity ID"
|
||||
// @Success 204 {object} utils.Response
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /activities/{id} [delete]
|
||||
func (h *ActivityHandler) DeleteActivity(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid activity ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete activity from the database
|
||||
err = models.DeleteActivity(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error deleting activity: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Helper functions for DTO conversion
|
||||
|
||||
func convertActivityToDTO(activity *models.Activity) dto.ActivityDto {
|
||||
return dto.ActivityDto{
|
||||
ID: activity.ID.String(),
|
||||
CreatedAt: activity.CreatedAt,
|
||||
UpdatedAt: activity.UpdatedAt,
|
||||
Name: activity.Name,
|
||||
BillingRate: activity.BillingRate,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCreateActivityDTOToModel(dto dto.ActivityCreateDto) models.ActivityCreate {
|
||||
return models.ActivityCreate{
|
||||
Name: dto.Name,
|
||||
BillingRate: dto.BillingRate,
|
||||
}
|
||||
}
|
||||
|
||||
func convertUpdateActivityDTOToModel(dto dto.ActivityUpdateDto) models.ActivityUpdate {
|
||||
id, _ := ulid.Parse(dto.ID)
|
||||
update := models.ActivityUpdate{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if dto.Name != nil {
|
||||
update.Name = dto.Name
|
||||
}
|
||||
|
||||
if dto.BillingRate != nil {
|
||||
update.BillingRate = dto.BillingRate
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
247
backend/internal/api/handlers/company_handler.go
Normal file
247
backend/internal/api/handlers/company_handler.go
Normal file
@ -0,0 +1,247 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// CompanyHandler handles company-related API endpoints
|
||||
type CompanyHandler struct{}
|
||||
|
||||
// NewCompanyHandler creates a new CompanyHandler
|
||||
func NewCompanyHandler() *CompanyHandler {
|
||||
return &CompanyHandler{}
|
||||
}
|
||||
|
||||
// GetCompanies handles GET /companies
|
||||
//
|
||||
// @Summary Get all companies
|
||||
// @Description Get a list of all companies
|
||||
// @Tags companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.CompanyDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /companies [get]
|
||||
func (h *CompanyHandler) GetCompanies(c *gin.Context) {
|
||||
// Get companies from the database
|
||||
companies, err := models.GetAllCompanies(c.Request.Context())
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving companies: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
companyDTOs := make([]dto.CompanyDto, len(companies))
|
||||
for i, company := range companies {
|
||||
companyDTOs[i] = convertCompanyToDTO(&company)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, companyDTOs)
|
||||
}
|
||||
|
||||
// GetCompanyByID handles GET /companies/:id
|
||||
//
|
||||
// @Summary Get company by ID
|
||||
// @Description Get a company by its ID
|
||||
// @Tags companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Company ID"
|
||||
// @Success 200 {object} utils.Response{data=dto.CompanyDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /companies/{id} [get]
|
||||
func (h *CompanyHandler) GetCompanyByID(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get company from the database
|
||||
company, err := models.GetCompanyByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving 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)
|
||||
}
|
||||
|
||||
// CreateCompany handles POST /companies
|
||||
//
|
||||
// @Summary Create a new company
|
||||
// @Description Create a new company
|
||||
// @Tags companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param company body dto.CompanyCreateDto true "Company data"
|
||||
// @Success 201 {object} utils.Response{data=dto.CompanyDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /companies [post]
|
||||
func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
||||
// Parse request body
|
||||
var companyCreateDTO dto.CompanyCreateDto
|
||||
if err := c.ShouldBindJSON(&companyCreateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+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
|
||||
//
|
||||
// @Summary Update a company
|
||||
// @Description Update an existing company
|
||||
// @Tags companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param company body dto.CompanyUpdateDto true "Company data"
|
||||
// @Success 200 {object} utils.Response{data=dto.CompanyDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /companies/{id} [put]
|
||||
func (h *CompanyHandler) UpdateCompany(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var companyUpdateDTO dto.CompanyUpdateDto
|
||||
if err := c.ShouldBindJSON(&companyUpdateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set ID from URL
|
||||
companyUpdateDTO.ID = id.String()
|
||||
|
||||
// Convert DTO to model
|
||||
companyUpdate := convertUpdateCompanyDTOToModel(companyUpdateDTO)
|
||||
|
||||
// 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
|
||||
//
|
||||
// @Summary Delete a company
|
||||
// @Description Delete a company by its ID
|
||||
// @Tags companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Company ID"
|
||||
// @Success 204 {object} utils.Response
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /companies/{id} [delete]
|
||||
func (h *CompanyHandler) DeleteCompany(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete company from the database
|
||||
err = models.DeleteCompany(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error deleting company: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Helper functions for DTO conversion
|
||||
|
||||
func convertCompanyToDTO(company *models.Company) dto.CompanyDto {
|
||||
return dto.CompanyDto{
|
||||
ID: company.ID.String(),
|
||||
CreatedAt: company.CreatedAt,
|
||||
UpdatedAt: company.UpdatedAt,
|
||||
Name: company.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCreateCompanyDTOToModel(dto dto.CompanyCreateDto) models.CompanyCreate {
|
||||
return models.CompanyCreate{
|
||||
Name: dto.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertUpdateCompanyDTOToModel(dto dto.CompanyUpdateDto) models.CompanyUpdate {
|
||||
id, _ := ulid.Parse(dto.ID)
|
||||
update := models.CompanyUpdate{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if dto.Name != nil {
|
||||
update.Name = dto.Name
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
300
backend/internal/api/handlers/customer_handler.go
Normal file
300
backend/internal/api/handlers/customer_handler.go
Normal file
@ -0,0 +1,300 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// CustomerHandler handles customer-related API endpoints
|
||||
type CustomerHandler struct{}
|
||||
|
||||
// NewCustomerHandler creates a new CustomerHandler
|
||||
func NewCustomerHandler() *CustomerHandler {
|
||||
return &CustomerHandler{}
|
||||
}
|
||||
|
||||
// GetCustomers handles GET /customers
|
||||
//
|
||||
// @Summary Get all customers
|
||||
// @Description Get a list of all customers
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.CustomerDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /customers [get]
|
||||
func (h *CustomerHandler) GetCustomers(c *gin.Context) {
|
||||
// Get customers from the database
|
||||
customers, err := models.GetAllCustomers(c.Request.Context())
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving customers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
customerDTOs := make([]dto.CustomerDto, len(customers))
|
||||
for i, customer := range customers {
|
||||
customerDTOs[i] = convertCustomerToDTO(&customer)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, customerDTOs)
|
||||
}
|
||||
|
||||
// GetCustomerByID handles GET /customers/:id
|
||||
//
|
||||
// @Summary Get customer by ID
|
||||
// @Description Get a customer by its ID
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Success 200 {object} utils.Response{data=dto.CustomerDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /customers/{id} [get]
|
||||
func (h *CustomerHandler) GetCustomerByID(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid customer ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get customer from the database
|
||||
customer, err := models.GetCustomerByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving 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)
|
||||
}
|
||||
|
||||
// GetCustomersByCompanyID handles GET /customers/company/:companyId
|
||||
//
|
||||
// @Summary Get customers by company ID
|
||||
// @Description Get a list of customers for a specific company
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param companyId path int true "Company ID"
|
||||
// @Success 200 {object} utils.Response{data=[]dto.CustomerDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /customers/company/{companyId} [get]
|
||||
func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) {
|
||||
// Parse company ID from URL
|
||||
companyIDStr := c.Param("companyId")
|
||||
companyID, err := parseCompanyID(companyIDStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get customers from the database
|
||||
customers, err := models.GetCustomersByCompanyID(c.Request.Context(), companyID)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving customers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
customerDTOs := make([]dto.CustomerDto, len(customers))
|
||||
for i, customer := range customers {
|
||||
customerDTOs[i] = convertCustomerToDTO(&customer)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, customerDTOs)
|
||||
}
|
||||
|
||||
// CreateCustomer handles POST /customers
|
||||
//
|
||||
// @Summary Create a new customer
|
||||
// @Description Create a new customer
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param customer body dto.CustomerCreateDto true "Customer data"
|
||||
// @Success 201 {object} utils.Response{data=dto.CustomerDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /customers [post]
|
||||
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
||||
// Parse request body
|
||||
var customerCreateDTO dto.CustomerCreateDto
|
||||
if err := c.ShouldBindJSON(&customerCreateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert DTO to model
|
||||
customerCreate := convertCreateCustomerDTOToModel(customerCreateDTO)
|
||||
|
||||
// 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
|
||||
customerDTO := convertCustomerToDTO(customer)
|
||||
|
||||
utils.SuccessResponse(c, http.StatusCreated, customerDTO)
|
||||
}
|
||||
|
||||
// UpdateCustomer handles PUT /customers/:id
|
||||
//
|
||||
// @Summary Update a customer
|
||||
// @Description Update an existing customer
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Param customer body dto.CustomerUpdateDto true "Customer data"
|
||||
// @Success 200 {object} utils.Response{data=dto.CustomerDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /customers/{id} [put]
|
||||
func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid customer ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var customerUpdateDTO dto.CustomerUpdateDto
|
||||
if err := c.ShouldBindJSON(&customerUpdateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set ID from URL
|
||||
customerUpdateDTO.ID = id.String()
|
||||
|
||||
// Convert DTO to model
|
||||
customerUpdate := convertUpdateCustomerDTOToModel(customerUpdateDTO)
|
||||
|
||||
// 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
|
||||
//
|
||||
// @Summary Delete a customer
|
||||
// @Description Delete a customer by its ID
|
||||
// @Tags customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Success 204 {object} utils.Response
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /customers/{id} [delete]
|
||||
func (h *CustomerHandler) DeleteCustomer(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid customer ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete customer from the database
|
||||
err = models.DeleteCustomer(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error deleting customer: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Helper functions for DTO conversion
|
||||
|
||||
func convertCustomerToDTO(customer *models.Customer) dto.CustomerDto {
|
||||
return dto.CustomerDto{
|
||||
ID: customer.ID.String(),
|
||||
CreatedAt: customer.CreatedAt,
|
||||
UpdatedAt: customer.UpdatedAt,
|
||||
Name: customer.Name,
|
||||
CompanyID: customer.CompanyID,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCreateCustomerDTOToModel(dto dto.CustomerCreateDto) models.CustomerCreate {
|
||||
return models.CustomerCreate{
|
||||
Name: dto.Name,
|
||||
CompanyID: dto.CompanyID,
|
||||
}
|
||||
}
|
||||
|
||||
func convertUpdateCustomerDTOToModel(dto dto.CustomerUpdateDto) models.CustomerUpdate {
|
||||
id, _ := ulid.Parse(dto.ID)
|
||||
update := models.CustomerUpdate{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if dto.Name != nil {
|
||||
update.Name = dto.Name
|
||||
}
|
||||
|
||||
if dto.CompanyID != nil {
|
||||
update.CompanyID = dto.CompanyID
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
||||
|
||||
// Helper function to parse company ID from string
|
||||
func parseCompanyID(idStr string) (int, error) {
|
||||
var id int
|
||||
_, err := fmt.Sscanf(idStr, "%d", &id)
|
||||
return id, err
|
||||
}
|
359
backend/internal/api/handlers/project_handler.go
Normal file
359
backend/internal/api/handlers/project_handler.go
Normal file
@ -0,0 +1,359 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// ProjectHandler handles project-related API endpoints
|
||||
type ProjectHandler struct{}
|
||||
|
||||
// NewProjectHandler creates a new ProjectHandler
|
||||
func NewProjectHandler() *ProjectHandler {
|
||||
return &ProjectHandler{}
|
||||
}
|
||||
|
||||
// GetProjects handles GET /projects
|
||||
//
|
||||
// @Summary Get all projects
|
||||
// @Description Get a list of all projects
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.ProjectDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects [get]
|
||||
func (h *ProjectHandler) GetProjects(c *gin.Context) {
|
||||
// Get projects from the database
|
||||
projects, err := models.GetAllProjects(c.Request.Context())
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
projectDTOs := make([]dto.ProjectDto, len(projects))
|
||||
for i, project := range projects {
|
||||
projectDTOs[i] = convertProjectToDTO(&project)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, projectDTOs)
|
||||
}
|
||||
|
||||
// GetProjectsWithCustomers handles GET /projects/with-customers
|
||||
//
|
||||
// @Summary Get all projects with customer information
|
||||
// @Description Get a list of all projects with their associated customer information
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.ProjectDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects/with-customers [get]
|
||||
func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) {
|
||||
// Get projects with customers from the database
|
||||
projects, err := models.GetAllProjectsWithCustomers(c.Request.Context())
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
projectDTOs := make([]dto.ProjectDto, len(projects))
|
||||
for i, project := range projects {
|
||||
projectDTOs[i] = convertProjectToDTO(&project)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, projectDTOs)
|
||||
}
|
||||
|
||||
// GetProjectByID handles GET /projects/:id
|
||||
//
|
||||
// @Summary Get project by ID
|
||||
// @Description Get a project by its ID
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Project ID"
|
||||
// @Success 200 {object} utils.Response{data=dto.ProjectDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects/{id} [get]
|
||||
func (h *ProjectHandler) GetProjectByID(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid project ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get project from the database
|
||||
project, err := models.GetProjectByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving 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)
|
||||
}
|
||||
|
||||
// GetProjectsByCustomerID handles GET /projects/customer/:customerId
|
||||
//
|
||||
// @Summary Get projects by customer ID
|
||||
// @Description Get a list of projects for a specific customer
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param customerId path string true "Customer ID"
|
||||
// @Success 200 {object} utils.Response{data=[]dto.ProjectDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects/customer/{customerId} [get]
|
||||
func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
||||
// Parse customer ID from URL
|
||||
customerIDStr := c.Param("customerId")
|
||||
customerID, err := ulid.Parse(customerIDStr)
|
||||
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 := make([]dto.ProjectDto, len(projects))
|
||||
for i, project := range projects {
|
||||
projectDTOs[i] = convertProjectToDTO(&project)
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, projectDTOs)
|
||||
}
|
||||
|
||||
// CreateProject handles POST /projects
|
||||
//
|
||||
// @Summary Create a new project
|
||||
// @Description Create a new project
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param project body dto.ProjectCreateDto true "Project data"
|
||||
// @Success 201 {object} utils.Response{data=dto.ProjectDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects [post]
|
||||
func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
||||
// Parse request body
|
||||
var projectCreateDTO dto.ProjectCreateDto
|
||||
if err := c.ShouldBindJSON(&projectCreateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+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
|
||||
//
|
||||
// @Summary Update a project
|
||||
// @Description Update an existing project
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Project ID"
|
||||
// @Param project body dto.ProjectUpdateDto true "Project data"
|
||||
// @Success 200 {object} utils.Response{data=dto.ProjectDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects/{id} [put]
|
||||
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid project ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var projectUpdateDTO dto.ProjectUpdateDto
|
||||
if err := c.ShouldBindJSON(&projectUpdateDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set ID from URL
|
||||
projectUpdateDTO.ID = id.String()
|
||||
|
||||
// Convert DTO to model
|
||||
projectUpdate, err := convertUpdateProjectDTOToModel(projectUpdateDTO)
|
||||
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
|
||||
//
|
||||
// @Summary Delete a project
|
||||
// @Description Delete a project by its ID
|
||||
// @Tags projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Project ID"
|
||||
// @Success 204 {object} utils.Response
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /projects/{id} [delete]
|
||||
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
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
|
||||
|
||||
func convertProjectToDTO(project *models.Project) dto.ProjectDto {
|
||||
customerID := 0
|
||||
if project.CustomerID.Compare(ulid.ULID{}) != 0 {
|
||||
// This is a simplification, adjust as needed
|
||||
customerID = int(project.CustomerID.Time())
|
||||
}
|
||||
|
||||
return dto.ProjectDto{
|
||||
ID: project.ID.String(),
|
||||
CreatedAt: project.CreatedAt,
|
||||
UpdatedAt: project.UpdatedAt,
|
||||
Name: project.Name,
|
||||
CustomerID: customerID,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCreateProjectDTOToModel(dto dto.ProjectCreateDto) (models.ProjectCreate, error) {
|
||||
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
|
||||
customerID, err := customerIDToULID(dto.CustomerID)
|
||||
if err != nil {
|
||||
return models.ProjectCreate{}, fmt.Errorf("invalid customer ID: %w", err)
|
||||
}
|
||||
|
||||
return models.ProjectCreate{
|
||||
Name: dto.Name,
|
||||
CustomerID: customerID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto) (models.ProjectUpdate, error) {
|
||||
id, _ := ulid.Parse(dto.ID)
|
||||
update := models.ProjectUpdate{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if dto.Name != nil {
|
||||
update.Name = dto.Name
|
||||
}
|
||||
|
||||
if dto.CustomerID != nil {
|
||||
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
|
||||
customerID, err := customerIDToULID(*dto.CustomerID)
|
||||
if err != nil {
|
||||
return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err)
|
||||
}
|
||||
update.CustomerID = &customerID
|
||||
}
|
||||
|
||||
return update, nil
|
||||
}
|
||||
|
||||
// Helper function to convert customer ID from int to ULID
|
||||
func customerIDToULID(id int) (ulid.ULID, error) {
|
||||
// This is a simplification, in a real application you would need to
|
||||
// fetch the actual ULID from the database or use a proper conversion method
|
||||
// For now, we'll create a deterministic ULID based on the int value
|
||||
entropy := ulid.Monotonic(nil, 0)
|
||||
timestamp := uint64(id)
|
||||
|
||||
// Create a new ULID with the timestamp and entropy
|
||||
return ulid.MustNew(timestamp, entropy), nil
|
||||
}
|
504
backend/internal/api/handlers/timeentry_handler.go
Normal file
504
backend/internal/api/handlers/timeentry_handler.go
Normal file
@ -0,0 +1,504 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/middleware"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// TimeEntryHandler handles time entry-related API endpoints
|
||||
type TimeEntryHandler struct{}
|
||||
|
||||
// NewTimeEntryHandler creates a new TimeEntryHandler
|
||||
func NewTimeEntryHandler() *TimeEntryHandler {
|
||||
return &TimeEntryHandler{}
|
||||
}
|
||||
|
||||
// GetTimeEntries handles GET /time-entries
|
||||
//
|
||||
// @Summary Get all time entries
|
||||
// @Description Get a list of all time entries
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries [get]
|
||||
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
||||
// Get time entries from the database
|
||||
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
|
||||
//
|
||||
// @Summary Get time entry by ID
|
||||
// @Description Get a time entry by its ID
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Time Entry ID"
|
||||
// @Success 200 {object} utils.Response{data=dto.TimeEntryDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/{id} [get]
|
||||
func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
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(), 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
|
||||
//
|
||||
// @Summary Get time entries by user ID
|
||||
// @Description Get a list of time entries for a specific user
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param userId path string true "User ID"
|
||||
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/user/{userId} [get]
|
||||
func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
||||
// Parse user ID from URL
|
||||
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(), 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
|
||||
//
|
||||
// @Summary Get current user's time entries
|
||||
// @Description Get a list of time entries for the currently authenticated user
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/me [get]
|
||||
func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
||||
// Get user ID from context (set by AuthMiddleware)
|
||||
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
|
||||
//
|
||||
// @Summary Get time entries by project ID
|
||||
// @Description Get a list of time entries for a specific project
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param projectId path string true "Project ID"
|
||||
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/project/{projectId} [get]
|
||||
func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
||||
// Parse project ID from URL
|
||||
projectIDStr := c.Param("projectId")
|
||||
projectID, err := ulid.Parse(projectIDStr)
|
||||
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
|
||||
//
|
||||
// @Summary Get time entries by date range
|
||||
// @Description Get a list of time entries within a specific date range
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param start query string true "Start date (ISO 8601 format)"
|
||||
// @Param end query string true "End date (ISO 8601 format)"
|
||||
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/range [get]
|
||||
func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
||||
// Parse date range from query parameters
|
||||
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
|
||||
//
|
||||
// @Summary Create a new time entry
|
||||
// @Description Create a new time entry
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param timeEntry body dto.TimeEntryCreateDto true "Time Entry data"
|
||||
// @Success 201 {object} utils.Response{data=dto.TimeEntryDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries [post]
|
||||
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
||||
// Parse request body
|
||||
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
|
||||
//
|
||||
// @Summary Update a time entry
|
||||
// @Description Update an existing time entry
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Time Entry ID"
|
||||
// @Param timeEntry body dto.TimeEntryUpdateDto true "Time Entry data"
|
||||
// @Success 200 {object} utils.Response{data=dto.TimeEntryDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/{id} [put]
|
||||
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(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
|
||||
}
|
||||
|
||||
// Set ID from URL
|
||||
timeEntryUpdateDTO.ID = id.String()
|
||||
|
||||
// Convert DTO to model
|
||||
timeEntryUpdate, err := convertUpdateTimeEntryDTOToModel(timeEntryUpdateDTO)
|
||||
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
|
||||
//
|
||||
// @Summary Delete a time entry
|
||||
// @Description Delete a time entry by its ID
|
||||
// @Tags time-entries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Time Entry ID"
|
||||
// @Success 204 {object} utils.Response
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /time-entries/{id} [delete]
|
||||
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
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(), 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
|
||||
|
||||
func convertTimeEntryToDTO(timeEntry *models.TimeEntry) dto.TimeEntryDto {
|
||||
return dto.TimeEntryDto{
|
||||
ID: timeEntry.ID.String(),
|
||||
CreatedAt: timeEntry.CreatedAt,
|
||||
UpdatedAt: timeEntry.UpdatedAt,
|
||||
UserID: int(timeEntry.UserID.Time()), // Simplified conversion
|
||||
ProjectID: int(timeEntry.ProjectID.Time()), // Simplified conversion
|
||||
ActivityID: int(timeEntry.ActivityID.Time()), // Simplified conversion
|
||||
Start: timeEntry.Start,
|
||||
End: timeEntry.End,
|
||||
Description: timeEntry.Description,
|
||||
Billable: timeEntry.Billable,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCreateTimeEntryDTOToModel(dto dto.TimeEntryCreateDto) (models.TimeEntryCreate, error) {
|
||||
// Convert IDs from int to ULID (this is a simplification, adjust as needed)
|
||||
userID, err := idToULID(dto.UserID)
|
||||
if err != nil {
|
||||
return models.TimeEntryCreate{}, fmt.Errorf("invalid user ID: %w", err)
|
||||
}
|
||||
|
||||
projectID, err := idToULID(dto.ProjectID)
|
||||
if err != nil {
|
||||
return models.TimeEntryCreate{}, fmt.Errorf("invalid project ID: %w", err)
|
||||
}
|
||||
|
||||
activityID, err := idToULID(dto.ActivityID)
|
||||
if err != nil {
|
||||
return models.TimeEntryCreate{}, fmt.Errorf("invalid activity ID: %w", err)
|
||||
}
|
||||
|
||||
return models.TimeEntryCreate{
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
ActivityID: activityID,
|
||||
Start: dto.Start,
|
||||
End: dto.End,
|
||||
Description: dto.Description,
|
||||
Billable: dto.Billable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertUpdateTimeEntryDTOToModel(dto dto.TimeEntryUpdateDto) (models.TimeEntryUpdate, error) {
|
||||
id, _ := ulid.Parse(dto.ID)
|
||||
update := models.TimeEntryUpdate{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if dto.UserID != nil {
|
||||
userID, err := idToULID(*dto.UserID)
|
||||
if err != nil {
|
||||
return models.TimeEntryUpdate{}, fmt.Errorf("invalid user ID: %w", err)
|
||||
}
|
||||
update.UserID = &userID
|
||||
}
|
||||
|
||||
if dto.ProjectID != nil {
|
||||
projectID, err := idToULID(*dto.ProjectID)
|
||||
if err != nil {
|
||||
return models.TimeEntryUpdate{}, fmt.Errorf("invalid project ID: %w", err)
|
||||
}
|
||||
update.ProjectID = &projectID
|
||||
}
|
||||
|
||||
if dto.ActivityID != nil {
|
||||
activityID, err := idToULID(*dto.ActivityID)
|
||||
if err != nil {
|
||||
return models.TimeEntryUpdate{}, fmt.Errorf("invalid activity ID: %w", err)
|
||||
}
|
||||
update.ActivityID = &activityID
|
||||
}
|
||||
|
||||
if dto.Start != nil {
|
||||
update.Start = dto.Start
|
||||
}
|
||||
|
||||
if dto.End != nil {
|
||||
update.End = dto.End
|
||||
}
|
||||
|
||||
if dto.Description != nil {
|
||||
update.Description = dto.Description
|
||||
}
|
||||
|
||||
if dto.Billable != nil {
|
||||
update.Billable = dto.Billable
|
||||
}
|
||||
|
||||
return update, nil
|
||||
}
|
||||
|
||||
// Helper function to convert ID from int to ULID
|
||||
func idToULID(id int) (ulid.ULID, error) {
|
||||
// This is a simplification, in a real application you would need to
|
||||
// fetch the actual ULID from the database or use a proper conversion method
|
||||
// For now, we'll create a deterministic ULID based on the int value
|
||||
entropy := ulid.Monotonic(nil, 0)
|
||||
timestamp := uint64(id)
|
||||
return ulid.MustNew(timestamp, entropy), nil
|
||||
}
|
358
backend/internal/api/handlers/user_handler.go
Normal file
358
backend/internal/api/handlers/user_handler.go
Normal file
@ -0,0 +1,358 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/middleware"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// UserHandler handles user-related API endpoints
|
||||
type UserHandler struct{}
|
||||
|
||||
// NewUserHandler creates a new UserHandler
|
||||
func NewUserHandler() *UserHandler {
|
||||
return &UserHandler{}
|
||||
}
|
||||
|
||||
// GetUsers handles GET /users
|
||||
//
|
||||
// @Summary Get all users
|
||||
// @Description Get a list of all users
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=[]dto.UserDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /users [get]
|
||||
func (h *UserHandler) GetUsers(c *gin.Context) {
|
||||
// Get users from the database
|
||||
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
|
||||
//
|
||||
// @Summary Get user by ID
|
||||
// @Description Get a user by their ID
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} utils.Response{data=dto.UserDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /users/{id} [get]
|
||||
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user from the database
|
||||
user, err := models.GetUserByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving 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)
|
||||
}
|
||||
|
||||
// CreateUser handles POST /users
|
||||
//
|
||||
// @Summary Create a new user
|
||||
// @Description Create a new user
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param user body dto.UserCreateDto true "User data"
|
||||
// @Success 201 {object} utils.Response{data=dto.UserDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /users [post]
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
// Parse request body
|
||||
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
|
||||
//
|
||||
// @Summary Update a user
|
||||
// @Description Update an existing user
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param user body dto.UserUpdateDto true "User data"
|
||||
// @Success 200 {object} utils.Response{data=dto.UserDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /users/{id} [put]
|
||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
idStr := c.Param("id")
|
||||
id, err := ulid.Parse(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
|
||||
}
|
||||
|
||||
// Set ID from URL
|
||||
userUpdateDTO.ID = id.String()
|
||||
|
||||
// Convert DTO to model
|
||||
userUpdate := convertUpdateDTOToModel(userUpdateDTO)
|
||||
|
||||
// Update user in the database
|
||||
user, err := models.UpdateUser(c.Request.Context(), userUpdate)
|
||||
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
|
||||
//
|
||||
// @Summary Delete a user
|
||||
// @Description Delete a user by their ID
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 204 {object} utils.Response
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /users/{id} [delete]
|
||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||
// Parse ID from URL
|
||||
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(), id)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error deleting user: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Login handles POST /auth/login
|
||||
//
|
||||
// @Summary Login
|
||||
// @Description Authenticate a user and get a JWT token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param credentials body dto.LoginDto true "Login credentials"
|
||||
// @Success 200 {object} utils.Response{data=dto.TokenDto}
|
||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /auth/login [post]
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
// Parse request body
|
||||
var loginDTO dto.LoginDto
|
||||
if err := c.ShouldBindJSON(&loginDTO); err != nil {
|
||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
user, err := models.AuthenticateUser(c.Request.Context(), loginDTO.Email, loginDTO.Password)
|
||||
if err != nil {
|
||||
utils.UnauthorizedResponse(c, "Invalid login credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := middleware.GenerateToken(user)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return token
|
||||
tokenDTO := dto.TokenDto{
|
||||
Token: token,
|
||||
User: convertUserToDTO(user),
|
||||
}
|
||||
|
||||
utils.SuccessResponse(c, http.StatusOK, tokenDTO)
|
||||
}
|
||||
|
||||
// GetCurrentUser handles GET /auth/me
|
||||
//
|
||||
// @Summary Get current user
|
||||
// @Description Get the currently authenticated user
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.Response{data=dto.UserDto}
|
||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||
// @Router /auth/me [get]
|
||||
func (h *UserHandler) GetCurrentUser(c *gin.Context) {
|
||||
// Get user ID from context (set by AuthMiddleware)
|
||||
userID, err := middleware.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user from the database
|
||||
user, err := models.GetUserByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
utils.InternalErrorResponse(c, "Error retrieving 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)
|
||||
}
|
||||
|
||||
// Helper functions for DTO conversion
|
||||
|
||||
func convertUserToDTO(user *models.User) dto.UserDto {
|
||||
return dto.UserDto{
|
||||
ID: user.ID.String(),
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
CompanyID: int(user.CompanyID.Time()), // This is a simplification, adjust as needed
|
||||
HourlyRate: user.HourlyRate,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
||||
// Convert CompanyID from int to ULID (this is a simplification, adjust as needed)
|
||||
companyID, _ := ulid.Parse("01H1VECTJQXS1RVWJT6QG3QJCJ")
|
||||
|
||||
return models.UserCreate{
|
||||
Email: dto.Email,
|
||||
Password: dto.Password,
|
||||
Role: dto.Role,
|
||||
CompanyID: companyID,
|
||||
HourlyRate: dto.HourlyRate,
|
||||
}
|
||||
}
|
||||
|
||||
func convertUpdateDTOToModel(dto dto.UserUpdateDto) models.UserUpdate {
|
||||
id, _ := ulid.Parse(dto.ID)
|
||||
update := models.UserUpdate{
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if dto.Email != nil {
|
||||
update.Email = dto.Email
|
||||
}
|
||||
|
||||
if dto.Password != nil {
|
||||
update.Password = dto.Password
|
||||
}
|
||||
|
||||
if dto.Role != nil {
|
||||
update.Role = dto.Role
|
||||
}
|
||||
|
||||
if dto.CompanyID != nil {
|
||||
// Convert CompanyID from int to ULID (this is a simplification, adjust as needed)
|
||||
companyID, _ := ulid.Parse("01H1VECTJQXS1RVWJT6QG3QJCJ")
|
||||
update.CompanyID = &companyID
|
||||
}
|
||||
|
||||
if dto.HourlyRate != nil {
|
||||
update.HourlyRate = dto.HourlyRate
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
198
backend/internal/api/middleware/auth.go
Normal file
198
backend/internal/api/middleware/auth.go
Normal file
@ -0,0 +1,198 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/timetracker/backend/internal/api/utils"
|
||||
"github.com/timetracker/backend/internal/models"
|
||||
)
|
||||
|
||||
// JWT configuration
|
||||
const (
|
||||
// This should be moved to environment variables in production
|
||||
jwtSecret = "your-secret-key-change-in-production"
|
||||
tokenDuration = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Claims represents the JWT claims
|
||||
type Claims struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
CompanyID string `json:"companyId"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// AuthMiddleware checks if the user is authenticated
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get the Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
utils.UnauthorizedResponse(c, "Authorization header is required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the header has the Bearer prefix
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
utils.UnauthorizedResponse(c, "Invalid authorization format, expected 'Bearer TOKEN'")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
claims, err := validateToken(tokenString)
|
||||
if err != nil {
|
||||
utils.UnauthorizedResponse(c, "Invalid or expired token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Store user information in the context
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("email", claims.Email)
|
||||
c.Set("role", claims.Role)
|
||||
c.Set("companyID", claims.CompanyID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RoleMiddleware checks if the user has the required role
|
||||
func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userRole, exists := c.Get("role")
|
||||
if !exists {
|
||||
utils.UnauthorizedResponse(c, "User role not found in context")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user's role is in the allowed roles
|
||||
roleStr, ok := userRole.(string)
|
||||
if !ok {
|
||||
utils.InternalErrorResponse(c, "Invalid role type in context")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
allowed := false
|
||||
for _, role := range roles {
|
||||
if roleStr == role {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
utils.ForbiddenResponse(c, "Insufficient permissions")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateToken creates a new JWT token for a user
|
||||
func GenerateToken(user *models.User) (string, error) {
|
||||
// Create the claims
|
||||
claims := Claims{
|
||||
UserID: user.ID.String(),
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
CompanyID: user.CompanyID.String(),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
// Create the token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Sign the token
|
||||
tokenString, err := token.SignedString([]byte(jwtSecret))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// validateToken validates a JWT token and returns the claims
|
||||
func validateToken(tokenString string) (*Claims, error) {
|
||||
// Parse the token
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the token is valid
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// Get the claims
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid claims")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// GetUserIDFromContext extracts the user ID from the context
|
||||
func GetUserIDFromContext(c *gin.Context) (ulid.ULID, error) {
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
return ulid.ULID{}, errors.New("user ID not found in context")
|
||||
}
|
||||
|
||||
userIDStr, ok := userID.(string)
|
||||
if !ok {
|
||||
return ulid.ULID{}, errors.New("invalid user ID type in context")
|
||||
}
|
||||
|
||||
id, err := ulid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
return ulid.ULID{}, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetCompanyIDFromContext extracts the company ID from the context
|
||||
func GetCompanyIDFromContext(c *gin.Context) (ulid.ULID, error) {
|
||||
companyID, exists := c.Get("companyID")
|
||||
if !exists {
|
||||
return ulid.ULID{}, errors.New("company ID not found in context")
|
||||
}
|
||||
|
||||
companyIDStr, ok := companyID.(string)
|
||||
if !ok {
|
||||
return ulid.ULID{}, errors.New("invalid company ID type in context")
|
||||
}
|
||||
|
||||
id, err := ulid.Parse(companyIDStr)
|
||||
if err != nil {
|
||||
return ulid.ULID{}, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
99
backend/internal/api/routes/router.go
Normal file
99
backend/internal/api/routes/router.go
Normal file
@ -0,0 +1,99 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/timetracker/backend/internal/api/handlers"
|
||||
"github.com/timetracker/backend/internal/api/middleware"
|
||||
)
|
||||
|
||||
// SetupRouter configures all the routes for the API
|
||||
func SetupRouter(r *gin.Engine) {
|
||||
// Create handlers
|
||||
userHandler := handlers.NewUserHandler()
|
||||
activityHandler := handlers.NewActivityHandler()
|
||||
companyHandler := handlers.NewCompanyHandler()
|
||||
customerHandler := handlers.NewCustomerHandler()
|
||||
projectHandler := handlers.NewProjectHandler()
|
||||
timeEntryHandler := handlers.NewTimeEntryHandler()
|
||||
|
||||
// Public routes
|
||||
r.POST("/auth/login", userHandler.Login)
|
||||
|
||||
// API routes (protected)
|
||||
api := r.Group("/api")
|
||||
api.Use(middleware.AuthMiddleware())
|
||||
{
|
||||
// Auth routes
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.GET("/me", userHandler.GetCurrentUser)
|
||||
}
|
||||
|
||||
// User routes
|
||||
users := api.Group("/users")
|
||||
{
|
||||
users.GET("", userHandler.GetUsers)
|
||||
users.GET("/:id", userHandler.GetUserByID)
|
||||
users.POST("", middleware.RoleMiddleware("admin"), userHandler.CreateUser)
|
||||
users.PUT("/:id", middleware.RoleMiddleware("admin"), userHandler.UpdateUser)
|
||||
users.DELETE("/:id", middleware.RoleMiddleware("admin"), userHandler.DeleteUser)
|
||||
}
|
||||
|
||||
// Activity routes
|
||||
activities := api.Group("/activities")
|
||||
{
|
||||
activities.GET("", activityHandler.GetActivities)
|
||||
activities.GET("/:id", activityHandler.GetActivityByID)
|
||||
activities.POST("", middleware.RoleMiddleware("admin"), activityHandler.CreateActivity)
|
||||
activities.PUT("/:id", middleware.RoleMiddleware("admin"), activityHandler.UpdateActivity)
|
||||
activities.DELETE("/:id", middleware.RoleMiddleware("admin"), activityHandler.DeleteActivity)
|
||||
}
|
||||
|
||||
// Company routes
|
||||
companies := api.Group("/companies")
|
||||
{
|
||||
companies.GET("", companyHandler.GetCompanies)
|
||||
companies.GET("/:id", companyHandler.GetCompanyByID)
|
||||
companies.POST("", middleware.RoleMiddleware("admin"), companyHandler.CreateCompany)
|
||||
companies.PUT("/:id", middleware.RoleMiddleware("admin"), companyHandler.UpdateCompany)
|
||||
companies.DELETE("/:id", middleware.RoleMiddleware("admin"), companyHandler.DeleteCompany)
|
||||
}
|
||||
|
||||
// Customer routes
|
||||
customers := api.Group("/customers")
|
||||
{
|
||||
customers.GET("", customerHandler.GetCustomers)
|
||||
customers.GET("/:id", customerHandler.GetCustomerByID)
|
||||
customers.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID)
|
||||
customers.POST("", middleware.RoleMiddleware("admin"), customerHandler.CreateCustomer)
|
||||
customers.PUT("/:id", middleware.RoleMiddleware("admin"), customerHandler.UpdateCustomer)
|
||||
customers.DELETE("/:id", middleware.RoleMiddleware("admin"), customerHandler.DeleteCustomer)
|
||||
}
|
||||
|
||||
// Project routes
|
||||
projects := api.Group("/projects")
|
||||
{
|
||||
projects.GET("", projectHandler.GetProjects)
|
||||
projects.GET("/with-customers", projectHandler.GetProjectsWithCustomers)
|
||||
projects.GET("/:id", projectHandler.GetProjectByID)
|
||||
projects.GET("/customer/:customerId", projectHandler.GetProjectsByCustomerID)
|
||||
projects.POST("", middleware.RoleMiddleware("admin"), projectHandler.CreateProject)
|
||||
projects.PUT("/:id", middleware.RoleMiddleware("admin"), projectHandler.UpdateProject)
|
||||
projects.DELETE("/:id", middleware.RoleMiddleware("admin"), projectHandler.DeleteProject)
|
||||
}
|
||||
|
||||
// Time Entry routes
|
||||
timeEntries := api.Group("/time-entries")
|
||||
{
|
||||
timeEntries.GET("", timeEntryHandler.GetTimeEntries)
|
||||
timeEntries.GET("/me", timeEntryHandler.GetMyTimeEntries)
|
||||
timeEntries.GET("/range", timeEntryHandler.GetTimeEntriesByDateRange)
|
||||
timeEntries.GET("/:id", timeEntryHandler.GetTimeEntryByID)
|
||||
timeEntries.GET("/user/:userId", timeEntryHandler.GetTimeEntriesByUserID)
|
||||
timeEntries.GET("/project/:projectId", timeEntryHandler.GetTimeEntriesByProjectID)
|
||||
timeEntries.POST("", timeEntryHandler.CreateTimeEntry)
|
||||
timeEntries.PUT("/:id", timeEntryHandler.UpdateTimeEntry)
|
||||
timeEntries.DELETE("/:id", timeEntryHandler.DeleteTimeEntry)
|
||||
}
|
||||
}
|
||||
}
|
85
backend/internal/api/utils/response.go
Normal file
85
backend/internal/api/utils/response.go
Normal file
@ -0,0 +1,85 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response is a standardized API response structure
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error *ErrorInfo `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorInfo contains detailed error information
|
||||
type ErrorInfo struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ErrorResponse codes
|
||||
const (
|
||||
ErrorCodeValidation = "VALIDATION_ERROR"
|
||||
ErrorCodeNotFound = "NOT_FOUND"
|
||||
ErrorCodeUnauthorized = "UNAUTHORIZED"
|
||||
ErrorCodeForbidden = "FORBIDDEN"
|
||||
ErrorCodeInternal = "INTERNAL_ERROR"
|
||||
ErrorCodeBadRequest = "BAD_REQUEST"
|
||||
ErrorCodeConflict = "CONFLICT"
|
||||
)
|
||||
|
||||
// SuccessResponse sends a successful response with data
|
||||
func SuccessResponse(c *gin.Context, statusCode int, data interface{}) {
|
||||
c.JSON(statusCode, Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorResponse sends an error response
|
||||
func ErrorResponse(c *gin.Context, statusCode int, errorCode string, message string) {
|
||||
c.JSON(statusCode, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: errorCode,
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequestResponse sends a 400 Bad Request response
|
||||
func BadRequestResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusBadRequest, ErrorCodeBadRequest, message)
|
||||
}
|
||||
|
||||
// ValidationErrorResponse sends a 400 Bad Request response for validation errors
|
||||
func ValidationErrorResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusBadRequest, ErrorCodeValidation, message)
|
||||
}
|
||||
|
||||
// NotFoundResponse sends a 404 Not Found response
|
||||
func NotFoundResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusNotFound, ErrorCodeNotFound, message)
|
||||
}
|
||||
|
||||
// UnauthorizedResponse sends a 401 Unauthorized response
|
||||
func UnauthorizedResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusUnauthorized, ErrorCodeUnauthorized, message)
|
||||
}
|
||||
|
||||
// ForbiddenResponse sends a 403 Forbidden response
|
||||
func ForbiddenResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusForbidden, ErrorCodeForbidden, message)
|
||||
}
|
||||
|
||||
// InternalErrorResponse sends a 500 Internal Server Error response
|
||||
func InternalErrorResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusInternalServerError, ErrorCodeInternal, message)
|
||||
}
|
||||
|
||||
// ConflictResponse sends a 409 Conflict response
|
||||
func ConflictResponse(c *gin.Context, message string) {
|
||||
ErrorResponse(c, http.StatusConflict, ErrorCodeConflict, message)
|
||||
}
|
19
backend/internal/api/utils/swagger.go
Normal file
19
backend/internal/api/utils/swagger.go
Normal file
@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
dto "github.com/timetracker/backend/internal/dtos"
|
||||
)
|
||||
|
||||
// This file contains type definitions for Swagger documentation
|
||||
|
||||
// LoginRequest is a Swagger representation of LoginDto
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" example:"user@example.com"`
|
||||
Password string `json:"password" example:"SecurePassword123!"`
|
||||
}
|
||||
|
||||
// TokenResponse is a Swagger representation of TokenDto
|
||||
type TokenResponse struct {
|
||||
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
|
||||
User dto.UserDto `json:"user"`
|
||||
}
|
@ -2,15 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type ActivityDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID string `json:"lastEditorID"`
|
||||
Name string `json:"name"`
|
||||
BillingRate float64 `json:"billingRate"`
|
||||
}
|
||||
@ -21,10 +19,10 @@ type ActivityCreateDto struct {
|
||||
}
|
||||
|
||||
type ActivityUpdateDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID *string `json:"lastEditorID"`
|
||||
Name *string `json:"name"`
|
||||
BillingRate *float64 `json:"billingRate"`
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
package dto
|
||||
|
||||
type AuthDto struct {
|
||||
// LoginDto represents the login request
|
||||
type LoginDto struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// TokenDto represents the response after successful authentication
|
||||
type TokenDto struct {
|
||||
Token string `json:"token"`
|
||||
User UserDto `json:"user"`
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type CompanyDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID string `json:"lastEditorID"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
@ -19,9 +17,9 @@ type CompanyCreateDto struct {
|
||||
}
|
||||
|
||||
type CompanyUpdateDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID *string `json:"lastEditorID"`
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type CustomerDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID string `json:"lastEditorID"`
|
||||
Name string `json:"name"`
|
||||
CompanyID int `json:"companyId"`
|
||||
}
|
||||
@ -21,10 +19,10 @@ type CustomerCreateDto struct {
|
||||
}
|
||||
|
||||
type CustomerUpdateDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID *string `json:"lastEditorID"`
|
||||
Name *string `json:"name"`
|
||||
CompanyID *int `json:"companyId"`
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type ProjectDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID string `json:"lastEditorID"`
|
||||
Name string `json:"name"`
|
||||
CustomerID int `json:"customerId"`
|
||||
}
|
||||
@ -21,10 +19,10 @@ type ProjectCreateDto struct {
|
||||
}
|
||||
|
||||
type ProjectUpdateDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID *string `json:"lastEditorID"`
|
||||
Name *string `json:"name"`
|
||||
CustomerID *int `json:"customerId"`
|
||||
}
|
||||
|
@ -2,15 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type TimeEntryDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID string `json:"lastEditorID"`
|
||||
UserID int `json:"userId"`
|
||||
ProjectID int `json:"projectId"`
|
||||
ActivityID int `json:"activityId"`
|
||||
@ -31,10 +29,10 @@ type TimeEntryCreateDto struct {
|
||||
}
|
||||
|
||||
type TimeEntryUpdateDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID *string `json:"lastEditorID"`
|
||||
UserID *int `json:"userId"`
|
||||
ProjectID *int `json:"projectId"`
|
||||
ActivityID *int `json:"activityId"`
|
||||
|
@ -2,15 +2,13 @@ package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type UserDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID string `json:"lastEditorID"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
CompanyID int `json:"companyId"`
|
||||
@ -26,10 +24,10 @@ type UserCreateDto struct {
|
||||
}
|
||||
|
||||
type UserUpdateDto struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||
LastEditorID *string `json:"lastEditorID"`
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
Role *string `json:"role"`
|
||||
|
Loading…
x
Reference in New Issue
Block a user