Compare commits
7 Commits
1198b326c1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b47da3673 | |||
| a9c7598862 | |||
| bcc3aadb85 | |||
| fcdeedf7e9 | |||
| 21c9233058 | |||
| b9c900578d | |||
| 294047a2b0 |
@@ -37,6 +37,7 @@ SOLVE TASKS AS FAST AS POSSIBLE. EACH REQUEST COSTS THE USER MONEY.
|
|||||||
- Unexpected behavior is encountered
|
- Unexpected behavior is encountered
|
||||||
- Specific conditions require warnings
|
- Specific conditions require warnings
|
||||||
- New patterns emerge that need documentation
|
- New patterns emerge that need documentation
|
||||||
|
- DO NOT FIX UNUSED IMPORTS - this is the job of the linter
|
||||||
10.Implement a REST API update handling in Go using Gin that ensures the following behavior:
|
10.Implement a REST API update handling in Go using Gin that ensures the following behavior:
|
||||||
- The update request is received as JSON.
|
- The update request is received as JSON.
|
||||||
- If a field is present in the JSON and set to null, the corresponding value in the database should be removed.
|
- If a field is present in the JSON and set to null, the corresponding value in the database should be removed.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Gitea Actions Demo
|
||||||
|
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Explore-Gitea-Actions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||||
|
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
||||||
|
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||||
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
|
- name: List files in the repository
|
||||||
|
run: |
|
||||||
|
ls ${{ gitea.workspace }}
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/config"
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,15 +17,15 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Get database configuration with sensible defaults
|
// Get database configuration with sensible defaults
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
dbConfig := config.DefaultDatabaseConfig()
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
fmt.Println("Connecting to database...")
|
fmt.Println("Connecting to database...")
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
if err := db.InitDB(dbConfig); err != nil {
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
log.Fatalf("Error initializing database: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := models.CloseDB(); err != nil {
|
if err := db.CloseDB(); err != nil {
|
||||||
log.Printf("Error closing database connection: %v", err)
|
log.Printf("Error closing database connection: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -34,7 +36,7 @@ func main() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Get the database engine
|
// Get the database engine
|
||||||
db := models.GetEngine(ctx)
|
db := db.GetEngine(ctx)
|
||||||
|
|
||||||
// Test database connection with a simple query
|
// Test database connection with a simple query
|
||||||
var result int
|
var result int
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/config"
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
@@ -29,7 +31,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get database configuration with sensible defaults
|
// Get database configuration with sensible defaults
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
dbConfig := config.DefaultDatabaseConfig()
|
||||||
|
|
||||||
// Override with environment variables if provided
|
// Override with environment variables if provided
|
||||||
if host := os.Getenv("DB_HOST"); host != "" {
|
if host := os.Getenv("DB_HOST"); host != "" {
|
||||||
@@ -62,7 +64,7 @@ func main() {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
gormDB, err := models.GetGormDB(dbConfig, "postgres")
|
gormDB, err := db.GetGormDB(dbConfig, "postgres")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error getting gorm DB: %v", err)
|
log.Fatalf("Error getting gorm DB: %v", err)
|
||||||
}
|
}
|
||||||
@@ -89,11 +91,11 @@ func main() {
|
|||||||
fmt.Printf("✓ Database %s created successfully\n", dbConfig.DBName)
|
fmt.Printf("✓ Database %s created successfully\n", dbConfig.DBName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = models.InitDB(dbConfig); err != nil {
|
if err = db.InitDB(dbConfig); err != nil {
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
log.Fatalf("Error initializing database: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := models.CloseDB(); err != nil {
|
if err := db.CloseDB(); err != nil {
|
||||||
log.Printf("Error closing database connection: %v", err)
|
log.Printf("Error closing database connection: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -7,21 +7,23 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/timetracker/backend/internal/config"
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Get database configuration with sensible defaults
|
// Get database configuration with sensible defaults
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
dbConfig := config.DefaultDatabaseConfig()
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
fmt.Println("Connecting to database...")
|
fmt.Println("Connecting to database...")
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
if err := db.InitDB(dbConfig); err != nil {
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
log.Fatalf("Error initializing database: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := models.CloseDB(); err != nil {
|
if err := db.CloseDB(); err != nil {
|
||||||
log.Printf("Error closing database connection: %v", err)
|
log.Printf("Error closing database connection: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/timetracker/backend/internal/config"
|
"github.com/timetracker/backend/internal/config"
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -23,11 +24,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
if err := models.InitDB(cfg.Database); err != nil {
|
if err := db.InitDB(cfg.Database); err != nil {
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
log.Fatalf("Error initializing database: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := models.CloseDB(); err != nil {
|
if err := db.CloseDB(); err != nil {
|
||||||
log.Printf("Error closing database connection: %v", err)
|
log.Printf("Error closing database connection: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -44,7 +45,7 @@ func main() {
|
|||||||
func seedDatabase(ctx context.Context) error {
|
func seedDatabase(ctx context.Context) error {
|
||||||
// Check if seeding is needed
|
// Check if seeding is needed
|
||||||
var count int64
|
var count int64
|
||||||
if err := models.GetEngine(ctx).Model(&models.Company{}).Count(&count).Error; err != nil {
|
if err := db.GetEngine(ctx).Model(&models.Company{}).Count(&count).Error; err != nil {
|
||||||
return fmt.Errorf("error checking if seeding is needed: %w", err)
|
return fmt.Errorf("error checking if seeding is needed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ func seedDatabase(ctx context.Context) error {
|
|||||||
log.Println("Seeding database with initial data...")
|
log.Println("Seeding database with initial data...")
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
return models.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
return db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Create default company
|
// Create default company
|
||||||
defaultCompany := models.Company{
|
defaultCompany := models.Company{
|
||||||
Name: "Default Company",
|
Name: "Default Company",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type ProjectDto struct {
|
|||||||
UpdatedAt time.Time `json:"updatedAt" example:"2024-01-01T00:00:00Z"`
|
UpdatedAt time.Time `json:"updatedAt" example:"2024-01-01T00:00:00Z"`
|
||||||
LastEditorID string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
|
LastEditorID string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
|
||||||
Name string `json:"name" example:"Time Tracking App"`
|
Name string `json:"name" example:"Time Tracking App"`
|
||||||
CustomerID *string `json:"customerId" example:"01HGW2BBG0000000000000000"`
|
CustomerID *string `json:"customerId,omitempty" example:"01HGW2BBG0000000000000000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectCreateDto struct {
|
type ProjectCreateDto struct {
|
||||||
@@ -13,7 +13,7 @@ type UserDto struct {
|
|||||||
LastEditorID string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
|
LastEditorID string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
|
||||||
Email string `json:"email" example:"test@example.com"`
|
Email string `json:"email" example:"test@example.com"`
|
||||||
Role string `json:"role" example:"admin"`
|
Role string `json:"role" example:"admin"`
|
||||||
CompanyID *string `json:"companyId" example:"01HGW2BBG0000000000000000"`
|
CompanyID *string `json:"companyId,omitempty" example:"01HGW2BBG0000000000000000"`
|
||||||
HourlyRate float64 `json:"hourlyRate" example:"50.00"`
|
HourlyRate float64 `json:"hourlyRate" example:"50.00"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@@ -32,20 +33,7 @@ func NewActivityHandler() *ActivityHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities [get]
|
// @Router /activities [get]
|
||||||
func (h *ActivityHandler) GetActivities(c *gin.Context) {
|
func (h *ActivityHandler) GetActivities(c *gin.Context) {
|
||||||
// Get activities from the database
|
utils.HandleGetAll(c, models.GetAllActivities, convertActivityToDTO, "activities")
|
||||||
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
|
// GetActivityByID handles GET /activities/:id
|
||||||
@@ -64,30 +52,7 @@ func (h *ActivityHandler) GetActivities(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities/{id} [get]
|
// @Router /activities/{id} [get]
|
||||||
func (h *ActivityHandler) GetActivityByID(c *gin.Context) {
|
func (h *ActivityHandler) GetActivityByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleGetByID(c, models.GetActivityByID, convertActivityToDTO, "activity")
|
||||||
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(), types.FromULID(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
|
// CreateActivity handles POST /activities
|
||||||
@@ -105,27 +70,7 @@ func (h *ActivityHandler) GetActivityByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities [post]
|
// @Router /activities [post]
|
||||||
func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createActivityWrapper, convertActivityToDTO, "activity")
|
||||||
var activityCreateDTO dto.ActivityCreateDto
|
|
||||||
if err := 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
|
// UpdateActivity handles PUT /activities/:id
|
||||||
@@ -145,43 +90,7 @@ func (h *ActivityHandler) CreateActivity(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities/{id} [put]
|
// @Router /activities/{id} [put]
|
||||||
func (h *ActivityHandler) UpdateActivity(c *gin.Context) {
|
func (h *ActivityHandler) UpdateActivity(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateActivity, convertActivityToDTO, prepareActivityUpdate, "activity")
|
||||||
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
|
// DeleteActivity handles DELETE /activities/:id
|
||||||
@@ -199,22 +108,7 @@ func (h *ActivityHandler) UpdateActivity(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities/{id} [delete]
|
// @Router /activities/{id} [delete]
|
||||||
func (h *ActivityHandler) DeleteActivity(c *gin.Context) {
|
func (h *ActivityHandler) DeleteActivity(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteActivity, "activity")
|
||||||
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(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting activity: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@@ -252,3 +146,35 @@ func convertUpdateActivityDTOToModel(dto dto.ActivityUpdateDto) models.ActivityU
|
|||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareActivityUpdate prepares the activity update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareActivityUpdate(c *gin.Context) (models.ActivityUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid activity ID format")
|
||||||
|
return models.ActivityUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var activityUpdateDTO dto.ActivityUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &activityUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.ActivityUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ID from URL
|
||||||
|
activityUpdateDTO.ID = id.String()
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
return convertUpdateActivityDTOToModel(activityUpdateDTO), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createActivityWrapper is a wrapper function for models.CreateActivity that takes a DTO as input
|
||||||
|
func createActivityWrapper(ctx context.Context, createDTO dto.ActivityCreateDto) (*models.Activity, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
activityCreate := convertCreateActivityDTOToModel(createDTO)
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateActivity(ctx, activityCreate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@@ -32,20 +32,7 @@ func NewCompanyHandler() *CompanyHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies [get]
|
// @Router /companies [get]
|
||||||
func (h *CompanyHandler) GetCompanies(c *gin.Context) {
|
func (h *CompanyHandler) GetCompanies(c *gin.Context) {
|
||||||
// Get companies from the database
|
utils.HandleGetAll(c, models.GetAllCompanies, convertCompanyToDTO, "companies")
|
||||||
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
|
// GetCompanyByID handles GET /companies/:id
|
||||||
@@ -64,30 +51,7 @@ func (h *CompanyHandler) GetCompanies(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies/{id} [get]
|
// @Router /companies/{id} [get]
|
||||||
func (h *CompanyHandler) GetCompanyByID(c *gin.Context) {
|
func (h *CompanyHandler) GetCompanyByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleGetByID(c, models.GetCompanyByID, convertCompanyToDTO, "company")
|
||||||
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(), types.FromULID(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
|
// CreateCompany handles POST /companies
|
||||||
@@ -105,27 +69,7 @@ func (h *CompanyHandler) GetCompanyByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies [post]
|
// @Router /companies [post]
|
||||||
func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createCompanyWrapper, convertCompanyToDTO, "company")
|
||||||
var companyCreateDTO dto.CompanyCreateDto
|
|
||||||
if err := 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
|
// UpdateCompany handles PUT /companies/:id
|
||||||
@@ -145,40 +89,7 @@ func (h *CompanyHandler) CreateCompany(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies/{id} [put]
|
// @Router /companies/{id} [put]
|
||||||
func (h *CompanyHandler) UpdateCompany(c *gin.Context) {
|
func (h *CompanyHandler) UpdateCompany(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateCompany, convertCompanyToDTO, prepareCompanyUpdate, "company")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := types.ULIDFromString(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
companyUpdate := convertUpdateCompanyDTOToModel(companyUpdateDTO, id)
|
|
||||||
|
|
||||||
// Update company in the database
|
|
||||||
company, err := models.UpdateCompany(c.Request.Context(), companyUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating company: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if company == nil {
|
|
||||||
utils.NotFoundResponse(c, "Company not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
companyDTO := convertCompanyToDTO(company)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, companyDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCompany handles DELETE /companies/:id
|
// DeleteCompany handles DELETE /companies/:id
|
||||||
@@ -196,22 +107,7 @@ func (h *CompanyHandler) UpdateCompany(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /companies/{id} [delete]
|
// @Router /companies/{id} [delete]
|
||||||
func (h *CompanyHandler) DeleteCompany(c *gin.Context) {
|
func (h *CompanyHandler) DeleteCompany(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteCompany, "company")
|
||||||
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(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting company: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@@ -242,3 +138,32 @@ func convertUpdateCompanyDTOToModel(dto dto.CompanyUpdateDto, id types.ULID) mod
|
|||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareCompanyUpdate prepares the company update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareCompanyUpdate(c *gin.Context) (models.CompanyUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid company ID format")
|
||||||
|
return models.CompanyUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var companyUpdateDTO dto.CompanyUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &companyUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.CompanyUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
return convertUpdateCompanyDTOToModel(companyUpdateDTO, id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCompanyWrapper is a wrapper function for models.CreateCompany that takes a DTO as input
|
||||||
|
func createCompanyWrapper(ctx context.Context, createDTO dto.CompanyCreateDto) (*models.Company, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
companyCreate := convertCreateCompanyDTOToModel(createDTO)
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateCompany(ctx, companyCreate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
"github.com/timetracker/backend/internal/api/middleware"
|
"github.com/timetracker/backend/internal/api/middleware"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@@ -34,20 +35,7 @@ func NewCustomerHandler() *CustomerHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers [get]
|
// @Router /customers [get]
|
||||||
func (h *CustomerHandler) GetCustomers(c *gin.Context) {
|
func (h *CustomerHandler) GetCustomers(c *gin.Context) {
|
||||||
// Get customers from the database
|
utils.HandleGetAll(c, models.GetAllCustomers, convertCustomerToDTO, "customers")
|
||||||
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
|
// GetCustomerByID handles GET /customers/:id
|
||||||
@@ -66,30 +54,7 @@ func (h *CustomerHandler) GetCustomers(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers/{id} [get]
|
// @Router /customers/{id} [get]
|
||||||
func (h *CustomerHandler) GetCustomerByID(c *gin.Context) {
|
func (h *CustomerHandler) GetCustomerByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleGetByID(c, models.GetCustomerByID, convertCustomerToDTO, "customer")
|
||||||
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(), types.FromULID(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
|
// GetCustomersByCompanyID handles GET /customers/company/:companyId
|
||||||
@@ -111,24 +76,26 @@ func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) {
|
|||||||
companyIDStr := c.Param("companyId")
|
companyIDStr := c.Param("companyId")
|
||||||
companyID, err := parseCompanyID(companyIDStr)
|
companyID, err := parseCompanyID(companyIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
responses.BadRequestResponse(c, "Invalid company ID format")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get customers from the database
|
// Create a wrapper function that takes a ULID but calls the original function with an int
|
||||||
customers, err := models.GetCustomersByCompanyID(c.Request.Context(), companyID)
|
getByCompanyIDFn := func(ctx context.Context, _ types.ULID) ([]models.Customer, error) {
|
||||||
|
return models.GetCustomersByCompanyID(ctx, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get customers from the database and convert to DTOs
|
||||||
|
customers, err := getByCompanyIDFn(c.Request.Context(), types.ULID{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error retrieving customers: "+err.Error())
|
responses.InternalErrorResponse(c, "Error retrieving customers: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTOs
|
// Convert to DTOs
|
||||||
customerDTOs := make([]dto.CustomerDto, len(customers))
|
customerDTOs := utils.ConvertToDTO(customers, convertCustomerToDTO)
|
||||||
for i, customer := range customers {
|
|
||||||
customerDTOs[i] = convertCustomerToDTO(&customer)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, customerDTOs)
|
responses.SuccessResponse(c, http.StatusOK, customerDTOs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCustomer handles POST /customers
|
// CreateCustomer handles POST /customers
|
||||||
@@ -146,37 +113,19 @@ func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers [post]
|
// @Router /customers [post]
|
||||||
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
||||||
|
// We need to use a custom wrapper for CreateCustomer because we need to get the user ID from the context
|
||||||
userID, err := middleware.GetUserIDFromContext(c)
|
userID, err := middleware.GetUserIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
responses.UnauthorizedResponse(c, "User not authenticated")
|
||||||
return
|
|
||||||
}
|
|
||||||
// Parse request body
|
|
||||||
var customerCreateDTO dto.CustomerCreateDto
|
|
||||||
if err := c.ShouldBindJSON(&customerCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert DTO to model
|
// Use a closure to capture the userID
|
||||||
customerCreate, err := convertCreateCustomerDTOToModel(customerCreateDTO)
|
createFn := func(ctx context.Context, createDTO dto.CustomerCreateDto) (*models.Customer, error) {
|
||||||
if err != nil {
|
return createCustomerWrapper(ctx, createDTO, userID)
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
customerCreate.OwnerUserID = &userID
|
|
||||||
|
|
||||||
// Create customer in the database
|
|
||||||
customer, err := models.CreateCustomer(c.Request.Context(), customerCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating customer: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
utils.HandleCreate(c, createFn, convertCustomerToDTO, "customer")
|
||||||
customerDTO := convertCustomerToDTO(customer)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, customerDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCustomer handles PUT /customers/:id
|
// UpdateCustomer handles PUT /customers/:id
|
||||||
@@ -196,47 +145,7 @@ func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers/{id} [put]
|
// @Router /customers/{id} [put]
|
||||||
func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
|
func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateCustomer, convertCustomerToDTO, prepareCustomerUpdate, "customer")
|
||||||
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, err := convertUpdateCustomerDTOToModel(customerUpdateDTO)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update customer in the database
|
|
||||||
customer, err := models.UpdateCustomer(c.Request.Context(), customerUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating customer: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if customer == nil {
|
|
||||||
utils.NotFoundResponse(c, "Customer not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
customerDTO := convertCustomerToDTO(customer)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, customerDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCustomer handles DELETE /customers/:id
|
// DeleteCustomer handles DELETE /customers/:id
|
||||||
@@ -254,22 +163,7 @@ func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /customers/{id} [delete]
|
// @Router /customers/{id} [delete]
|
||||||
func (h *CustomerHandler) DeleteCustomer(c *gin.Context) {
|
func (h *CustomerHandler) DeleteCustomer(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteCustomer, "customer")
|
||||||
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(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting customer: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@@ -338,3 +232,47 @@ func parseCompanyID(idStr string) (int, error) {
|
|||||||
_, err := fmt.Sscanf(idStr, "%d", &id)
|
_, err := fmt.Sscanf(idStr, "%d", &id)
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareCustomerUpdate prepares the customer update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareCustomerUpdate(c *gin.Context) (models.CustomerUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid customer ID format")
|
||||||
|
return models.CustomerUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var customerUpdateDTO dto.CustomerUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &customerUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.CustomerUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ID from URL
|
||||||
|
customerUpdateDTO.ID = id.String()
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
customerUpdate, err := convertUpdateCustomerDTOToModel(customerUpdateDTO)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||||
|
return models.CustomerUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return customerUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCustomerWrapper is a wrapper function for models.CreateCustomer that takes a DTO as input
|
||||||
|
func createCustomerWrapper(ctx context.Context, createDTO dto.CustomerCreateDto, userID types.ULID) (*models.Customer, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
customerCreate, err := convertCreateCustomerDTOToModel(createDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the owner user ID
|
||||||
|
customerCreate.OwnerUserID = &userID
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateCustomer(ctx, customerCreate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@@ -33,20 +33,7 @@ func NewProjectHandler() *ProjectHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects [get]
|
// @Router /projects [get]
|
||||||
func (h *ProjectHandler) GetProjects(c *gin.Context) {
|
func (h *ProjectHandler) GetProjects(c *gin.Context) {
|
||||||
// Get projects from the database
|
utils.HandleGetAll(c, models.GetAllProjects, convertProjectToDTO, "projects")
|
||||||
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
|
// GetProjectsWithCustomers handles GET /projects/with-customers
|
||||||
@@ -62,20 +49,7 @@ func (h *ProjectHandler) GetProjects(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/with-customers [get]
|
// @Router /projects/with-customers [get]
|
||||||
func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) {
|
func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) {
|
||||||
// Get projects with customers from the database
|
utils.HandleGetAll(c, models.GetAllProjectsWithCustomers, convertProjectToDTO, "projects with customers")
|
||||||
projects, err := models.GetAllProjectsWithCustomers(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
projectDTOs := 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
|
// GetProjectByID handles GET /projects/:id
|
||||||
@@ -94,30 +68,7 @@ func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/{id} [get]
|
// @Router /projects/{id} [get]
|
||||||
func (h *ProjectHandler) GetProjectByID(c *gin.Context) {
|
func (h *ProjectHandler) GetProjectByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleGetByID(c, models.GetProjectByID, convertProjectToDTO, "project")
|
||||||
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(), types.FromULID(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
|
// GetProjectsByCustomerID handles GET /projects/customer/:customerId
|
||||||
@@ -135,28 +86,7 @@ func (h *ProjectHandler) GetProjectByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/customer/{customerId} [get]
|
// @Router /projects/customer/{customerId} [get]
|
||||||
func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
||||||
// Parse customer ID from URL
|
utils.HandleGetByFilter(c, models.GetProjectsByCustomerID, convertProjectToDTO, "projects", "customerId")
|
||||||
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
|
// CreateProject handles POST /projects
|
||||||
@@ -174,31 +104,7 @@ func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects [post]
|
// @Router /projects [post]
|
||||||
func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createProjectWrapper, convertProjectToDTO, "project")
|
||||||
var projectCreateDTO dto.ProjectCreateDto
|
|
||||||
if err := 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
|
// UpdateProject handles PUT /projects/:id
|
||||||
@@ -218,44 +124,7 @@ func (h *ProjectHandler) CreateProject(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/{id} [put]
|
// @Router /projects/{id} [put]
|
||||||
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateProject, convertProjectToDTO, prepareProjectUpdate, "project")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := types.ULIDFromString(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
projectUpdate, err := convertUpdateProjectDTOToModel(projectUpdateDTO, id)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update project in the database
|
|
||||||
project, err := models.UpdateProject(c.Request.Context(), projectUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating project: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if project == nil {
|
|
||||||
utils.NotFoundResponse(c, "Project not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
projectDTO := convertProjectToDTO(project)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, projectDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProject handles DELETE /projects/:id
|
// DeleteProject handles DELETE /projects/:id
|
||||||
@@ -273,34 +142,23 @@ func (h *ProjectHandler) UpdateProject(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /projects/{id} [delete]
|
// @Router /projects/{id} [delete]
|
||||||
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
|
func (h *ProjectHandler) DeleteProject(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteProject, "project")
|
||||||
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
|
// Helper functions for DTO conversion
|
||||||
|
|
||||||
func convertProjectToDTO(project *models.Project) dto.ProjectDto {
|
func convertProjectToDTO(project *models.Project) dto.ProjectDto {
|
||||||
customerId := project.CustomerID.String()
|
var customerIdPtr *string
|
||||||
|
if project.CustomerID != nil {
|
||||||
|
customerIdStr := project.CustomerID.String()
|
||||||
|
customerIdPtr = &customerIdStr
|
||||||
|
}
|
||||||
return dto.ProjectDto{
|
return dto.ProjectDto{
|
||||||
ID: project.ID.String(),
|
ID: project.ID.String(),
|
||||||
CreatedAt: project.CreatedAt,
|
CreatedAt: project.CreatedAt,
|
||||||
UpdatedAt: project.UpdatedAt,
|
UpdatedAt: project.UpdatedAt,
|
||||||
Name: project.Name,
|
Name: project.Name,
|
||||||
CustomerID: &customerId,
|
CustomerID: customerIdPtr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +166,6 @@ func convertCreateProjectDTOToModel(dto dto.ProjectCreateDto) (models.ProjectCre
|
|||||||
create := models.ProjectCreate{Name: dto.Name}
|
create := models.ProjectCreate{Name: dto.Name}
|
||||||
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
|
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
|
||||||
if dto.CustomerID != nil {
|
if dto.CustomerID != nil {
|
||||||
|
|
||||||
customerID, err := types.ULIDFromString(*dto.CustomerID)
|
customerID, err := types.ULIDFromString(*dto.CustomerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.ProjectCreate{}, fmt.Errorf("invalid customer ID: %w", err)
|
return models.ProjectCreate{}, fmt.Errorf("invalid customer ID: %w", err)
|
||||||
@@ -329,17 +186,53 @@ func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto, id types.ULID) (mo
|
|||||||
|
|
||||||
if dto.CustomerID.Valid {
|
if dto.CustomerID.Valid {
|
||||||
if dto.CustomerID.Value == nil {
|
if dto.CustomerID.Value == nil {
|
||||||
update.CustomerID = nil
|
update.CustomerID = types.Null[types.ULID]()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
|
|
||||||
customerID, err := types.ULIDFromString(*dto.CustomerID.Value)
|
customerID, err := types.ULIDFromString(*dto.CustomerID.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err)
|
return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err)
|
||||||
}
|
}
|
||||||
update.CustomerID = &customerID
|
update.CustomerID = types.NewNullable(customerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareProjectUpdate prepares the project update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareProjectUpdate(c *gin.Context) (models.ProjectUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := utils.ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid project ID format")
|
||||||
|
return models.ProjectUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var projectUpdateDTO dto.ProjectUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &projectUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.ProjectUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
projectUpdate, err := convertUpdateProjectDTOToModel(projectUpdateDTO, id)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.ProjectUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createProjectWrapper is a wrapper function for models.CreateProject that takes a DTO as input
|
||||||
|
func createProjectWrapper(ctx context.Context, createDTO dto.ProjectCreateDto) (*models.Project, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
projectCreate, err := convertCreateProjectDTOToModel(createDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateProject(ctx, projectCreate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
"github.com/timetracker/backend/internal/api/middleware"
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@@ -35,20 +33,7 @@ func NewTimeEntryHandler() *TimeEntryHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries [get]
|
// @Router /time-entries [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
||||||
// Get time entries from the database
|
utils.HandleGetAll(c, models.GetAllTimeEntries, convertTimeEntryToDTO, "time entries")
|
||||||
timeEntries, err := models.GetAllTimeEntries(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntryByID handles GET /time-entries/:id
|
// GetTimeEntryByID handles GET /time-entries/:id
|
||||||
@@ -67,30 +52,7 @@ func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/{id} [get]
|
// @Router /time-entries/{id} [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleGetByID(c, models.GetTimeEntryByID, convertTimeEntryToDTO, "time entry")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entry from the database
|
|
||||||
timeEntry, err := models.GetTimeEntryByID(c.Request.Context(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeEntry == nil {
|
|
||||||
utils.NotFoundResponse(c, "Time entry not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntriesByUserID handles GET /time-entries/user/:userId
|
// GetTimeEntriesByUserID handles GET /time-entries/user/:userId
|
||||||
@@ -108,28 +70,7 @@ func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/user/{userId} [get]
|
// @Router /time-entries/user/{userId} [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
||||||
// Parse user ID from URL
|
utils.HandleGetByFilter(c, models.GetTimeEntriesByUserID, convertTimeEntryToDTO, "time entries", "userId")
|
||||||
userIDStr := c.Param("userId")
|
|
||||||
userID, err := ulid.Parse(userIDStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByUserID(c.Request.Context(), types.FromULID(userID))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMyTimeEntries handles GET /time-entries/me
|
// GetMyTimeEntries handles GET /time-entries/me
|
||||||
@@ -145,27 +86,7 @@ func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/me [get]
|
// @Router /time-entries/me [get]
|
||||||
func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
||||||
// Get user ID from context (set by AuthMiddleware)
|
utils.HandleGetByUserID(c, models.GetTimeEntriesByUserID, convertTimeEntryToDTO, "time entries")
|
||||||
userID, err := middleware.GetUserIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByUserID(c.Request.Context(), userID)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntriesByProjectID handles GET /time-entries/project/:projectId
|
// GetTimeEntriesByProjectID handles GET /time-entries/project/:projectId
|
||||||
@@ -183,28 +104,7 @@ func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/project/{projectId} [get]
|
// @Router /time-entries/project/{projectId} [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
||||||
// Parse project ID from URL
|
utils.HandleGetByFilter(c, models.GetTimeEntriesByProjectID, convertTimeEntryToDTO, "time entries", "projectId")
|
||||||
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(), types.FromULID(projectID))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeEntriesByDateRange handles GET /time-entries/range
|
// GetTimeEntriesByDateRange handles GET /time-entries/range
|
||||||
@@ -223,46 +123,7 @@ func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/range [get]
|
// @Router /time-entries/range [get]
|
||||||
func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
||||||
// Parse date range from query parameters
|
utils.HandleGetByDateRange(c, models.GetTimeEntriesByDateRange, convertTimeEntryToDTO, "time entries")
|
||||||
startStr := c.Query("start")
|
|
||||||
endStr := c.Query("end")
|
|
||||||
|
|
||||||
if startStr == "" || endStr == "" {
|
|
||||||
utils.BadRequestResponse(c, "Start and end dates are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
start, err := time.Parse(time.RFC3339, startStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid start date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
end, err := time.Parse(time.RFC3339, endStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid end date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if end.Before(start) {
|
|
||||||
utils.BadRequestResponse(c, "End date cannot be before start date")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time entries from the database
|
|
||||||
timeEntries, err := models.GetTimeEntriesByDateRange(c.Request.Context(), start, end)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
||||||
for i, timeEntry := range timeEntries {
|
|
||||||
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTimeEntry handles POST /time-entries
|
// CreateTimeEntry handles POST /time-entries
|
||||||
@@ -280,31 +141,7 @@ func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries [post]
|
// @Router /time-entries [post]
|
||||||
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createTimeEntryWrapper, convertTimeEntryToDTO, "time entry")
|
||||||
var timeEntryCreateDTO dto.TimeEntryCreateDto
|
|
||||||
if err := c.ShouldBindJSON(&timeEntryCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
timeEntryCreate, err := convertCreateTimeEntryDTOToModel(timeEntryCreateDTO)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create time entry in the database
|
|
||||||
timeEntry, err := models.CreateTimeEntry(c.Request.Context(), timeEntryCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, timeEntryDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTimeEntry handles PUT /time-entries/:id
|
// UpdateTimeEntry handles PUT /time-entries/:id
|
||||||
@@ -324,44 +161,7 @@ func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/{id} [put]
|
// @Router /time-entries/{id} [put]
|
||||||
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateTimeEntry, convertTimeEntryToDTO, prepareTimeEntryUpdate, "time entry")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := types.ULIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var timeEntryUpdateDTO dto.TimeEntryUpdateDto
|
|
||||||
if err := c.ShouldBindJSON(&timeEntryUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
timeEntryUpdate, err := convertUpdateTimeEntryDTOToModel(timeEntryUpdateDTO, id)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update time entry in the database
|
|
||||||
timeEntry, err := models.UpdateTimeEntry(c.Request.Context(), timeEntryUpdate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeEntry == nil {
|
|
||||||
utils.NotFoundResponse(c, "Time entry not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, timeEntryDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTimeEntry handles DELETE /time-entries/:id
|
// DeleteTimeEntry handles DELETE /time-entries/:id
|
||||||
@@ -379,22 +179,7 @@ func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /time-entries/{id} [delete]
|
// @Router /time-entries/{id} [delete]
|
||||||
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
|
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteTimeEntry, "time entry")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete time entry from the database
|
|
||||||
err = models.DeleteTimeEntry(c.Request.Context(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting time entry: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@@ -490,3 +275,42 @@ func convertUpdateTimeEntryDTOToModel(dto dto.TimeEntryUpdateDto, id types.ULID)
|
|||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareTimeEntryUpdate prepares the time entry update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareTimeEntryUpdate(c *gin.Context) (models.TimeEntryUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := types.ULIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid time entry ID format")
|
||||||
|
return models.TimeEntryUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var timeEntryUpdateDTO dto.TimeEntryUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &timeEntryUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.TimeEntryUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to model
|
||||||
|
timeEntryUpdate, err := convertUpdateTimeEntryDTOToModel(timeEntryUpdateDTO, id)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.TimeEntryUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeEntryUpdate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTimeEntryWrapper is a wrapper function for models.CreateTimeEntry that takes a DTO as input
|
||||||
|
func createTimeEntryWrapper(ctx context.Context, createDTO dto.TimeEntryCreateDto) (*models.TimeEntry, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
timeEntryCreate, err := convertCreateTimeEntryDTOToModel(createDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateTimeEntry(ctx, timeEntryCreate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/api/dto"
|
||||||
"github.com/timetracker/backend/internal/api/middleware"
|
"github.com/timetracker/backend/internal/api/middleware"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/utils"
|
||||||
dto "github.com/timetracker/backend/internal/dtos"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
)
|
)
|
||||||
@@ -33,20 +34,7 @@ func NewUserHandler() *UserHandler {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users [get]
|
// @Router /users [get]
|
||||||
func (h *UserHandler) GetUsers(c *gin.Context) {
|
func (h *UserHandler) GetUsers(c *gin.Context) {
|
||||||
// Get users from the database
|
utils.HandleGetAll(c, models.GetAllUsers, convertUserToDTO, "users")
|
||||||
users, err := models.GetAllUsers(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error retrieving users: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTOs
|
|
||||||
userDTOs := make([]dto.UserDto, len(users))
|
|
||||||
for i, user := range users {
|
|
||||||
userDTOs[i] = convertUserToDTO(&user)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTOs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByID handles GET /users/:id
|
// GetUserByID handles GET /users/:id
|
||||||
@@ -65,30 +53,29 @@ func (h *UserHandler) GetUsers(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users/{id} [get]
|
// @Router /users/{id} [get]
|
||||||
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
||||||
// Parse ID from URL
|
// We need a custom wrapper for GetUserByID because the ID parameter is parsed differently
|
||||||
idStr := c.Param("id")
|
id, err := utils.ParseID(c, "id")
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
responses.BadRequestResponse(c, "Invalid user ID format")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user from the database
|
// Get user from the database
|
||||||
user, err := models.GetUserByID(c.Request.Context(), types.FromULID(id))
|
user, err := models.GetUserByID(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
responses.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.NotFoundResponse(c, "User not found")
|
responses.NotFoundResponse(c, "User not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
// Convert to DTO
|
||||||
userDTO := convertUserToDTO(user)
|
userDTO := convertUserToDTO(user)
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTO)
|
responses.SuccessResponse(c, http.StatusOK, userDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser handles POST /users
|
// CreateUser handles POST /users
|
||||||
@@ -106,27 +93,7 @@ func (h *UserHandler) GetUserByID(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users [post]
|
// @Router /users [post]
|
||||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||||
// Parse request body
|
utils.HandleCreate(c, createUserWrapper, convertUserToDTO, "user")
|
||||||
var userCreateDTO dto.UserCreateDto
|
|
||||||
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
userCreate := convertCreateDTOToModel(userCreateDTO)
|
|
||||||
|
|
||||||
// Create user in the database
|
|
||||||
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
userDTO := convertUserToDTO(user)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, userDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser handles PUT /users/:id
|
// UpdateUser handles PUT /users/:id
|
||||||
@@ -146,68 +113,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users/{id} [put]
|
// @Router /users/{id} [put]
|
||||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleUpdate(c, models.UpdateUser, convertUserToDTO, prepareUserUpdate, "user")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := types.ULIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
|
||||||
var userUpdateDTO dto.UserUpdateDto
|
|
||||||
if err := c.ShouldBindJSON(&userUpdateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to Model
|
|
||||||
update := models.UserUpdate{
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if userUpdateDTO.Email != nil {
|
|
||||||
update.Email = userUpdateDTO.Email
|
|
||||||
}
|
|
||||||
if userUpdateDTO.Password != nil {
|
|
||||||
update.Password = userUpdateDTO.Password
|
|
||||||
}
|
|
||||||
if userUpdateDTO.Role != nil {
|
|
||||||
update.Role = userUpdateDTO.Role
|
|
||||||
}
|
|
||||||
|
|
||||||
if userUpdateDTO.CompanyID.Valid {
|
|
||||||
if userUpdateDTO.CompanyID.Value != nil {
|
|
||||||
companyID, err := types.ULIDFromString(*userUpdateDTO.CompanyID.Value)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid company ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
update.CompanyID = types.NewNullable(companyID)
|
|
||||||
} else {
|
|
||||||
update.CompanyID = types.Null[types.ULID]()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if userUpdateDTO.HourlyRate != nil {
|
|
||||||
update.HourlyRate = userUpdateDTO.HourlyRate
|
|
||||||
}
|
|
||||||
// Update user in the database
|
|
||||||
user, err := models.UpdateUser(c.Request.Context(), update)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error updating user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
utils.NotFoundResponse(c, "User not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to DTO
|
|
||||||
userDTO := convertUserToDTO(user)
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser handles DELETE /users/:id
|
// DeleteUser handles DELETE /users/:id
|
||||||
@@ -225,22 +131,7 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
|
|||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /users/{id} [delete]
|
// @Router /users/{id} [delete]
|
||||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||||
// Parse ID from URL
|
utils.HandleDelete(c, models.DeleteUser, "user")
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := ulid.Parse(idStr)
|
|
||||||
if err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete user from the database
|
|
||||||
err = models.DeleteUser(c.Request.Context(), types.FromULID(id))
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error deleting user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login handles POST /auth/login
|
// Login handles POST /auth/login
|
||||||
@@ -260,21 +151,21 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
// Parse request body
|
// Parse request body
|
||||||
var loginDTO dto.LoginDto
|
var loginDTO dto.LoginDto
|
||||||
if err := c.ShouldBindJSON(&loginDTO); err != nil {
|
if err := c.ShouldBindJSON(&loginDTO); err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
responses.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate user
|
// Authenticate user
|
||||||
user, err := models.AuthenticateUser(c.Request.Context(), loginDTO.Email, loginDTO.Password)
|
user, err := models.AuthenticateUser(c.Request.Context(), loginDTO.Email, loginDTO.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "Invalid login credentials")
|
responses.UnauthorizedResponse(c, "Invalid login credentials")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
token, err := middleware.GenerateToken(user, c)
|
token, err := middleware.GenerateToken(user, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
responses.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +175,7 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
User: convertUserToDTO(user),
|
User: convertUserToDTO(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, tokenDTO)
|
responses.SuccessResponse(c, http.StatusOK, tokenDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register handles POST /auth/register
|
// Register handles POST /auth/register
|
||||||
@@ -303,7 +194,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
// Parse request body
|
// Parse request body
|
||||||
var userCreateDTO dto.UserCreateDto
|
var userCreateDTO dto.UserCreateDto
|
||||||
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
responses.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,14 +204,14 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
// Create user in the database
|
// Create user in the database
|
||||||
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
responses.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
token, err := middleware.GenerateToken(user, c)
|
token, err := middleware.GenerateToken(user, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
responses.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +221,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
User: convertUserToDTO(user),
|
User: convertUserToDTO(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, tokenDTO)
|
responses.SuccessResponse(c, http.StatusCreated, tokenDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentUser handles GET /auth/me
|
// GetCurrentUser handles GET /auth/me
|
||||||
@@ -349,26 +240,26 @@ func (h *UserHandler) GetCurrentUser(c *gin.Context) {
|
|||||||
// Get user ID from context (set by AuthMiddleware)
|
// Get user ID from context (set by AuthMiddleware)
|
||||||
userID, err := middleware.GetUserIDFromContext(c)
|
userID, err := middleware.GetUserIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "User not authenticated")
|
responses.UnauthorizedResponse(c, "User not authenticated")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user from the database
|
// Get user from the database
|
||||||
user, err := models.GetUserByID(c.Request.Context(), userID)
|
user, err := models.GetUserByID(c.Request.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
responses.InternalErrorResponse(c, "Error retrieving user: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
utils.NotFoundResponse(c, "User not found")
|
responses.NotFoundResponse(c, "User not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to DTO
|
// Convert to DTO
|
||||||
userDTO := convertUserToDTO(user)
|
userDTO := convertUserToDTO(user)
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusOK, userDTO)
|
responses.SuccessResponse(c, http.StatusOK, userDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for DTO conversion
|
// Helper functions for DTO conversion
|
||||||
@@ -390,6 +281,58 @@ func convertUserToDTO(user *models.User) dto.UserDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareUserUpdate prepares the user update object by parsing the ID, binding the JSON, and converting the DTO to a model
|
||||||
|
func prepareUserUpdate(c *gin.Context) (models.UserUpdate, error) {
|
||||||
|
// Parse ID from URL
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := types.ULIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid user ID format")
|
||||||
|
return models.UserUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var userUpdateDTO dto.UserUpdateDto
|
||||||
|
if err := utils.BindJSON(c, &userUpdateDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return models.UserUpdate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DTO to Model
|
||||||
|
update := models.UserUpdate{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateDTO.Email != nil {
|
||||||
|
update.Email = userUpdateDTO.Email
|
||||||
|
}
|
||||||
|
if userUpdateDTO.Password != nil {
|
||||||
|
update.Password = userUpdateDTO.Password
|
||||||
|
}
|
||||||
|
if userUpdateDTO.Role != nil {
|
||||||
|
update.Role = userUpdateDTO.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateDTO.CompanyID.Valid {
|
||||||
|
if userUpdateDTO.CompanyID.Value != nil {
|
||||||
|
companyID, err := types.ULIDFromString(*userUpdateDTO.CompanyID.Value)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid company ID format")
|
||||||
|
return models.UserUpdate{}, err
|
||||||
|
}
|
||||||
|
update.CompanyID = types.NewNullable(companyID)
|
||||||
|
} else {
|
||||||
|
update.CompanyID = types.Null[types.ULID]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateDTO.HourlyRate != nil {
|
||||||
|
update.HourlyRate = userUpdateDTO.HourlyRate
|
||||||
|
}
|
||||||
|
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
||||||
var companyID *types.ULID
|
var companyID *types.ULID
|
||||||
if dto.CompanyID != nil {
|
if dto.CompanyID != nil {
|
||||||
@@ -405,3 +348,12 @@ func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
|
|||||||
HourlyRate: dto.HourlyRate,
|
HourlyRate: dto.HourlyRate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createUserWrapper is a wrapper function for models.CreateUser that takes a DTO as input
|
||||||
|
func createUserWrapper(ctx context.Context, createDTO dto.UserCreateDto) (*models.User, error) {
|
||||||
|
// Convert DTO to model
|
||||||
|
userCreate := convertCreateDTOToModel(createDTO)
|
||||||
|
|
||||||
|
// Call the original function
|
||||||
|
return models.CreateUser(ctx, userCreate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/config"
|
"github.com/timetracker/backend/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,14 +18,14 @@ func APIKeyMiddleware(cfg *config.Config) gin.HandlerFunc {
|
|||||||
// Get API key from header
|
// Get API key from header
|
||||||
apiKey := c.GetHeader("X-API-Key")
|
apiKey := c.GetHeader("X-API-Key")
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
utils.UnauthorizedResponse(c, "API key is required")
|
responses.UnauthorizedResponse(c, "API key is required")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate API key
|
// Validate API key
|
||||||
if apiKey != cfg.APIKey {
|
if apiKey != cfg.APIKey {
|
||||||
utils.UnauthorizedResponse(c, "Invalid API key")
|
responses.UnauthorizedResponse(c, "Invalid API key")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
"github.com/timetracker/backend/internal/api/utils"
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
"github.com/timetracker/backend/internal/config"
|
"github.com/timetracker/backend/internal/config"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
@@ -73,7 +73,7 @@ func fileExists(path string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateRSAKeys generates RSA keys and saves them to disk
|
// generateRSAKeys generates RSA keys and saves them to disk
|
||||||
func generateRSAKeys(cfg models.JWTConfig) error {
|
func generateRSAKeys(cfg config.JWTConfig) error {
|
||||||
// Create key directory if it doesn't exist
|
// Create key directory if it doesn't exist
|
||||||
if err := os.MkdirAll(cfg.KeyDir, 0700); err != nil {
|
if err := os.MkdirAll(cfg.KeyDir, 0700); err != nil {
|
||||||
return fmt.Errorf("failed to create key directory: %w", err)
|
return fmt.Errorf("failed to create key directory: %w", err)
|
||||||
@@ -164,10 +164,10 @@ func loadPublicKey(path string) (*rsa.PublicKey, error) {
|
|||||||
|
|
||||||
// Claims represents the JWT claims
|
// Claims represents the JWT claims
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID *string `json:"companyId"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,14 +177,14 @@ func AuthMiddleware() gin.HandlerFunc {
|
|||||||
// Get the token from cookie
|
// Get the token from cookie
|
||||||
tokenString, err := c.Cookie("jwt")
|
tokenString, err := c.Cookie("jwt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "Authentication cookie is required")
|
responses.UnauthorizedResponse(c, "Authentication cookie is required")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := validateToken(tokenString)
|
claims, err := validateToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.UnauthorizedResponse(c, "Invalid or expired token")
|
responses.UnauthorizedResponse(c, "Invalid or expired token")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
userRole, exists := c.Get("role")
|
userRole, exists := c.Get("role")
|
||||||
if !exists {
|
if !exists {
|
||||||
utils.UnauthorizedResponse(c, "User role not found in context")
|
responses.UnauthorizedResponse(c, "User role not found in context")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
// Check if the user's role is in the allowed roles
|
// Check if the user's role is in the allowed roles
|
||||||
roleStr, ok := userRole.(string)
|
roleStr, ok := userRole.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
utils.InternalErrorResponse(c, "Invalid role type in context")
|
responses.InternalErrorResponse(c, "Invalid role type in context")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
utils.ForbiddenResponse(c, "Insufficient permissions")
|
responses.ForbiddenResponse(c, "Insufficient permissions")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -238,11 +238,16 @@ func RoleMiddleware(roles ...string) gin.HandlerFunc {
|
|||||||
// GenerateToken creates a new JWT token for a user
|
// GenerateToken creates a new JWT token for a user
|
||||||
func GenerateToken(user *models.User, c *gin.Context) (string, error) {
|
func GenerateToken(user *models.User, c *gin.Context) (string, error) {
|
||||||
// Create the claims
|
// Create the claims
|
||||||
|
var companyId *string
|
||||||
|
if user.CompanyID != nil {
|
||||||
|
wrapper := user.CompanyID.String()
|
||||||
|
companyId = &wrapper
|
||||||
|
}
|
||||||
claims := Claims{
|
claims := Claims{
|
||||||
UserID: user.ID.String(),
|
UserID: user.ID.String(),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
CompanyID: user.CompanyID.String(),
|
CompanyID: companyId,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.MustLoadConfig().JWTConfig.TokenDuration)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.MustLoadConfig().JWTConfig.TokenDuration)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package utils
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/timetracker/backend/internal/api/responses"
|
||||||
|
"github.com/timetracker/backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseID parses an ID from the URL parameter and converts it to a types.ULID
|
||||||
|
func ParseID(c *gin.Context, paramName string) (types.ULID, error) {
|
||||||
|
idStr := c.Param(paramName)
|
||||||
|
return types.ULIDFromString(idStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindJSON binds the request body to the provided struct
|
||||||
|
func BindJSON(c *gin.Context, obj interface{}) error {
|
||||||
|
if err := c.ShouldBindJSON(obj); err != nil {
|
||||||
|
return fmt.Errorf("invalid request body: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToDTO converts a slice of models to a slice of DTOs using the provided conversion function
|
||||||
|
func ConvertToDTO[M any, D any](models []M, convertFn func(*M) D) []D {
|
||||||
|
dtos := make([]D, len(models))
|
||||||
|
for i, model := range models {
|
||||||
|
// Create a copy of the model to avoid issues with loop variable capture
|
||||||
|
modelCopy := model
|
||||||
|
dtos[i] = convertFn(&modelCopy)
|
||||||
|
}
|
||||||
|
return dtos
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetAll is a generic function to handle GET all entities endpoints
|
||||||
|
func HandleGetAll[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getAllFn func(ctx context.Context) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getAllFn(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, 200, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByID is a generic function to handle GET entity by ID endpoints
|
||||||
|
func HandleGetByID[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByIDFn func(ctx context.Context, id types.ULID) (*M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entity from the database
|
||||||
|
entity, err := getByIDFn(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity == nil {
|
||||||
|
responses.NotFoundResponse(c, fmt.Sprintf("%s not found", entityName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTO
|
||||||
|
dto := convertFn(entity)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, 200, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCreate is a generic function to handle POST entity endpoints
|
||||||
|
func HandleCreate[C any, M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
createFn func(ctx context.Context, create C) (*M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Parse request body
|
||||||
|
var createDTO C
|
||||||
|
if err := BindJSON(c, &createDTO); err != nil {
|
||||||
|
responses.BadRequestResponse(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create entity in the database
|
||||||
|
entity, err := createFn(c.Request.Context(), createDTO)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error creating %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTO
|
||||||
|
dto := convertFn(entity)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, 201, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDelete is a generic function to handle DELETE entity endpoints
|
||||||
|
func HandleDelete(
|
||||||
|
c *gin.Context,
|
||||||
|
deleteFn func(ctx context.Context, id types.ULID) error,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Parse ID from URL
|
||||||
|
id, err := ParseID(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", entityName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete entity from the database
|
||||||
|
err = deleteFn(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error deleting %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, 204, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUpdate is a generic function to handle PUT entity endpoints
|
||||||
|
// It takes a prepareUpdateFn that handles parsing the ID, binding the JSON, and converting the DTO to a model update object
|
||||||
|
func HandleUpdate[U any, M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
updateFn func(ctx context.Context, update U) (*M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
prepareUpdateFn func(*gin.Context) (U, error),
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Prepare the update object (parse ID, bind JSON, convert DTO to model)
|
||||||
|
update, err := prepareUpdateFn(c)
|
||||||
|
if err != nil {
|
||||||
|
// The prepareUpdateFn should handle setting the appropriate error response
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update entity in the database
|
||||||
|
entity, err := updateFn(c.Request.Context(), update)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error updating %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity == nil {
|
||||||
|
responses.NotFoundResponse(c, fmt.Sprintf("%s not found", entityName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTO
|
||||||
|
dto := convertFn(entity)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByFilter is a generic function to handle GET entities by a filter parameter
|
||||||
|
func HandleGetByFilter[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByFilterFn func(ctx context.Context, filterID types.ULID) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
paramName string,
|
||||||
|
) {
|
||||||
|
// Parse filter ID from URL
|
||||||
|
filterID, err := ParseID(c, paramName)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, fmt.Sprintf("Invalid %s ID format", paramName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getByFilterFn(c.Request.Context(), filterID)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByUserID is a specialized function to handle GET entities by user ID
|
||||||
|
func HandleGetByUserID[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByUserIDFn func(ctx context.Context, userID types.ULID) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Get user ID from context (set by AuthMiddleware)
|
||||||
|
userID, exists := c.Get("userID")
|
||||||
|
if !exists {
|
||||||
|
responses.UnauthorizedResponse(c, "User not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDStr, ok := userID.(string)
|
||||||
|
if !ok {
|
||||||
|
responses.InternalErrorResponse(c, "Invalid user ID type in context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUserID, err := types.ULIDFromString(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error parsing user ID: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getByUserIDFn(c.Request.Context(), parsedUserID)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetByDateRange is a specialized function to handle GET entities by date range
|
||||||
|
func HandleGetByDateRange[M any, D any](
|
||||||
|
c *gin.Context,
|
||||||
|
getByDateRangeFn func(ctx context.Context, start, end time.Time) ([]M, error),
|
||||||
|
convertFn func(*M) D,
|
||||||
|
entityName string,
|
||||||
|
) {
|
||||||
|
// Parse date range from query parameters
|
||||||
|
startStr := c.Query("start")
|
||||||
|
endStr := c.Query("end")
|
||||||
|
|
||||||
|
if startStr == "" || endStr == "" {
|
||||||
|
responses.BadRequestResponse(c, "Start and end dates are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := time.Parse(time.RFC3339, startStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid start date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := time.Parse(time.RFC3339, endStr)
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequestResponse(c, "Invalid end date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if end.Before(start) {
|
||||||
|
responses.BadRequestResponse(c, "End date cannot be before start date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entities from the database
|
||||||
|
entities, err := getByDateRangeFn(c.Request.Context(), start, end)
|
||||||
|
if err != nil {
|
||||||
|
responses.InternalErrorResponse(c, fmt.Sprintf("Error retrieving %s: %s", entityName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTOs
|
||||||
|
dtos := ConvertToDTO(entities, convertFn)
|
||||||
|
|
||||||
|
responses.SuccessResponse(c, http.StatusOK, dtos)
|
||||||
|
}
|
||||||
@@ -9,14 +9,54 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/timetracker/backend/internal/models"
|
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DatabaseConfig contains the configuration data for the database connection
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
DBName string
|
||||||
|
SSLMode string
|
||||||
|
MaxIdleConns int // Maximum number of idle connections
|
||||||
|
MaxOpenConns int // Maximum number of open connections
|
||||||
|
MaxLifetime time.Duration // Maximum lifetime of a connection
|
||||||
|
LogLevel logger.LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultDatabaseConfig returns a default configuration with sensible values
|
||||||
|
func DefaultDatabaseConfig() DatabaseConfig {
|
||||||
|
return DatabaseConfig{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "timetracker",
|
||||||
|
Password: "password",
|
||||||
|
DBName: "timetracker",
|
||||||
|
SSLMode: "disable",
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
MaxOpenConns: 100,
|
||||||
|
MaxLifetime: time.Hour,
|
||||||
|
LogLevel: logger.Info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTConfig represents the configuration for JWT authentication
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string
|
||||||
|
TokenDuration time.Duration
|
||||||
|
KeyGenerate bool
|
||||||
|
KeyDir string
|
||||||
|
PrivKeyFile string
|
||||||
|
PubKeyFile string
|
||||||
|
KeyBits int
|
||||||
|
}
|
||||||
|
|
||||||
// Config represents the application configuration
|
// Config represents the application configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Database models.DatabaseConfig
|
Database DatabaseConfig
|
||||||
JWTConfig models.JWTConfig
|
JWTConfig JWTConfig
|
||||||
APIKey string
|
APIKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +66,8 @@ func LoadConfig() (*Config, error) {
|
|||||||
_ = godotenv.Load()
|
_ = godotenv.Load()
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Database: models.DefaultDatabaseConfig(),
|
Database: DefaultDatabaseConfig(),
|
||||||
JWTConfig: models.JWTConfig{},
|
JWTConfig: JWTConfig{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load database configuration
|
// Load database configuration
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/config"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global variable for the DB connection
|
||||||
|
var db *gorm.DB
|
||||||
|
|
||||||
|
// ErrDBNotInitialized is returned when a database operation is attempted before initialization
|
||||||
|
var ErrDBNotInitialized = errors.New("database not initialized")
|
||||||
|
|
||||||
|
// InitDB initializes the database connection (once at startup)
|
||||||
|
// with the provided configuration
|
||||||
|
func InitDB(config config.DatabaseConfig) error {
|
||||||
|
// Create connection using the default database name
|
||||||
|
gormDB, err := createConnection(config, config.DBName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the global db instance
|
||||||
|
db = gormDB
|
||||||
|
|
||||||
|
// Configure connection pool
|
||||||
|
return configureConnectionPool(db, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEngine returns the DB instance with context
|
||||||
|
func GetEngine(ctx context.Context) *gorm.DB {
|
||||||
|
if db == nil {
|
||||||
|
panic(ErrDBNotInitialized)
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseDB closes the database connection
|
||||||
|
func CloseDB() error {
|
||||||
|
if db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting database connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sqlDB.Close(); err != nil {
|
||||||
|
return fmt.Errorf("error closing database connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGormDB is used for special cases like database creation
|
||||||
|
func GetGormDB(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) {
|
||||||
|
return createConnection(dbConfig, dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateDB performs database migrations for all models
|
||||||
|
// This is a placeholder that will be called by models.MigrateDB
|
||||||
|
func MigrateDB() error {
|
||||||
|
if db == nil {
|
||||||
|
return ErrDBNotInitialized
|
||||||
|
}
|
||||||
|
// The actual migration is implemented in models.MigrateDB
|
||||||
|
// This is just a placeholder to make the migrate/main.go file work
|
||||||
|
return errors.New("MigrateDB should be called from models package")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createConnection creates a new database connection with the given configuration
|
||||||
|
func createConnection(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) {
|
||||||
|
// Create DSN (Data Source Name)
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode)
|
||||||
|
|
||||||
|
// Configure GORM logger
|
||||||
|
gormLogger := createGormLogger(dbConfig)
|
||||||
|
|
||||||
|
// Establish database connection with custom logger
|
||||||
|
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
|
Logger: gormLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error connecting to the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gormDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createGormLogger creates a configured GORM logger instance
|
||||||
|
func createGormLogger(dbConfig config.DatabaseConfig) logger.Interface {
|
||||||
|
return logger.New(
|
||||||
|
log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold
|
||||||
|
LogLevel: dbConfig.LogLevel, // Log level
|
||||||
|
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||||
|
Colorful: true, // Enable color
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureConnectionPool sets up the connection pool parameters
|
||||||
|
func configureConnectionPool(db *gorm.DB, config config.DatabaseConfig) error {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting database connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connection pool parameters
|
||||||
|
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
|
||||||
|
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(config.MaxLifetime)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -36,7 +37,7 @@ type ActivityCreate struct {
|
|||||||
// GetActivityByID finds an Activity by its ID
|
// GetActivityByID finds an Activity by its ID
|
||||||
func GetActivityByID(ctx context.Context, id types.ULID) (*Activity, error) {
|
func GetActivityByID(ctx context.Context, id types.ULID) (*Activity, error) {
|
||||||
var activity Activity
|
var activity Activity
|
||||||
result := GetEngine(ctx).Where("id = ?", id).First(&activity)
|
result := db.GetEngine(ctx).Where("id = ?", id).First(&activity)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -49,7 +50,7 @@ func GetActivityByID(ctx context.Context, id types.ULID) (*Activity, error) {
|
|||||||
// GetAllActivities returns all Activities
|
// GetAllActivities returns all Activities
|
||||||
func GetAllActivities(ctx context.Context) ([]Activity, error) {
|
func GetAllActivities(ctx context.Context) ([]Activity, error) {
|
||||||
var activities []Activity
|
var activities []Activity
|
||||||
result := GetEngine(ctx).Find(&activities)
|
result := db.GetEngine(ctx).Find(&activities)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, erro
|
|||||||
BillingRate: create.BillingRate,
|
BillingRate: create.BillingRate,
|
||||||
}
|
}
|
||||||
|
|
||||||
result := GetEngine(ctx).Create(&activity)
|
result := db.GetEngine(ctx).Create(&activity)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -91,6 +92,6 @@ func UpdateActivity(ctx context.Context, update ActivityUpdate) (*Activity, erro
|
|||||||
|
|
||||||
// DeleteActivity deletes an Activity by its ID
|
// DeleteActivity deletes an Activity by its ID
|
||||||
func DeleteActivity(ctx context.Context, id types.ULID) error {
|
func DeleteActivity(ctx context.Context, id types.ULID) error {
|
||||||
result := GetEngine(ctx).Delete(&Activity{}, id)
|
result := db.GetEngine(ctx).Delete(&Activity{}, id)
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -33,7 +34,7 @@ type CompanyUpdate struct {
|
|||||||
// GetCompanyByID finds a company by its ID
|
// GetCompanyByID finds a company by its ID
|
||||||
func GetCompanyByID(ctx context.Context, id types.ULID) (*Company, error) {
|
func GetCompanyByID(ctx context.Context, id types.ULID) (*Company, error) {
|
||||||
var company Company
|
var company Company
|
||||||
result := GetEngine(ctx).Where("id = ?", id).First(&company)
|
result := db.GetEngine(ctx).Where("id = ?", id).First(&company)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -46,7 +47,7 @@ func GetCompanyByID(ctx context.Context, id types.ULID) (*Company, error) {
|
|||||||
// GetAllCompanies returns all companies
|
// GetAllCompanies returns all companies
|
||||||
func GetAllCompanies(ctx context.Context) ([]Company, error) {
|
func GetAllCompanies(ctx context.Context) ([]Company, error) {
|
||||||
var companies []Company
|
var companies []Company
|
||||||
result := GetEngine(ctx).Find(&companies)
|
result := db.GetEngine(ctx).Find(&companies)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ func GetAllCompanies(ctx context.Context) ([]Company, error) {
|
|||||||
|
|
||||||
func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, error) {
|
func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, error) {
|
||||||
var customers []Customer
|
var customers []Customer
|
||||||
result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&customers)
|
result := db.GetEngine(ctx).Where("company_id = ?", companyID).Find(&customers)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error)
|
|||||||
Name: create.Name,
|
Name: create.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
result := GetEngine(ctx).Create(&company)
|
result := db.GetEngine(ctx).Create(&company)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -96,6 +97,6 @@ func UpdateCompany(ctx context.Context, update CompanyUpdate) (*Company, error)
|
|||||||
|
|
||||||
// DeleteCompany deletes a company by its ID
|
// DeleteCompany deletes a company by its ID
|
||||||
func DeleteCompany(ctx context.Context, id types.ULID) error {
|
func DeleteCompany(ctx context.Context, id types.ULID) error {
|
||||||
result := GetEngine(ctx).Delete(&Company{}, id)
|
result := db.GetEngine(ctx).Delete(&Company{}, id)
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -39,7 +40,7 @@ type CustomerUpdate struct {
|
|||||||
// GetCustomerByID finds a customer by its ID
|
// GetCustomerByID finds a customer by its ID
|
||||||
func GetCustomerByID(ctx context.Context, id types.ULID) (*Customer, error) {
|
func GetCustomerByID(ctx context.Context, id types.ULID) (*Customer, error) {
|
||||||
var customer Customer
|
var customer Customer
|
||||||
result := GetEngine(ctx).Where("id = ?", id).First(&customer)
|
result := db.GetEngine(ctx).Where("id = ?", id).First(&customer)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -52,7 +53,7 @@ func GetCustomerByID(ctx context.Context, id types.ULID) (*Customer, error) {
|
|||||||
// GetAllCustomers returns all customers
|
// GetAllCustomers returns all customers
|
||||||
func GetAllCustomers(ctx context.Context) ([]Customer, error) {
|
func GetAllCustomers(ctx context.Context) ([]Customer, error) {
|
||||||
var customers []Customer
|
var customers []Customer
|
||||||
result := GetEngine(ctx).Find(&customers)
|
result := db.GetEngine(ctx).Find(&customers)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, erro
|
|||||||
CompanyID: create.CompanyID,
|
CompanyID: create.CompanyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
result := GetEngine(ctx).Create(&customer)
|
result := db.GetEngine(ctx).Create(&customer)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -94,6 +95,6 @@ func UpdateCustomer(ctx context.Context, update CustomerUpdate) (*Customer, erro
|
|||||||
|
|
||||||
// DeleteCustomer deletes a customer by its ID
|
// DeleteCustomer deletes a customer by its ID
|
||||||
func DeleteCustomer(ctx context.Context, id types.ULID) error {
|
func DeleteCustomer(ctx context.Context, id types.ULID) error {
|
||||||
result := GetEngine(ctx).Delete(&Customer{}, id)
|
result := db.GetEngine(ctx).Delete(&Customer{}, id)
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-107
@@ -9,101 +9,33 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/driver/postgres" // For PostgreSQL
|
"github.com/timetracker/backend/internal/config"
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
|
"github.com/timetracker/backend/internal/permissions"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global variable for the DB connection
|
|
||||||
var defaultDB *gorm.DB
|
|
||||||
|
|
||||||
// DatabaseConfig contains the configuration data for the database connection
|
|
||||||
type DatabaseConfig struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
DBName string
|
|
||||||
SSLMode string
|
|
||||||
MaxIdleConns int // Maximum number of idle connections
|
|
||||||
MaxOpenConns int // Maximum number of open connections
|
|
||||||
MaxLifetime time.Duration // Maximum lifetime of a connection
|
|
||||||
LogLevel logger.LogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultDatabaseConfig returns a default configuration with sensible values
|
|
||||||
func DefaultDatabaseConfig() DatabaseConfig {
|
|
||||||
return DatabaseConfig{
|
|
||||||
Host: "localhost",
|
|
||||||
Port: 5432,
|
|
||||||
User: "timetracker",
|
|
||||||
Password: "password",
|
|
||||||
DBName: "timetracker",
|
|
||||||
SSLMode: "disable",
|
|
||||||
MaxIdleConns: 10,
|
|
||||||
MaxOpenConns: 100,
|
|
||||||
MaxLifetime: time.Hour,
|
|
||||||
LogLevel: logger.Info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitDB initializes the database connection (once at startup)
|
|
||||||
// with the provided configuration
|
|
||||||
func InitDB(config DatabaseConfig) error {
|
|
||||||
// Create DSN (Data Source Name)
|
|
||||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
|
||||||
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
|
||||||
|
|
||||||
// Configure GORM logger
|
|
||||||
gormLogger := logger.New(
|
|
||||||
log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer
|
|
||||||
logger.Config{
|
|
||||||
SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold
|
|
||||||
LogLevel: config.LogLevel, // Log level
|
|
||||||
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
|
||||||
Colorful: true, // Enable color
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Establish database connection with custom logger
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
||||||
Logger: gormLogger,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error connecting to the database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure connection pool
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting database connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set connection pool parameters
|
|
||||||
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
|
|
||||||
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
|
|
||||||
sqlDB.SetConnMaxLifetime(config.MaxLifetime)
|
|
||||||
|
|
||||||
defaultDB = db
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MigrateDB performs database migrations for all models
|
// MigrateDB performs database migrations for all models
|
||||||
func MigrateDB() error {
|
func MigrateDB() error {
|
||||||
if defaultDB == nil {
|
gormDB := db.GetEngine(context.Background())
|
||||||
|
if gormDB == nil {
|
||||||
return errors.New("database not initialized")
|
return errors.New("database not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Starting database migration...")
|
log.Println("Starting database migration...")
|
||||||
|
|
||||||
// Add all models that should be migrated here
|
// Add all models that should be migrated here
|
||||||
err := defaultDB.AutoMigrate(
|
err := gormDB.AutoMigrate(
|
||||||
&Company{},
|
&Company{},
|
||||||
&User{},
|
&User{},
|
||||||
&Customer{},
|
&Customer{},
|
||||||
&Project{},
|
&Project{},
|
||||||
&Activity{},
|
&Activity{},
|
||||||
&TimeEntry{},
|
&TimeEntry{},
|
||||||
|
&permissions.Role{},
|
||||||
|
&permissions.Policy{},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,34 +46,8 @@ func MigrateDB() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEngine returns the DB instance, possibly with context
|
// GetGormDB is used for special cases like database creation
|
||||||
func GetEngine(ctx context.Context) *gorm.DB {
|
func GetGormDB(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) {
|
||||||
if defaultDB == nil {
|
|
||||||
panic("database not initialized")
|
|
||||||
}
|
|
||||||
// If a special transaction is in ctx, you could check it here
|
|
||||||
return defaultDB.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseDB closes the database connection
|
|
||||||
func CloseDB() error {
|
|
||||||
if defaultDB == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB, err := defaultDB.DB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting database connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sqlDB.Close(); err != nil {
|
|
||||||
return fmt.Errorf("error closing database connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetGormDB(dbConfig DatabaseConfig, dbName string) (*gorm.DB, error) {
|
|
||||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode)
|
dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode)
|
||||||
|
|
||||||
@@ -184,7 +90,7 @@ func UpdateModel(ctx context.Context, model any, updates any) error {
|
|||||||
updateMap := make(map[string]any)
|
updateMap := make(map[string]any)
|
||||||
|
|
||||||
// Iterate through all fields
|
// Iterate through all fields
|
||||||
for i := 0; i < updateValue.NumField(); i++ {
|
for i := range updateValue.NumField() {
|
||||||
field := updateValue.Field(i)
|
field := updateValue.Field(i)
|
||||||
fieldType := updateType.Field(i)
|
fieldType := updateType.Field(i)
|
||||||
|
|
||||||
@@ -223,5 +129,14 @@ func UpdateModel(ctx context.Context, model any, updates any) error {
|
|||||||
return nil // Nothing to update
|
return nil // Nothing to update
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetEngine(ctx).Model(model).Updates(updateMap).Error
|
return db.GetEngine(ctx).Model(model).Updates(updateMap).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitDB and CloseDB are forwarded to the db package for backward compatibility
|
||||||
|
func InitDB(config config.DatabaseConfig) error {
|
||||||
|
return db.InitDB(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDB() error {
|
||||||
|
return db.CloseDB()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
// This file is intentionally left empty.
|
||||||
|
// The JWTConfig struct has been moved to the config package.
|
||||||
type JWTConfig struct {
|
|
||||||
Secret string `env:"JWT_SECRET" default:""`
|
|
||||||
TokenDuration time.Duration `env:"JWT_TOKEN_DURATION" default:"24h"`
|
|
||||||
KeyGenerate bool `env:"JWT_KEY_GENERATE" default:"true"`
|
|
||||||
KeyDir string `env:"JWT_KEY_DIR" default:"./keys"`
|
|
||||||
PrivKeyFile string `env:"JWT_PRIV_KEY_FILE" default:"jwt.key"`
|
|
||||||
PubKeyFile string `env:"JWT_PUB_KEY_FILE" default:"jwt.key.pub"`
|
|
||||||
KeyBits int `env:"JWT_KEY_BITS" default:"2048"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
type Project struct {
|
type Project struct {
|
||||||
EntityBase
|
EntityBase
|
||||||
Name string `gorm:"column:name;not null"`
|
Name string `gorm:"column:name;not null"`
|
||||||
CustomerID *types.ULID `gorm:"column:customer_id;type:bytea;not null"`
|
CustomerID *types.ULID `gorm:"column:customer_id;type:bytea;index"`
|
||||||
|
|
||||||
// Relationships (for Eager Loading)
|
// Relationships (for Eager Loading)
|
||||||
Customer *Customer `gorm:"foreignKey:CustomerID"`
|
Customer *Customer `gorm:"foreignKey:CustomerID"`
|
||||||
@@ -33,9 +33,9 @@ type ProjectCreate struct {
|
|||||||
|
|
||||||
// ProjectUpdate contains the updatable fields of a project
|
// ProjectUpdate contains the updatable fields of a project
|
||||||
type ProjectUpdate struct {
|
type ProjectUpdate struct {
|
||||||
ID types.ULID `gorm:"-"` // Exclude from updates
|
ID types.ULID `gorm:"-"` // Exclude from updates
|
||||||
Name *string `gorm:"column:name"`
|
Name *string `gorm:"column:name"`
|
||||||
CustomerID *types.ULID `gorm:"column:customer_id"`
|
CustomerID types.Nullable[types.ULID] `gorm:"column:customer_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the Create struct contains valid data
|
// Validate checks if the Create struct contains valid data
|
||||||
@@ -44,7 +44,7 @@ func (pc *ProjectCreate) Validate() error {
|
|||||||
return errors.New("project name cannot be empty")
|
return errors.New("project name cannot be empty")
|
||||||
}
|
}
|
||||||
// Check for valid CustomerID
|
// Check for valid CustomerID
|
||||||
if pc.CustomerID.Compare(types.ULID{}) == 0 {
|
if pc.CustomerID != nil && pc.CustomerID.Compare(types.ULID{}) == 0 {
|
||||||
return errors.New("customerID cannot be empty")
|
return errors.New("customerID cannot be empty")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -61,7 +61,7 @@ func (pu *ProjectUpdate) Validate() error {
|
|||||||
// GetProjectByID finds a project by its ID
|
// GetProjectByID finds a project by its ID
|
||||||
func GetProjectByID(ctx context.Context, id types.ULID) (*Project, error) {
|
func GetProjectByID(ctx context.Context, id types.ULID) (*Project, error) {
|
||||||
var project Project
|
var project Project
|
||||||
result := GetEngine(ctx).Where("id = ?", id).First(&project)
|
result := db.GetEngine(ctx).Where("id = ?", id).First(&project)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -72,9 +72,9 @@ func GetProjectByID(ctx context.Context, id types.ULID) (*Project, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectWithCustomer loads a project with the associated customer information
|
// GetProjectWithCustomer loads a project with the associated customer information
|
||||||
func GetProjectWithCustomer(ctx context.Context, id ulid.ULID) (*Project, error) {
|
func GetProjectWithCustomer(ctx context.Context, id types.ULID) (*Project, error) {
|
||||||
var project Project
|
var project Project
|
||||||
result := GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&project)
|
result := db.GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&project)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -87,7 +87,7 @@ func GetProjectWithCustomer(ctx context.Context, id ulid.ULID) (*Project, error)
|
|||||||
// GetAllProjects returns all projects
|
// GetAllProjects returns all projects
|
||||||
func GetAllProjects(ctx context.Context) ([]Project, error) {
|
func GetAllProjects(ctx context.Context) ([]Project, error) {
|
||||||
var projects []Project
|
var projects []Project
|
||||||
result := GetEngine(ctx).Find(&projects)
|
result := db.GetEngine(ctx).Find(&projects)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ func GetAllProjects(ctx context.Context) ([]Project, error) {
|
|||||||
// GetAllProjectsWithCustomers returns all projects with customer information
|
// GetAllProjectsWithCustomers returns all projects with customer information
|
||||||
func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) {
|
func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) {
|
||||||
var projects []Project
|
var projects []Project
|
||||||
result := GetEngine(ctx).Preload("Customer").Find(&projects)
|
result := db.GetEngine(ctx).Preload("Customer").Find(&projects)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -105,9 +105,9 @@ func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectsByCustomerID returns all projects of a specific customer
|
// GetProjectsByCustomerID returns all projects of a specific customer
|
||||||
func GetProjectsByCustomerID(ctx context.Context, customerID ulid.ULID) ([]Project, error) {
|
func GetProjectsByCustomerID(ctx context.Context, customerId types.ULID) ([]Project, error) {
|
||||||
var projects []Project
|
var projects []Project
|
||||||
result := GetEngine(ctx).Where("customer_id = ?", customerID).Find(&projects)
|
result := db.GetEngine(ctx).Where("customer_id = ?", customerId.ULID).Find(&projects)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the customer exists
|
// Check if the customer exists
|
||||||
if create.CustomerID == nil {
|
if create.CustomerID != nil {
|
||||||
customer, err := GetCustomerByID(ctx, *create.CustomerID)
|
customer, err := GetCustomerByID(ctx, *create.CustomerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error checking the customer: %w", err)
|
return nil, fmt.Errorf("error checking the customer: %w", err)
|
||||||
@@ -137,7 +137,7 @@ func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error)
|
|||||||
CustomerID: create.CustomerID,
|
CustomerID: create.CustomerID,
|
||||||
}
|
}
|
||||||
|
|
||||||
result := GetEngine(ctx).Create(&project)
|
result := db.GetEngine(ctx).Create(&project)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, fmt.Errorf("error creating the project: %w", result.Error)
|
return nil, fmt.Errorf("error creating the project: %w", result.Error)
|
||||||
}
|
}
|
||||||
@@ -160,13 +160,18 @@ func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If CustomerID is updated, check if the customer exists
|
// If CustomerID is updated, check if the customer exists
|
||||||
if update.CustomerID != nil {
|
if update.CustomerID.Valid {
|
||||||
customer, err := GetCustomerByID(ctx, *update.CustomerID)
|
if update.CustomerID.Value != nil {
|
||||||
if err != nil {
|
customer, err := GetCustomerByID(ctx, *update.CustomerID.Value)
|
||||||
return nil, fmt.Errorf("error checking the customer: %w", err)
|
if err != nil {
|
||||||
}
|
return nil, fmt.Errorf("error checking the customer: %w", err)
|
||||||
if customer == nil {
|
}
|
||||||
return nil, errors.New("the specified customer does not exist")
|
if customer == nil {
|
||||||
|
return nil, errors.New("the specified customer does not exist")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If CustomerID is nil, set it to nil in the project
|
||||||
|
project.CustomerID = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +185,9 @@ func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProject deletes a project by its ID
|
// DeleteProject deletes a project by its ID
|
||||||
func DeleteProject(ctx context.Context, id ulid.ULID) error {
|
func DeleteProject(ctx context.Context, id types.ULID) error {
|
||||||
// Here you could check if dependent entities exist
|
// Here you could check if dependent entities exist
|
||||||
result := GetEngine(ctx).Delete(&Project{}, id)
|
result := db.GetEngine(ctx).Delete(&Project{}, id)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return fmt.Errorf("error deleting the project: %w", result.Error)
|
return fmt.Errorf("error deleting the project: %w", result.Error)
|
||||||
}
|
}
|
||||||
@@ -199,7 +204,7 @@ func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*P
|
|||||||
var project *Project
|
var project *Project
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Customer check within the transaction
|
// Customer check within the transaction
|
||||||
var customer Customer
|
var customer Customer
|
||||||
if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil {
|
if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -105,7 +106,7 @@ func (tu *TimeEntryUpdate) Validate() error {
|
|||||||
// GetTimeEntryByID finds a time entry by its ID
|
// GetTimeEntryByID finds a time entry by its ID
|
||||||
func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) {
|
func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) {
|
||||||
var timeEntry TimeEntry
|
var timeEntry TimeEntry
|
||||||
result := GetEngine(ctx).Where("id = ?", id).First(&timeEntry)
|
result := db.GetEngine(ctx).Where("id = ?", id).First(&timeEntry)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -118,7 +119,7 @@ func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) {
|
|||||||
// GetTimeEntryWithRelations loads a time entry with all associated data
|
// GetTimeEntryWithRelations loads a time entry with all associated data
|
||||||
func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry, error) {
|
func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry, error) {
|
||||||
var timeEntry TimeEntry
|
var timeEntry TimeEntry
|
||||||
result := GetEngine(ctx).
|
result := db.GetEngine(ctx).
|
||||||
Preload("User").
|
Preload("User").
|
||||||
Preload("Project").
|
Preload("Project").
|
||||||
Preload("Project.Customer"). // Nested relationship
|
Preload("Project.Customer"). // Nested relationship
|
||||||
@@ -138,7 +139,7 @@ func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry,
|
|||||||
// GetAllTimeEntries returns all time entries
|
// GetAllTimeEntries returns all time entries
|
||||||
func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
|
func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
|
||||||
var timeEntries []TimeEntry
|
var timeEntries []TimeEntry
|
||||||
result := GetEngine(ctx).Find(&timeEntries)
|
result := db.GetEngine(ctx).Find(&timeEntries)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,7 @@ func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
|
|||||||
// GetTimeEntriesByUserID returns all time entries of a user
|
// GetTimeEntriesByUserID returns all time entries of a user
|
||||||
func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry, error) {
|
func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry, error) {
|
||||||
var timeEntries []TimeEntry
|
var timeEntries []TimeEntry
|
||||||
result := GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries)
|
result := db.GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,7 @@ func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry
|
|||||||
// GetTimeEntriesByProjectID returns all time entries of a project
|
// GetTimeEntriesByProjectID returns all time entries of a project
|
||||||
func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]TimeEntry, error) {
|
func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]TimeEntry, error) {
|
||||||
var timeEntries []TimeEntry
|
var timeEntries []TimeEntry
|
||||||
result := GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries)
|
result := db.GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -169,7 +170,7 @@ func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]Tim
|
|||||||
func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) {
|
func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) {
|
||||||
var timeEntries []TimeEntry
|
var timeEntries []TimeEntry
|
||||||
// Search for overlaps in the time range
|
// Search for overlaps in the time range
|
||||||
result := GetEngine(ctx).
|
result := db.GetEngine(ctx).
|
||||||
Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)",
|
Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)",
|
||||||
start, end, start, end).
|
start, end, start, end).
|
||||||
Find(&timeEntries)
|
Find(&timeEntries)
|
||||||
@@ -189,7 +190,7 @@ func SumBillableHoursByProject(ctx context.Context, projectID types.ULID) (float
|
|||||||
var result Result
|
var result Result
|
||||||
|
|
||||||
// SQL calculation of weighted hours
|
// SQL calculation of weighted hours
|
||||||
err := GetEngine(ctx).Raw(`
|
err := db.GetEngine(ctx).Raw(`
|
||||||
SELECT SUM(
|
SELECT SUM(
|
||||||
EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0)
|
EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0)
|
||||||
) as total_hours
|
) as total_hours
|
||||||
@@ -214,7 +215,7 @@ func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, e
|
|||||||
// Start a transaction
|
// Start a transaction
|
||||||
var timeEntry *TimeEntry
|
var timeEntry *TimeEntry
|
||||||
|
|
||||||
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Check references
|
// Check references
|
||||||
if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil {
|
if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -295,7 +296,7 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start a transaction for the update
|
// Start a transaction for the update
|
||||||
err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
err = db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Check references if they are updated
|
// Check references if they are updated
|
||||||
if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil {
|
if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil {
|
||||||
// Use current values if not updated
|
// Use current values if not updated
|
||||||
@@ -352,7 +353,7 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e
|
|||||||
|
|
||||||
// DeleteTimeEntry deletes a time entry by its ID
|
// DeleteTimeEntry deletes a time entry by its ID
|
||||||
func DeleteTimeEntry(ctx context.Context, id types.ULID) error {
|
func DeleteTimeEntry(ctx context.Context, id types.ULID) error {
|
||||||
result := GetEngine(ctx).Delete(&TimeEntry{}, id)
|
result := db.GetEngine(ctx).Delete(&TimeEntry{}, id)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return fmt.Errorf("error deleting the time entry: %w", result.Error)
|
return fmt.Errorf("error deleting the time entry: %w", result.Error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,28 +11,12 @@ import (
|
|||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
"github.com/timetracker/backend/internal/types"
|
"github.com/timetracker/backend/internal/types"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Argon2 Parameters
|
|
||||||
const (
|
|
||||||
// Recommended values for Argon2id
|
|
||||||
ArgonTime = 1
|
|
||||||
ArgonMemory = 64 * 1024 // 64MB
|
|
||||||
ArgonThreads = 4
|
|
||||||
ArgonKeyLen = 32
|
|
||||||
SaltLength = 16
|
|
||||||
)
|
|
||||||
|
|
||||||
// Role Constants
|
|
||||||
const (
|
|
||||||
RoleAdmin = "admin"
|
|
||||||
RoleUser = "user"
|
|
||||||
RoleViewer = "viewer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User represents a user in the system
|
// User represents a user in the system
|
||||||
type User struct {
|
type User struct {
|
||||||
EntityBase
|
EntityBase
|
||||||
@@ -42,6 +26,7 @@ type User struct {
|
|||||||
Role string `gorm:"column:role;not null;default:'user'"`
|
Role string `gorm:"column:role;not null;default:'user'"`
|
||||||
CompanyID *types.ULID `gorm:"column:company_id;type:bytea;index"`
|
CompanyID *types.ULID `gorm:"column:company_id;type:bytea;index"`
|
||||||
HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"`
|
HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"`
|
||||||
|
Companies []string `gorm:"type:text[]"`
|
||||||
|
|
||||||
// Relationship for Eager Loading
|
// Relationship for Eager Loading
|
||||||
Company *Company `gorm:"foreignKey:CompanyID"`
|
Company *Company `gorm:"foreignKey:CompanyID"`
|
||||||
@@ -202,7 +187,7 @@ func (uc *UserCreate) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if uc.CompanyID.Compare(types.ULID{}) == 0 {
|
if uc.CompanyID != nil && uc.CompanyID.Compare(types.ULID{}) == 0 {
|
||||||
return errors.New("companyID cannot be empty")
|
return errors.New("companyID cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +275,7 @@ func (uu *UserUpdate) Validate() error {
|
|||||||
// GetUserByID finds a user by their ID
|
// GetUserByID finds a user by their ID
|
||||||
func GetUserByID(ctx context.Context, id types.ULID) (*User, error) {
|
func GetUserByID(ctx context.Context, id types.ULID) (*User, error) {
|
||||||
var user User
|
var user User
|
||||||
result := GetEngine(ctx).Where("id = ?", id).First(&user)
|
result := db.GetEngine(ctx).Where("id = ?", id).First(&user)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -303,7 +288,7 @@ func GetUserByID(ctx context.Context, id types.ULID) (*User, error) {
|
|||||||
// GetUserByEmail finds a user by their email
|
// GetUserByEmail finds a user by their email
|
||||||
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||||
var user User
|
var user User
|
||||||
result := GetEngine(ctx).Where("email = ?", email).First(&user)
|
result := db.GetEngine(ctx).Where("email = ?", email).First(&user)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -316,7 +301,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
|||||||
// GetUserWithCompany loads a user with their company
|
// GetUserWithCompany loads a user with their company
|
||||||
func GetUserWithCompany(ctx context.Context, id types.ULID) (*User, error) {
|
func GetUserWithCompany(ctx context.Context, id types.ULID) (*User, error) {
|
||||||
var user User
|
var user User
|
||||||
result := GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&user)
|
result := db.GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&user)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -329,7 +314,7 @@ func GetUserWithCompany(ctx context.Context, id types.ULID) (*User, error) {
|
|||||||
// GetAllUsers returns all users
|
// GetAllUsers returns all users
|
||||||
func GetAllUsers(ctx context.Context) ([]User, error) {
|
func GetAllUsers(ctx context.Context) ([]User, error) {
|
||||||
var users []User
|
var users []User
|
||||||
result := GetEngine(ctx).Find(&users)
|
result := db.GetEngine(ctx).Find(&users)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -351,7 +336,7 @@ func GetUsersByCompanyID(ctx context.Context, companyID types.ULID) ([]User, err
|
|||||||
var users []User
|
var users []User
|
||||||
// Apply the dynamic company condition
|
// Apply the dynamic company condition
|
||||||
condition := getCompanyCondition(&companyID)
|
condition := getCompanyCondition(&companyID)
|
||||||
result := GetEngine(ctx).Scopes(condition).Find(&users)
|
result := db.GetEngine(ctx).Scopes(condition).Find(&users)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -368,7 +353,7 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
|
|||||||
// Start a transaction
|
// Start a transaction
|
||||||
var user *User
|
var user *User
|
||||||
|
|
||||||
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Check if email already exists
|
// Check if email already exists
|
||||||
var count int64
|
var count int64
|
||||||
if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil {
|
if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil {
|
||||||
@@ -378,13 +363,15 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
|
|||||||
return errors.New("email is already in use")
|
return errors.New("email is already in use")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if company exists
|
if create.CompanyID != nil {
|
||||||
var companyCount int64
|
// Check if company exists
|
||||||
if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil {
|
var companyCount int64
|
||||||
return fmt.Errorf("error checking company: %w", err)
|
if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil {
|
||||||
}
|
return fmt.Errorf("error checking company: %w", err)
|
||||||
if companyCount == 0 {
|
}
|
||||||
return errors.New("the specified company does not exist")
|
if companyCount == 0 {
|
||||||
|
return errors.New("the specified company does not exist")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password with unique salt
|
// Hash password with unique salt
|
||||||
@@ -435,7 +422,7 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start a transaction for the update
|
// Start a transaction for the update
|
||||||
err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
err = db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// If email is updated, check if it's already in use
|
// If email is updated, check if it's already in use
|
||||||
if update.Email != nil && *update.Email != user.Email {
|
if update.Email != nil && *update.Email != user.Email {
|
||||||
var count int64
|
var count int64
|
||||||
@@ -492,7 +479,6 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) {
|
|||||||
} else {
|
} else {
|
||||||
updates["company_id"] = *update.CompanyID.Value
|
updates["company_id"] = *update.CompanyID.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if update.HourlyRate != nil {
|
if update.HourlyRate != nil {
|
||||||
updates["hourly_rate"] = *update.HourlyRate
|
updates["hourly_rate"] = *update.HourlyRate
|
||||||
@@ -521,7 +507,7 @@ func DeleteUser(ctx context.Context, id types.ULID) error {
|
|||||||
// Here one could check if dependent entities exist
|
// Here one could check if dependent entities exist
|
||||||
// e.g., don't delete if time entries still exist
|
// e.g., don't delete if time entries still exist
|
||||||
|
|
||||||
result := GetEngine(ctx).Delete(&User{}, id)
|
result := db.GetEngine(ctx).Delete(&User{}, id)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return fmt.Errorf("error deleting user: %w", result.Error)
|
return fmt.Errorf("error deleting user: %w", result.Error)
|
||||||
}
|
}
|
||||||
@@ -551,3 +537,20 @@ func AuthenticateUser(ctx context.Context, email, password string) (*User, error
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Argon2 Parameters
|
||||||
|
const (
|
||||||
|
// Recommended values for Argon2id
|
||||||
|
ArgonTime = 1
|
||||||
|
ArgonMemory = 64 * 1024 // 64MB
|
||||||
|
ArgonThreads = 4
|
||||||
|
ArgonKeyLen = 32
|
||||||
|
SaltLength = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role Constants
|
||||||
|
const (
|
||||||
|
RoleAdmin = "admin"
|
||||||
|
RoleUser = "user"
|
||||||
|
RoleViewer = "viewer"
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *User) EffectivePermissions(ctx context.Context, scope string) (Permission, error) {
|
||||||
|
if u.ActiveRole == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the role and its associated policies using the helper function.
|
||||||
|
role, err := LoadRoleWithPolicies(ctx, u.ActiveRole.ID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var perm Permission
|
||||||
|
for _, policy := range role.Policies {
|
||||||
|
for pat, p := range policy.Scopes {
|
||||||
|
if MatchScope(pat, scope) {
|
||||||
|
perm |= p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return perm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) HasPermission(ctx context.Context, scope string, requiredPerm Permission) (bool, error) {
|
||||||
|
effective, err := u.EffectivePermissions(ctx, scope)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return (effective & requiredPerm) == requiredPerm, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"github.com/timetracker/backend/internal/db"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadRoleWithPolicies loads a role with its associated policies from the database.
|
||||||
|
func LoadRoleWithPolicies(ctx context.Context, roleID ulid.ULID) (*Role, error) {
|
||||||
|
var role Role
|
||||||
|
err := db.GetEngine(ctx).Preload("Policies").First(&role, "id = ?", roleID).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, fmt.Errorf("role with ID %s not found", roleID)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to load role: %w", err)
|
||||||
|
}
|
||||||
|
return &role, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func MatchScope(pattern, scope string) bool {
|
||||||
|
if strings.HasSuffix(pattern, "/*") {
|
||||||
|
prefix := strings.TrimSuffix(pattern, "/*")
|
||||||
|
return strings.HasPrefix(scope, prefix)
|
||||||
|
}
|
||||||
|
return pattern == scope
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
type Permission uint64
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermRead Permission = 1 << iota // 1
|
||||||
|
PermWrite // 2
|
||||||
|
PermCreate // 4
|
||||||
|
PermList // 8
|
||||||
|
PermDelete // 16
|
||||||
|
PermModerate // 32
|
||||||
|
PermSuperadmin // 64
|
||||||
|
)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
ID ulid.ULID `gorm:"primaryKey;type:bytea"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
RoleID ulid.ULID `gorm:"type:bytea"` //Fremdschlüssel
|
||||||
|
Scopes Scopes `gorm:"type:jsonb;not null"` // JSONB-Spalte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes type to handle JSON marshalling
|
||||||
|
type Scopes map[string]Permission
|
||||||
|
|
||||||
|
// Scan scan value into Jsonb, implements sql.Scanner interface
|
||||||
|
func (j *Scopes) Scan(value interface{}) error {
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes map[string]Permission
|
||||||
|
if err := json.Unmarshal(bytes, &scopes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*j = scopes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value return json value, implement driver.Valuer interface
|
||||||
|
func (j Scopes) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(j)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID ulid.ULID `gorm:"primaryKey;type:bytea"`
|
||||||
|
Name string `gorm:"unique;not null"`
|
||||||
|
Policies []Policy `gorm:"foreignKey:RoleID"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ActiveRole *Role `gorm:"foreignKey:UserID"` // Beziehung zur aktiven Rolle
|
||||||
|
UserID ulid.ULID `gorm:"type:bytea"` //Fremdschlüssel
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "YOUR_POSTMAN_ID",
|
||||||
|
"name": "Activity API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "GET /api/activities",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/activities",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"activities"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/activities/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/activities/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"activities",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/activities",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\",\n\t\"billingRate\": 0\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/activities",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"activities"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUT /api/activities/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\",\n\t\"billingRate\": 0\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/activities/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"activities",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DELETE /api/activities/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/activities/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"activities",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "YOUR_POSTMAN_ID",
|
||||||
|
"name": "Company API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "GET /api/companies",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/companies",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"companies"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/companies/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/companies/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"companies",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/companies",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/companies",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"companies"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUT /api/companies/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/companies/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"companies",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DELETE /api/companies/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/companies/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"companies",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "YOUR_POSTMAN_ID",
|
||||||
|
"name": "Customer API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "GET /api/customers",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/customers",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"customers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/customers/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/customers/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"customers",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/customers/company/:companyId",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/customers/company/:companyId",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"customers",
|
||||||
|
"company",
|
||||||
|
":companyId"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "companyId",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/customers",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\",\n\t\"companyId\": \"\",\n\t\"ownerUserID\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/customers",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"customers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUT /api/customers/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\",\n\t\"companyId\": \"\",\n\t\"ownerUserID\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/customers/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"customers",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DELETE /api/customers/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/customers/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"customers",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "YOUR_POSTMAN_ID",
|
||||||
|
"name": "Project API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "GET /api/projects",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/projects/with-customers",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects/with-customers",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects",
|
||||||
|
"with-customers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/projects/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/projects/customer/:customerId",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects/customer/:customerId",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects",
|
||||||
|
"customer",
|
||||||
|
":customerId"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "customerId",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/projects",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\",\n\t\"customerId\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUT /api/projects/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"name\": \"\",\n\t\"customerId\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DELETE /api/projects/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/projects/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"projects",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "YOUR_POSTMAN_ID",
|
||||||
|
"name": "TimeEntry API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "GET /api/time-entries",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/time-entries/me",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/me",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
"me"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/time-entries/range",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/range?start=2023-01-01T00:00:00Z&end=2023-01-02T00:00:00Z",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
"range"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "start",
|
||||||
|
"value": "2023-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "end",
|
||||||
|
"value": "2023-01-02T00:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/time-entries/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/time-entries/user/:userId",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/user/:userId",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
"user",
|
||||||
|
":userId"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "userId",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/time-entries/project/:projectId",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/project/:projectId",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
"project",
|
||||||
|
":projectId"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "projectId",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/time-entries",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userID\": \"\",\n \"projectID\": \"\",\n \"activityID\": \"\",\n \"start\": \"2023-01-01T00:00:00Z\",\n \"end\": \"2023-01-01T01:00:00Z\",\n \"description\": \"\",\n \"billable\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUT /api/time-entries/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userID\": \"\",\n \"projectID\": \"\",\n \"activityID\": \"\",\n \"start\": \"2023-01-01T00:00:00Z\",\n \"end\": \"2023-01-01T01:00:00Z\",\n \"description\": \"\",\n \"billable\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DELETE /api/time-entries/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/time-entries/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"time-entries",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "YOUR_POSTMAN_ID",
|
||||||
|
"name": "User API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Auth",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "POST /api/auth/login",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/auth/login",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"login"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/auth/register",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\",\n\t\"role\": \"user\",\n\t\"companyID\": \"\",\n\t\"hourlyRate\": 0\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/auth/register",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"register"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/auth/me",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/auth/me",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"me"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Users",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "GET /api/users",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/users",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GET /api/users/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/users/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"users",
|
||||||
|
":id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POST /api/users",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\",\n\t\"role\": \"user\",\n\t\"companyID\": \"\",\n\t\"hourlyRate\": 0\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/users",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PUT /api/users/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\",\n\t\"role\": \"user\",\n\t\"companyID\": \"\",\n\t\"hourlyRate\": 0\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/users/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"users",
|
||||||
|
":id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DELETE /api/users/:id",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{JWT_TOKEN}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{API_URL}}/api/users/:id",
|
||||||
|
"host": [
|
||||||
|
"{{API_URL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"users",
|
||||||
|
":id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
packages:
|
packages:
|
||||||
- path: github.com/timetracker/backend/internal/dtos
|
- path: "github.com/timetracker/backend/internal/api/dto"
|
||||||
type_mappings:
|
type_mappings:
|
||||||
"time.Time": "string"
|
"time.Time": "string"
|
||||||
"types.ULID": "string"
|
"types.ULID": "string"
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# Berechtigungssystem Plan
|
||||||
|
|
||||||
|
Dieser Plan beschreibt die Implementierung eines scope-basierten Berechtigungssystems für das TimeTracker-Projekt.
|
||||||
|
|
||||||
|
## Grundkonzept
|
||||||
|
|
||||||
|
- Ein **Benutzer** kann eine **Rolle** annehmen, aber immer nur eine ist aktiv.
|
||||||
|
- Eine **Rolle** besteht aus mehreren **Policies**.
|
||||||
|
- Eine **Policy** hat einen Namen und eine Map, die **Scopes** (z. B. `items/books`) einem **Berechtigungsschlüssel** (Bitflag) zuordnet.
|
||||||
|
- Berechtigungsschlüssel sind Bitflags, die Permissions wie `read`, `write`, `create`, `list`, `delete`, `moderate`, `superadmin` usw. repräsentieren.
|
||||||
|
- Scopes können **Wildcards** enthalten, z. B. `items/*`, das auf `items/books` vererbt wird.
|
||||||
|
- Ziel: Berechtigungen sowohl im Go-Backend (für API-Sicherheit) als auch im TypeScript-Frontend (für UI-Anpassung) evaluieren.
|
||||||
|
|
||||||
|
## Implementierung im Go-Backend
|
||||||
|
|
||||||
|
### 1. Ordnerstruktur
|
||||||
|
|
||||||
|
- Neuer Ordner: `backend/internal/permissions`
|
||||||
|
- Dateien:
|
||||||
|
- `permissions.go`: `Permission`-Konstanten (Bitflags).
|
||||||
|
- `policy.go`: `Policy`-Struktur.
|
||||||
|
- `role.go`: `Role`-Struktur.
|
||||||
|
- `user.go`: Erweiterung der `User`-Struktur.
|
||||||
|
- `matching.go`: `matchScope`-Funktion.
|
||||||
|
- `evaluator.go`: `EffectivePermissions`- und `HasPermission`-Funktionen.
|
||||||
|
|
||||||
|
### 2. Go-Strukturen
|
||||||
|
|
||||||
|
- `permissions.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
type Permission uint64
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermRead Permission = 1 << iota // 1
|
||||||
|
PermWrite // 2
|
||||||
|
PermCreate // 4
|
||||||
|
PermList // 8
|
||||||
|
PermDelete // 16
|
||||||
|
PermModerate // 32
|
||||||
|
PermSuperadmin // 64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `policy.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
Name string
|
||||||
|
Scopes map[string]Permission
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `role.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
Name string
|
||||||
|
Policies []Policy
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import "github.com/your-org/your-project/backend/internal/models" // Pfad anpassen
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
models.User // Einbettung
|
||||||
|
ActiveRole *Role
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Funktionen
|
||||||
|
|
||||||
|
- `matching.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func MatchScope(pattern, scope string) bool {
|
||||||
|
if strings.HasSuffix(pattern, "/*") {
|
||||||
|
prefix := strings.TrimSuffix(pattern, "/*")
|
||||||
|
return strings.HasPrefix(scope, prefix)
|
||||||
|
}
|
||||||
|
return pattern == scope
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `evaluator.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
func (u *User) EffectivePermissions(scope string) Permission {
|
||||||
|
if u.ActiveRole == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var perm Permission
|
||||||
|
for _, policy := range u.ActiveRole.Policies {
|
||||||
|
for pat, p := range policy.Scopes {
|
||||||
|
if MatchScope(pat, scope) {
|
||||||
|
perm |= p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return perm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) HasPermission(scope string, requiredPerm Permission) bool {
|
||||||
|
effective := u.EffectivePermissions(scope)
|
||||||
|
return (effective & requiredPerm) == requiredPerm
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Integration in die API-Handler
|
||||||
|
|
||||||
|
- Anpassung der `jwt_auth.go` Middleware.
|
||||||
|
- Verwendung von `HasPermission` in den API-Handlern.
|
||||||
|
|
||||||
|
## Persistierung (Datenbank)
|
||||||
|
|
||||||
|
### 1. Datenbankmodell
|
||||||
|
|
||||||
|
- Zwei neue Tabellen: `roles` und `policies`.
|
||||||
|
- `roles`:
|
||||||
|
- `id` (ULID, Primärschlüssel)
|
||||||
|
- `name` (VARCHAR, eindeutig)
|
||||||
|
- `policies`:
|
||||||
|
- `id` (ULID, Primärschlüssel)
|
||||||
|
- `name` (VARCHAR, eindeutig)
|
||||||
|
- `role_id` (ULID, Fremdschlüssel, der auf `roles.id` verweist)
|
||||||
|
- `scopes` (JSONB oder TEXT, speichert die `map[string]Permission` als JSON)
|
||||||
|
- Beziehung: 1:n zwischen `roles` und `policies`.
|
||||||
|
|
||||||
|
### 2. Go-Strukturen (Anpassungen)
|
||||||
|
|
||||||
|
- `role.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/your-org/your-project/backend/internal/types" // Pfad anpassen
|
||||||
|
)
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID types.ULID `gorm:"primaryKey;type:bytea"`
|
||||||
|
Name string `gorm:"unique;not null"`
|
||||||
|
Policies []Policy `gorm:"foreignKey:RoleID"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `policy.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/your-org/your-project/backend/internal/types" // Pfad anpassen
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
ID types.ULID `gorm:"primaryKey;type:bytea"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
RoleID types.ULID `gorm:"type:bytea"` //Fremdschlüssel
|
||||||
|
Scopes Scopes `gorm:"type:jsonb;not null"` // JSONB-Spalte
|
||||||
|
}
|
||||||
|
|
||||||
|
//Scopes type to handle JSON marshalling
|
||||||
|
type Scopes map[string]Permission
|
||||||
|
|
||||||
|
// Scan scan value into Jsonb, implements sql.Scanner interface
|
||||||
|
func (j *Scopes) Scan(value interface{}) error {
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes map[string]Permission
|
||||||
|
if err := json.Unmarshal(bytes, &scopes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*j = scopes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value return json value, implement driver.Valuer interface
|
||||||
|
func (j Scopes) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(j)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### 3. Migration
|
||||||
|
|
||||||
|
- Verwendung des vorhandenen Migrations-Frameworks (`backend/cmd/migrate/main.go`).
|
||||||
|
|
||||||
|
### 4. Seed-Daten
|
||||||
|
|
||||||
|
- Optionale Seed-Daten (`backend/cmd/seed/main.go`).
|
||||||
|
|
||||||
|
### 5. Anpassung der Funktionen
|
||||||
|
|
||||||
|
- Anpassung von `EffectivePermissions` und `HasPermission` in `evaluator.go` für Datenbankzugriff.
|
||||||
|
|
||||||
|
## Mermaid Diagramm
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph Benutzer
|
||||||
|
U[User] --> AR(ActiveRole)
|
||||||
|
end
|
||||||
|
subgraph Rolle
|
||||||
|
AR --> R(Role)
|
||||||
|
R --> P1(Policy 1)
|
||||||
|
R --> P2(Policy 2)
|
||||||
|
R --> Pn(Policy n)
|
||||||
|
end
|
||||||
|
subgraph Policy
|
||||||
|
P1 --> S1(Scope 1: Permissions)
|
||||||
|
P1 --> S2(Scope 2: Permissions)
|
||||||
|
P2 --> S3(Scope 3: Permissions)
|
||||||
|
Pn --> Sm(Scope m: Permissions)
|
||||||
|
end
|
||||||
|
|
||||||
|
S1 -- Permissions --> PR(PermRead)
|
||||||
|
S1 -- Permissions --> PW(PermWrite)
|
||||||
|
S2 -- Permissions --> PL(PermList)
|
||||||
|
Sm -- Permissions --> PD(PermDelete)
|
||||||
|
|
||||||
|
style U fill:#f9f,stroke:#333,stroke-width:2px
|
||||||
|
style R fill:#ccf,stroke:#333,stroke-width:2px
|
||||||
|
style P1,P2,Pn fill:#ddf,stroke:#333,stroke-width:2px
|
||||||
|
style S1,S2,S3,Sm fill:#eef,stroke:#333,stroke-width:1px
|
||||||
|
style PR,PW,PL,PD fill:#ff9,stroke:#333,stroke-width:1px
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
description = "Development environment for Go and Next.js (TypeScript)";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Go development tools
|
||||||
|
goPackages = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
golangci-lint
|
||||||
|
delve
|
||||||
|
go-outline
|
||||||
|
gotools
|
||||||
|
go-mockgen
|
||||||
|
gomodifytags
|
||||||
|
impl
|
||||||
|
gotests
|
||||||
|
];
|
||||||
|
|
||||||
|
# TypeScript/Next.js development tools
|
||||||
|
nodePackages = with pkgs; [
|
||||||
|
nodejs_20
|
||||||
|
nodePackages.typescript
|
||||||
|
nodePackages.typescript-language-server
|
||||||
|
nodePackages.yarn
|
||||||
|
nodePackages.pnpm
|
||||||
|
nodePackages.npm
|
||||||
|
nodePackages.prettier
|
||||||
|
nodePackages.eslint
|
||||||
|
nodePackages.next
|
||||||
|
];
|
||||||
|
|
||||||
|
# General development tools
|
||||||
|
commonPackages = with pkgs; [
|
||||||
|
git
|
||||||
|
gh
|
||||||
|
nixpkgs-fmt
|
||||||
|
pre-commit
|
||||||
|
ripgrep
|
||||||
|
jq
|
||||||
|
curl
|
||||||
|
coreutils
|
||||||
|
gnumake
|
||||||
|
];
|
||||||
|
|
||||||
|
# VSCode with extensions
|
||||||
|
vscodeWithExtensions = pkgs.vscode-with-extensions.override {
|
||||||
|
vscodeExtensions = with pkgs.vscode-extensions; [
|
||||||
|
golang.go # Go support
|
||||||
|
esbenp.prettier-vscode # Prettier
|
||||||
|
dbaeumer.vscode-eslint # ESLint
|
||||||
|
ms-vscode.vscode-typescript-tslint-plugin # TypeScript
|
||||||
|
bradlc.vscode-tailwindcss # Tailwind CSS support
|
||||||
|
jnoortheen.nix-ide # Nix support
|
||||||
|
] ++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [
|
||||||
|
{
|
||||||
|
name = "nextjs";
|
||||||
|
publisher = "pulkitgangwar";
|
||||||
|
version = "1.0.6";
|
||||||
|
sha256 = "sha256-L6ZgqNkM0qzSiTKiGfgQB9m3U0HmwLA3NZ9nrslQjeg=";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = goPackages ++ nodePackages ++ commonPackages ++ [ vscodeWithExtensions ];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "🚀 Welcome to the Go and Next.js (TypeScript) development environment!"
|
||||||
|
echo "📦 Available tools:"
|
||||||
|
echo " Go: $(go version)"
|
||||||
|
echo " Node: $(node --version)"
|
||||||
|
echo " TypeScript: $(tsc --version)"
|
||||||
|
echo " Next.js: $(npx next --version)"
|
||||||
|
echo ""
|
||||||
|
echo "🔧 Use 'code .' to open VSCode with the appropriate extensions"
|
||||||
|
echo "🔄 Run 'nix flake update' to update dependencies"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
GOROOT = "${pkgs.go}/share/go";
|
||||||
|
GOPATH = "$(pwd)/.go";
|
||||||
|
GO111MODULE = "on";
|
||||||
|
|
||||||
|
# NodeJS setup
|
||||||
|
NODE_OPTIONS = "--max-old-space-size=4096";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Optional: Add custom packages if needed
|
||||||
|
packages = {
|
||||||
|
# Example of a custom package or script if needed
|
||||||
|
# my-tool = ...
|
||||||
|
};
|
||||||
|
|
||||||
|
# Default package if someone runs `nix build`
|
||||||
|
defaultPackage = self.devShells.${system}.default;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Refactoring Plan for backend/internal/api/handlers
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Refactor the code in `backend/internal/api/handlers` to reduce repetition and create helper functions for boilerplate operations, utilizing functions from `backend/internal/api/utils/handler_utils.go` and creating new ones if necessary.
|
||||||
|
|
||||||
|
## Analysis
|
||||||
|
|
||||||
|
The following common patterns were identified in the handler files:
|
||||||
|
|
||||||
|
1. **Error Handling:** Each handler function repeats the same error handling pattern.
|
||||||
|
2. **DTO Binding:** Parsing the request body and handling potential errors.
|
||||||
|
3. **ID Parsing:** Parsing the ID from the URL and handling potential errors.
|
||||||
|
4. **DTO Conversion:** Converting between DTOs and models.
|
||||||
|
5. **Success Responses:** Calling `responses.SuccessResponse` with the appropriate HTTP status code and data.
|
||||||
|
6. **Not Found Responses:** Checking if a record exists and calling `responses.NotFoundResponse` if it doesn't.
|
||||||
|
|
||||||
|
The `Update` handler is the most complex and has the most potential for refactoring.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. **Implement a generic `HandleUpdate` function in `handler_utils.go`:** This function will encapsulate the common logic for updating entities, including parsing the ID, binding the JSON, converting the DTO to a model, calling the update function, and handling errors and not found cases. The function will also handle nullable fields correctly.
|
||||||
|
2. **Modify the existing handlers to use the new `HandleUpdate` function:** This will involve removing the duplicated code from each handler and calling the generic function instead.
|
||||||
|
3. **Create new helper functions in `handler_utils.go` if needed:** If there are any specific operations that are not covered by the existing utility functions, I will create new ones to handle them.
|
||||||
|
|
||||||
Reference in New Issue
Block a user