feat: Add authentication DTOs and setup API routes for user and activity management
This commit is contained in:
		
							parent
							
								
									aa5c7e77fc
								
							
						
					
					
						commit
						558ee70c21
					
				| @ -9,6 +9,7 @@ import ( | |||||||
| 	swaggerFiles "github.com/swaggo/files" | 	swaggerFiles "github.com/swaggo/files" | ||||||
| 	ginSwagger "github.com/swaggo/gin-swagger" | 	ginSwagger "github.com/swaggo/gin-swagger" | ||||||
| 	_ "github.com/timetracker/backend/docs" // This line is important for swag to work | 	_ "github.com/timetracker/backend/docs" // This line is important for swag to work | ||||||
|  | 	"github.com/timetracker/backend/internal/api/routes" | ||||||
| 	"github.com/timetracker/backend/internal/models" | 	"github.com/timetracker/backend/internal/models" | ||||||
| 	_ "gorm.io/driver/postgres" | 	_ "gorm.io/driver/postgres" | ||||||
| 	// GORM IMPORTS MARKER | 	// GORM IMPORTS MARKER | ||||||
| @ -18,7 +19,12 @@ import ( | |||||||
| //	@version					1.0 | //	@version					1.0 | ||||||
| //	@description				This is a simple time tracker API. | //	@description				This is a simple time tracker API. | ||||||
| //	@host						localhost:8080 | //	@host						localhost:8080 | ||||||
| // @BasePath / | //	@BasePath					/api | ||||||
|  | //	@securityDefinitions.apikey	BearerAuth | ||||||
|  | //	@in							header | ||||||
|  | //	@name						Authorization | ||||||
|  | 
 | ||||||
|  | //	@x-extension	ulid.ULID string | ||||||
| 
 | 
 | ||||||
| //	@Summary		Say hello | //	@Summary		Say hello | ||||||
| //	@Description	Get a hello message | //	@Description	Get a hello message | ||||||
| @ -31,25 +37,34 @@ func helloHandler(c *gin.Context) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
| 
 | 	// Configure database | ||||||
| 	dbConfig := models.DatabaseConfig{ | 	dbConfig := models.DatabaseConfig{ | ||||||
| 		Host:     "localhost", | 		Host:     "localhost", | ||||||
| 		Port:     5432, | 		Port:     5432, | ||||||
| 		User:     "postgres", | 		User:     "postgres", | ||||||
| 		Password: "password", | 		Password: "password", | ||||||
| 		DBName:   "mydatabase", | 		DBName:   "mydatabase", | ||||||
| 		SSLMode:  "disable", // Für Entwicklungsumgebung | 		SSLMode:  "disable", // For development environment | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Datenbank initialisieren | 	// Initialize database | ||||||
| 	if err := models.InitDB(dbConfig); err != nil { | 	if err := models.InitDB(dbConfig); err != nil { | ||||||
| 		log.Fatalf("Fehler bei der DB-Initialisierung: %v", err) | 		log.Fatalf("Error initializing database: %v", err) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create Gin router | ||||||
| 	r := gin.Default() | 	r := gin.Default() | ||||||
| 
 | 
 | ||||||
|  | 	// Basic route for health check | ||||||
| 	r.GET("/", helloHandler) | 	r.GET("/", helloHandler) | ||||||
|  | 
 | ||||||
|  | 	// Setup API routes | ||||||
|  | 	routes.SetupRouter(r) | ||||||
|  | 
 | ||||||
|  | 	// Swagger documentation | ||||||
| 	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) | 	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) | ||||||
| 
 | 
 | ||||||
|  | 	// Start server | ||||||
| 	fmt.Println("Server listening on port 8080") | 	fmt.Println("Server listening on port 8080") | ||||||
| 	r.Run(":8080") // Use Gin's Run method | 	r.Run(":8080") | ||||||
| } | } | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ require ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
|  | 	github.com/golang-jwt/jwt/v5 v5.2.1 | ||||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||||
| 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect | 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect | ||||||
| 	github.com/jackc/pgx/v5 v5.5.5 // indirect | 	github.com/jackc/pgx/v5 v5.5.5 // indirect | ||||||
|  | |||||||
| @ -37,6 +37,8 @@ github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0 | |||||||
| github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= | github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= | ||||||
| github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | ||||||
| github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||||
|  | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= | ||||||
|  | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
|  | |||||||
							
								
								
									
										247
									
								
								backend/internal/api/handlers/activity_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								backend/internal/api/handlers/activity_handler.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,247 @@ | |||||||
|  | package handlers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/oklog/ulid/v2" | ||||||
|  | 	"github.com/timetracker/backend/internal/api/utils" | ||||||
|  | 	dto "github.com/timetracker/backend/internal/dtos" | ||||||
|  | 	"github.com/timetracker/backend/internal/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ActivityHandler handles activity-related API endpoints | ||||||
|  | type ActivityHandler struct{} | ||||||
|  | 
 | ||||||
|  | // NewActivityHandler creates a new ActivityHandler | ||||||
|  | func NewActivityHandler() *ActivityHandler { | ||||||
|  | 	return &ActivityHandler{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetActivities handles GET /activities | ||||||
|  | //	@Summary		Get all activities | ||||||
|  | //	@Description	Get a list of all activities | ||||||
|  | //	@Tags			activities | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Success		200	{object}	utils.Response{data=[]utils.ActivityResponse} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/activities [get] | ||||||
|  | func (h *ActivityHandler) GetActivities(c *gin.Context) { | ||||||
|  | 	// Get activities from the database | ||||||
|  | 	activities, err := models.GetAllActivities(c.Request.Context()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error retrieving activities: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTOs | ||||||
|  | 	activityDTOs := make([]dto.ActivityDto, len(activities)) | ||||||
|  | 	for i, activity := range activities { | ||||||
|  | 		activityDTOs[i] = convertActivityToDTO(&activity) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, activityDTOs) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetActivityByID handles GET /activities/:id | ||||||
|  | //	@Summary		Get activity by ID | ||||||
|  | //	@Description	Get an activity by its ID | ||||||
|  | //	@Tags			activities | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			id	path		string	true	"Activity ID" | ||||||
|  | //	@Success		200	{object}	utils.Response{data=utils.ActivityResponse} | ||||||
|  | //	@Failure		400	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		404	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/activities/{id} [get] | ||||||
|  | func (h *ActivityHandler) GetActivityByID(c *gin.Context) { | ||||||
|  | 	// Parse ID from URL | ||||||
|  | 	idStr := c.Param("id") | ||||||
|  | 	id, err := ulid.Parse(idStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid activity ID format") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get activity from the database | ||||||
|  | 	activity, err := models.GetActivityByID(c.Request.Context(), id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error retrieving activity: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if activity == nil { | ||||||
|  | 		utils.NotFoundResponse(c, "Activity not found") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	activityDTO := convertActivityToDTO(activity) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, activityDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CreateActivity handles POST /activities | ||||||
|  | //	@Summary		Create a new activity | ||||||
|  | //	@Description	Create a new activity | ||||||
|  | //	@Tags			activities | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			activity	body		dto.ActivityCreateDto	true	"Activity data" | ||||||
|  | //	@Success		201			{object}	utils.Response{data=dto.ActivityDto} | ||||||
|  | //	@Failure		400			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/activities [post] | ||||||
|  | func (h *ActivityHandler) CreateActivity(c *gin.Context) { | ||||||
|  | 	// Parse request body | ||||||
|  | 	var activityCreateDTO dto.ActivityCreateDto | ||||||
|  | 	if err := c.ShouldBindJSON(&activityCreateDTO); err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert DTO to model | ||||||
|  | 	activityCreate := convertCreateActivityDTOToModel(activityCreateDTO) | ||||||
|  | 
 | ||||||
|  | 	// Create activity in the database | ||||||
|  | 	activity, err := models.CreateActivity(c.Request.Context(), activityCreate) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error creating activity: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	activityDTO := convertActivityToDTO(activity) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusCreated, activityDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UpdateActivity handles PUT /activities/:id | ||||||
|  | //	@Summary		Update an activity | ||||||
|  | //	@Description	Update an existing activity | ||||||
|  | //	@Tags			activities | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			id			path		string					true	"Activity ID" | ||||||
|  | //	@Param			activity	body		dto.ActivityUpdateDto	true	"Activity data" | ||||||
|  | //	@Success		200			{object}	utils.Response{data=dto.ActivityDto} | ||||||
|  | //	@Failure		400			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		404			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/activities/{id} [put] | ||||||
|  | func (h *ActivityHandler) UpdateActivity(c *gin.Context) { | ||||||
|  | 	// Parse ID from URL | ||||||
|  | 	idStr := c.Param("id") | ||||||
|  | 	id, err := ulid.Parse(idStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid activity ID format") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Parse request body | ||||||
|  | 	var activityUpdateDTO dto.ActivityUpdateDto | ||||||
|  | 	if err := c.ShouldBindJSON(&activityUpdateDTO); err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Set ID from URL | ||||||
|  | 	activityUpdateDTO.ID = id | ||||||
|  | 
 | ||||||
|  | 	// Convert DTO to model | ||||||
|  | 	activityUpdate := convertUpdateActivityDTOToModel(activityUpdateDTO) | ||||||
|  | 
 | ||||||
|  | 	// Update activity in the database | ||||||
|  | 	activity, err := models.UpdateActivity(c.Request.Context(), activityUpdate) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error updating activity: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if activity == nil { | ||||||
|  | 		utils.NotFoundResponse(c, "Activity not found") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	activityDTO := convertActivityToDTO(activity) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, activityDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DeleteActivity handles DELETE /activities/:id | ||||||
|  | //	@Summary		Delete an activity | ||||||
|  | //	@Description	Delete an activity by its ID | ||||||
|  | //	@Tags			activities | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			id	path		string	true	"Activity ID" | ||||||
|  | //	@Success		204	{object}	utils.Response | ||||||
|  | //	@Failure		400	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/activities/{id} [delete] | ||||||
|  | func (h *ActivityHandler) DeleteActivity(c *gin.Context) { | ||||||
|  | 	// Parse ID from URL | ||||||
|  | 	idStr := c.Param("id") | ||||||
|  | 	id, err := ulid.Parse(idStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid activity ID format") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Delete activity from the database | ||||||
|  | 	err = models.DeleteActivity(c.Request.Context(), id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error deleting activity: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusNoContent, nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Helper functions for DTO conversion | ||||||
|  | 
 | ||||||
|  | func convertActivityToDTO(activity *models.Activity) dto.ActivityDto { | ||||||
|  | 	return dto.ActivityDto{ | ||||||
|  | 		ID:          activity.ID, | ||||||
|  | 		CreatedAt:   activity.CreatedAt, | ||||||
|  | 		UpdatedAt:   activity.UpdatedAt, | ||||||
|  | 		Name:        activity.Name, | ||||||
|  | 		BillingRate: activity.BillingRate, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func convertCreateActivityDTOToModel(dto dto.ActivityCreateDto) models.ActivityCreate { | ||||||
|  | 	return models.ActivityCreate{ | ||||||
|  | 		Name:        dto.Name, | ||||||
|  | 		BillingRate: dto.BillingRate, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func convertUpdateActivityDTOToModel(dto dto.ActivityUpdateDto) models.ActivityUpdate { | ||||||
|  | 	update := models.ActivityUpdate{ | ||||||
|  | 		ID: dto.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.Name != nil { | ||||||
|  | 		update.Name = dto.Name | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.BillingRate != nil { | ||||||
|  | 		update.BillingRate = dto.BillingRate | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return update | ||||||
|  | } | ||||||
							
								
								
									
										350
									
								
								backend/internal/api/handlers/user_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								backend/internal/api/handlers/user_handler.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,350 @@ | |||||||
|  | package handlers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/oklog/ulid/v2" | ||||||
|  | 	"github.com/timetracker/backend/internal/api/middleware" | ||||||
|  | 	"github.com/timetracker/backend/internal/api/utils" | ||||||
|  | 	dto "github.com/timetracker/backend/internal/dtos" | ||||||
|  | 	"github.com/timetracker/backend/internal/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // UserHandler handles user-related API endpoints | ||||||
|  | type UserHandler struct{} | ||||||
|  | 
 | ||||||
|  | // NewUserHandler creates a new UserHandler | ||||||
|  | func NewUserHandler() *UserHandler { | ||||||
|  | 	return &UserHandler{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUsers handles GET /users | ||||||
|  | //	@Summary		Get all users | ||||||
|  | //	@Description	Get a list of all users | ||||||
|  | //	@Tags			users | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Success		200	{object}	utils.Response{data=[]dto.UserDto} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/users [get] | ||||||
|  | func (h *UserHandler) GetUsers(c *gin.Context) { | ||||||
|  | 	// Get users from the database | ||||||
|  | 	users, err := models.GetAllUsers(c.Request.Context()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error retrieving users: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTOs | ||||||
|  | 	userDTOs := make([]dto.UserDto, len(users)) | ||||||
|  | 	for i, user := range users { | ||||||
|  | 		userDTOs[i] = convertUserToDTO(&user) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, userDTOs) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUserByID handles GET /users/:id | ||||||
|  | //	@Summary		Get user by ID | ||||||
|  | //	@Description	Get a user by their ID | ||||||
|  | //	@Tags			users | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			id	path		string	true	"User ID" | ||||||
|  | //	@Success		200	{object}	utils.Response{data=dto.UserDto} | ||||||
|  | //	@Failure		400	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		404	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/users/{id} [get] | ||||||
|  | func (h *UserHandler) GetUserByID(c *gin.Context) { | ||||||
|  | 	// Parse ID from URL | ||||||
|  | 	idStr := c.Param("id") | ||||||
|  | 	id, err := ulid.Parse(idStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid user ID format") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get user from the database | ||||||
|  | 	user, err := models.GetUserByID(c.Request.Context(), id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error retrieving user: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if user == nil { | ||||||
|  | 		utils.NotFoundResponse(c, "User not found") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	userDTO := convertUserToDTO(user) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, userDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CreateUser handles POST /users | ||||||
|  | //	@Summary		Create a new user | ||||||
|  | //	@Description	Create a new user | ||||||
|  | //	@Tags			users | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			user	body		dto.UserCreateDto	true	"User data" | ||||||
|  | //	@Success		201		{object}	utils.Response{data=dto.UserDto} | ||||||
|  | //	@Failure		400		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/users [post] | ||||||
|  | func (h *UserHandler) CreateUser(c *gin.Context) { | ||||||
|  | 	// Parse request body | ||||||
|  | 	var userCreateDTO dto.UserCreateDto | ||||||
|  | 	if err := c.ShouldBindJSON(&userCreateDTO); err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert DTO to model | ||||||
|  | 	userCreate := convertCreateDTOToModel(userCreateDTO) | ||||||
|  | 
 | ||||||
|  | 	// Create user in the database | ||||||
|  | 	user, err := models.CreateUser(c.Request.Context(), userCreate) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error creating user: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	userDTO := convertUserToDTO(user) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusCreated, userDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UpdateUser handles PUT /users/:id | ||||||
|  | //	@Summary		Update a user | ||||||
|  | //	@Description	Update an existing user | ||||||
|  | //	@Tags			users | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			id		path		string				true	"User ID" | ||||||
|  | //	@Param			user	body		dto.UserUpdateDto	true	"User data" | ||||||
|  | //	@Success		200		{object}	utils.Response{data=dto.UserDto} | ||||||
|  | //	@Failure		400		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		404		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500		{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/users/{id} [put] | ||||||
|  | func (h *UserHandler) UpdateUser(c *gin.Context) { | ||||||
|  | 	// Parse ID from URL | ||||||
|  | 	idStr := c.Param("id") | ||||||
|  | 	id, err := ulid.Parse(idStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid user ID format") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Parse request body | ||||||
|  | 	var userUpdateDTO dto.UserUpdateDto | ||||||
|  | 	if err := c.ShouldBindJSON(&userUpdateDTO); err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Set ID from URL | ||||||
|  | 	userUpdateDTO.ID = id | ||||||
|  | 
 | ||||||
|  | 	// Convert DTO to model | ||||||
|  | 	userUpdate := convertUpdateDTOToModel(userUpdateDTO) | ||||||
|  | 
 | ||||||
|  | 	// Update user in the database | ||||||
|  | 	user, err := models.UpdateUser(c.Request.Context(), userUpdate) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error updating user: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if user == nil { | ||||||
|  | 		utils.NotFoundResponse(c, "User not found") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	userDTO := convertUserToDTO(user) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, userDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DeleteUser handles DELETE /users/:id | ||||||
|  | //	@Summary		Delete a user | ||||||
|  | //	@Description	Delete a user by their ID | ||||||
|  | //	@Tags			users | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Param			id	path		string	true	"User ID" | ||||||
|  | //	@Success		204	{object}	utils.Response | ||||||
|  | //	@Failure		400	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/users/{id} [delete] | ||||||
|  | func (h *UserHandler) DeleteUser(c *gin.Context) { | ||||||
|  | 	// Parse ID from URL | ||||||
|  | 	idStr := c.Param("id") | ||||||
|  | 	id, err := ulid.Parse(idStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid user ID format") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Delete user from the database | ||||||
|  | 	err = models.DeleteUser(c.Request.Context(), id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error deleting user: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusNoContent, nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Login handles POST /auth/login | ||||||
|  | //	@Summary		Login | ||||||
|  | //	@Description	Authenticate a user and get a JWT token | ||||||
|  | //	@Tags			auth | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Param			credentials	body		dto.LoginDto	true	"Login credentials" | ||||||
|  | //	@Success		200			{object}	utils.Response{data=dto.TokenDto} | ||||||
|  | //	@Failure		400			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		401			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500			{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/auth/login [post] | ||||||
|  | func (h *UserHandler) Login(c *gin.Context) { | ||||||
|  | 	// Parse request body | ||||||
|  | 	var loginDTO dto.LoginDto | ||||||
|  | 	if err := c.ShouldBindJSON(&loginDTO); err != nil { | ||||||
|  | 		utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Authenticate user | ||||||
|  | 	user, err := models.AuthenticateUser(c.Request.Context(), loginDTO.Email, loginDTO.Password) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.UnauthorizedResponse(c, "Invalid login credentials") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Generate JWT token | ||||||
|  | 	token, err := middleware.GenerateToken(user) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error generating token: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Return token | ||||||
|  | 	tokenDTO := dto.TokenDto{ | ||||||
|  | 		Token: token, | ||||||
|  | 		User:  convertUserToDTO(user), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, tokenDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetCurrentUser handles GET /auth/me | ||||||
|  | //	@Summary		Get current user | ||||||
|  | //	@Description	Get the currently authenticated user | ||||||
|  | //	@Tags			auth | ||||||
|  | //	@Accept			json | ||||||
|  | //	@Produce		json | ||||||
|  | //	@Security		BearerAuth | ||||||
|  | //	@Success		200	{object}	utils.Response{data=dto.UserDto} | ||||||
|  | //	@Failure		401	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Failure		500	{object}	utils.Response{error=utils.ErrorInfo} | ||||||
|  | //	@Router			/auth/me [get] | ||||||
|  | func (h *UserHandler) GetCurrentUser(c *gin.Context) { | ||||||
|  | 	// Get user ID from context (set by AuthMiddleware) | ||||||
|  | 	userID, err := middleware.GetUserIDFromContext(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.UnauthorizedResponse(c, "User not authenticated") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get user from the database | ||||||
|  | 	user, err := models.GetUserByID(c.Request.Context(), userID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.InternalErrorResponse(c, "Error retrieving user: "+err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if user == nil { | ||||||
|  | 		utils.NotFoundResponse(c, "User not found") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Convert to DTO | ||||||
|  | 	userDTO := convertUserToDTO(user) | ||||||
|  | 
 | ||||||
|  | 	utils.SuccessResponse(c, http.StatusOK, userDTO) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Helper functions for DTO conversion | ||||||
|  | 
 | ||||||
|  | func convertUserToDTO(user *models.User) dto.UserDto { | ||||||
|  | 	return dto.UserDto{ | ||||||
|  | 		ID:         user.ID, | ||||||
|  | 		CreatedAt:  user.CreatedAt, | ||||||
|  | 		UpdatedAt:  user.UpdatedAt, | ||||||
|  | 		Email:      user.Email, | ||||||
|  | 		Role:       user.Role, | ||||||
|  | 		CompanyID:  int(user.CompanyID.Time()), // This is a simplification, adjust as needed | ||||||
|  | 		HourlyRate: user.HourlyRate, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate { | ||||||
|  | 	// Convert CompanyID from int to ULID (this is a simplification, adjust as needed) | ||||||
|  | 	companyID, _ := ulid.Parse("01H1VECTJQXS1RVWJT6QG3QJCJ") | ||||||
|  | 
 | ||||||
|  | 	return models.UserCreate{ | ||||||
|  | 		Email:      dto.Email, | ||||||
|  | 		Password:   dto.Password, | ||||||
|  | 		Role:       dto.Role, | ||||||
|  | 		CompanyID:  companyID, | ||||||
|  | 		HourlyRate: dto.HourlyRate, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func convertUpdateDTOToModel(dto dto.UserUpdateDto) models.UserUpdate { | ||||||
|  | 	update := models.UserUpdate{ | ||||||
|  | 		ID: dto.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.Email != nil { | ||||||
|  | 		update.Email = dto.Email | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.Password != nil { | ||||||
|  | 		update.Password = dto.Password | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.Role != nil { | ||||||
|  | 		update.Role = dto.Role | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.CompanyID != nil { | ||||||
|  | 		// Convert CompanyID from int to ULID (this is a simplification, adjust as needed) | ||||||
|  | 		companyID, _ := ulid.Parse("01H1VECTJQXS1RVWJT6QG3QJCJ") | ||||||
|  | 		update.CompanyID = &companyID | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dto.HourlyRate != nil { | ||||||
|  | 		update.HourlyRate = dto.HourlyRate | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return update | ||||||
|  | } | ||||||
							
								
								
									
										198
									
								
								backend/internal/api/middleware/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								backend/internal/api/middleware/auth.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,198 @@ | |||||||
|  | package middleware | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/golang-jwt/jwt/v5" | ||||||
|  | 	"github.com/oklog/ulid/v2" | ||||||
|  | 	"github.com/timetracker/backend/internal/api/utils" | ||||||
|  | 	"github.com/timetracker/backend/internal/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // JWT configuration | ||||||
|  | const ( | ||||||
|  | 	// This should be moved to environment variables in production | ||||||
|  | 	jwtSecret     = "your-secret-key-change-in-production" | ||||||
|  | 	tokenDuration = 24 * time.Hour | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Claims represents the JWT claims | ||||||
|  | type Claims struct { | ||||||
|  | 	UserID    string `json:"userId"` | ||||||
|  | 	Email     string `json:"email"` | ||||||
|  | 	Role      string `json:"role"` | ||||||
|  | 	CompanyID string `json:"companyId"` | ||||||
|  | 	jwt.RegisteredClaims | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AuthMiddleware checks if the user is authenticated | ||||||
|  | func AuthMiddleware() gin.HandlerFunc { | ||||||
|  | 	return func(c *gin.Context) { | ||||||
|  | 		// Get the Authorization header | ||||||
|  | 		authHeader := c.GetHeader("Authorization") | ||||||
|  | 		if authHeader == "" { | ||||||
|  | 			utils.UnauthorizedResponse(c, "Authorization header is required") | ||||||
|  | 			c.Abort() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Check if the header has the Bearer prefix | ||||||
|  | 		parts := strings.Split(authHeader, " ") | ||||||
|  | 		if len(parts) != 2 || parts[0] != "Bearer" { | ||||||
|  | 			utils.UnauthorizedResponse(c, "Invalid authorization format, expected 'Bearer TOKEN'") | ||||||
|  | 			c.Abort() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		tokenString := parts[1] | ||||||
|  | 		claims, err := validateToken(tokenString) | ||||||
|  | 		if err != nil { | ||||||
|  | 			utils.UnauthorizedResponse(c, "Invalid or expired token") | ||||||
|  | 			c.Abort() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Store user information in the context | ||||||
|  | 		c.Set("userID", claims.UserID) | ||||||
|  | 		c.Set("email", claims.Email) | ||||||
|  | 		c.Set("role", claims.Role) | ||||||
|  | 		c.Set("companyID", claims.CompanyID) | ||||||
|  | 
 | ||||||
|  | 		c.Next() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RoleMiddleware checks if the user has the required role | ||||||
|  | func RoleMiddleware(roles ...string) gin.HandlerFunc { | ||||||
|  | 	return func(c *gin.Context) { | ||||||
|  | 		userRole, exists := c.Get("role") | ||||||
|  | 		if !exists { | ||||||
|  | 			utils.UnauthorizedResponse(c, "User role not found in context") | ||||||
|  | 			c.Abort() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Check if the user's role is in the allowed roles | ||||||
|  | 		roleStr, ok := userRole.(string) | ||||||
|  | 		if !ok { | ||||||
|  | 			utils.InternalErrorResponse(c, "Invalid role type in context") | ||||||
|  | 			c.Abort() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		allowed := false | ||||||
|  | 		for _, role := range roles { | ||||||
|  | 			if roleStr == role { | ||||||
|  | 				allowed = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if !allowed { | ||||||
|  | 			utils.ForbiddenResponse(c, "Insufficient permissions") | ||||||
|  | 			c.Abort() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		c.Next() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GenerateToken creates a new JWT token for a user | ||||||
|  | func GenerateToken(user *models.User) (string, error) { | ||||||
|  | 	// Create the claims | ||||||
|  | 	claims := Claims{ | ||||||
|  | 		UserID:    user.ID.String(), | ||||||
|  | 		Email:     user.Email, | ||||||
|  | 		Role:      user.Role, | ||||||
|  | 		CompanyID: user.CompanyID.String(), | ||||||
|  | 		RegisteredClaims: jwt.RegisteredClaims{ | ||||||
|  | 			ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenDuration)), | ||||||
|  | 			IssuedAt:  jwt.NewNumericDate(time.Now()), | ||||||
|  | 			NotBefore: jwt.NewNumericDate(time.Now()), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create the token | ||||||
|  | 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||||||
|  | 
 | ||||||
|  | 	// Sign the token | ||||||
|  | 	tokenString, err := token.SignedString([]byte(jwtSecret)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return tokenString, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // validateToken validates a JWT token and returns the claims | ||||||
|  | func validateToken(tokenString string) (*Claims, error) { | ||||||
|  | 	// Parse the token | ||||||
|  | 	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { | ||||||
|  | 		// Validate the signing method | ||||||
|  | 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||||||
|  | 			return nil, errors.New("unexpected signing method") | ||||||
|  | 		} | ||||||
|  | 		return []byte(jwtSecret), nil | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check if the token is valid | ||||||
|  | 	if !token.Valid { | ||||||
|  | 		return nil, errors.New("invalid token") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Get the claims | ||||||
|  | 	claims, ok := token.Claims.(*Claims) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, errors.New("invalid claims") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return claims, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUserIDFromContext extracts the user ID from the context | ||||||
|  | func GetUserIDFromContext(c *gin.Context) (ulid.ULID, error) { | ||||||
|  | 	userID, exists := c.Get("userID") | ||||||
|  | 	if !exists { | ||||||
|  | 		return ulid.ULID{}, errors.New("user ID not found in context") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	userIDStr, ok := userID.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return ulid.ULID{}, errors.New("invalid user ID type in context") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	id, err := ulid.Parse(userIDStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return ulid.ULID{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return id, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetCompanyIDFromContext extracts the company ID from the context | ||||||
|  | func GetCompanyIDFromContext(c *gin.Context) (ulid.ULID, error) { | ||||||
|  | 	companyID, exists := c.Get("companyID") | ||||||
|  | 	if !exists { | ||||||
|  | 		return ulid.ULID{}, errors.New("company ID not found in context") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	companyIDStr, ok := companyID.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return ulid.ULID{}, errors.New("invalid company ID type in context") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	id, err := ulid.Parse(companyIDStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return ulid.ULID{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return id, nil | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								backend/internal/api/routes/router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								backend/internal/api/routes/router.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | |||||||
|  | package routes | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/timetracker/backend/internal/api/handlers" | ||||||
|  | 	"github.com/timetracker/backend/internal/api/middleware" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // SetupRouter configures all the routes for the API | ||||||
|  | func SetupRouter(r *gin.Engine) { | ||||||
|  | 	// Create handlers | ||||||
|  | 	userHandler := handlers.NewUserHandler() | ||||||
|  | 	activityHandler := handlers.NewActivityHandler() | ||||||
|  | 
 | ||||||
|  | 	// Public routes | ||||||
|  | 	r.POST("/auth/login", userHandler.Login) | ||||||
|  | 
 | ||||||
|  | 	// API routes (protected) | ||||||
|  | 	api := r.Group("/api") | ||||||
|  | 	api.Use(middleware.AuthMiddleware()) | ||||||
|  | 	{ | ||||||
|  | 		// Auth routes | ||||||
|  | 		auth := api.Group("/auth") | ||||||
|  | 		{ | ||||||
|  | 			auth.GET("/me", userHandler.GetCurrentUser) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// User routes | ||||||
|  | 		users := api.Group("/users") | ||||||
|  | 		{ | ||||||
|  | 			users.GET("", userHandler.GetUsers) | ||||||
|  | 			users.GET("/:id", userHandler.GetUserByID) | ||||||
|  | 			users.POST("", middleware.RoleMiddleware("admin"), userHandler.CreateUser) | ||||||
|  | 			users.PUT("/:id", middleware.RoleMiddleware("admin"), userHandler.UpdateUser) | ||||||
|  | 			users.DELETE("/:id", middleware.RoleMiddleware("admin"), userHandler.DeleteUser) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Activity routes | ||||||
|  | 		activities := api.Group("/activities") | ||||||
|  | 		{ | ||||||
|  | 			activities.GET("", activityHandler.GetActivities) | ||||||
|  | 			activities.GET("/:id", activityHandler.GetActivityByID) | ||||||
|  | 			activities.POST("", middleware.RoleMiddleware("admin"), activityHandler.CreateActivity) | ||||||
|  | 			activities.PUT("/:id", middleware.RoleMiddleware("admin"), activityHandler.UpdateActivity) | ||||||
|  | 			activities.DELETE("/:id", middleware.RoleMiddleware("admin"), activityHandler.DeleteActivity) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// TODO: Add routes for other entities (Company, Project, TimeEntry, etc.) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										85
									
								
								backend/internal/api/utils/response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								backend/internal/api/utils/response.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | |||||||
|  | package utils | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Response is a standardized API response structure | ||||||
|  | type Response struct { | ||||||
|  | 	Success bool        `json:"success"` | ||||||
|  | 	Data    interface{} `json:"data,omitempty"` | ||||||
|  | 	Error   *ErrorInfo  `json:"error,omitempty"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrorInfo contains detailed error information | ||||||
|  | type ErrorInfo struct { | ||||||
|  | 	Code    string `json:"code"` | ||||||
|  | 	Message string `json:"message"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrorResponse codes | ||||||
|  | const ( | ||||||
|  | 	ErrorCodeValidation   = "VALIDATION_ERROR" | ||||||
|  | 	ErrorCodeNotFound     = "NOT_FOUND" | ||||||
|  | 	ErrorCodeUnauthorized = "UNAUTHORIZED" | ||||||
|  | 	ErrorCodeForbidden    = "FORBIDDEN" | ||||||
|  | 	ErrorCodeInternal     = "INTERNAL_ERROR" | ||||||
|  | 	ErrorCodeBadRequest   = "BAD_REQUEST" | ||||||
|  | 	ErrorCodeConflict     = "CONFLICT" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // SuccessResponse sends a successful response with data | ||||||
|  | func SuccessResponse(c *gin.Context, statusCode int, data interface{}) { | ||||||
|  | 	c.JSON(statusCode, Response{ | ||||||
|  | 		Success: true, | ||||||
|  | 		Data:    data, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrorResponse sends an error response | ||||||
|  | func ErrorResponse(c *gin.Context, statusCode int, errorCode string, message string) { | ||||||
|  | 	c.JSON(statusCode, Response{ | ||||||
|  | 		Success: false, | ||||||
|  | 		Error: &ErrorInfo{ | ||||||
|  | 			Code:    errorCode, | ||||||
|  | 			Message: message, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // BadRequestResponse sends a 400 Bad Request response | ||||||
|  | func BadRequestResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusBadRequest, ErrorCodeBadRequest, message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ValidationErrorResponse sends a 400 Bad Request response for validation errors | ||||||
|  | func ValidationErrorResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusBadRequest, ErrorCodeValidation, message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NotFoundResponse sends a 404 Not Found response | ||||||
|  | func NotFoundResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusNotFound, ErrorCodeNotFound, message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UnauthorizedResponse sends a 401 Unauthorized response | ||||||
|  | func UnauthorizedResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusUnauthorized, ErrorCodeUnauthorized, message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ForbiddenResponse sends a 403 Forbidden response | ||||||
|  | func ForbiddenResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusForbidden, ErrorCodeForbidden, message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // InternalErrorResponse sends a 500 Internal Server Error response | ||||||
|  | func InternalErrorResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusInternalServerError, ErrorCodeInternal, message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ConflictResponse sends a 409 Conflict response | ||||||
|  | func ConflictResponse(c *gin.Context, message string) { | ||||||
|  | 	ErrorResponse(c, http.StatusConflict, ErrorCodeConflict, message) | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								backend/internal/api/utils/swagger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								backend/internal/api/utils/swagger.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | |||||||
|  | package utils | ||||||
|  | 
 | ||||||
|  | // This file contains type definitions for Swagger documentation | ||||||
|  | 
 | ||||||
|  | // SwaggerULID is a string representation of ULID for Swagger | ||||||
|  | type SwaggerULID string | ||||||
|  | 
 | ||||||
|  | // SwaggerTime is a string representation of time.Time for Swagger | ||||||
|  | type SwaggerTime string | ||||||
|  | 
 | ||||||
|  | // ActivityResponse is a Swagger representation of ActivityDto | ||||||
|  | type ActivityResponse struct { | ||||||
|  | 	ID          SwaggerULID `json:"id" example:"01H1VECTJQXS1RVWJT6QG3QJCJ"` | ||||||
|  | 	CreatedAt   SwaggerTime `json:"createdAt" example:"2023-01-01T12:00:00Z"` | ||||||
|  | 	UpdatedAt   SwaggerTime `json:"updatedAt" example:"2023-01-01T12:00:00Z"` | ||||||
|  | 	Name        string      `json:"name" example:"Development"` | ||||||
|  | 	BillingRate float64     `json:"billingRate" example:"100.0"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UserResponse is a Swagger representation of UserDto | ||||||
|  | type UserResponse struct { | ||||||
|  | 	ID         SwaggerULID `json:"id" example:"01H1VECTJQXS1RVWJT6QG3QJCJ"` | ||||||
|  | 	CreatedAt  SwaggerTime `json:"createdAt" example:"2023-01-01T12:00:00Z"` | ||||||
|  | 	UpdatedAt  SwaggerTime `json:"updatedAt" example:"2023-01-01T12:00:00Z"` | ||||||
|  | 	Email      string      `json:"email" example:"user@example.com"` | ||||||
|  | 	Role       string      `json:"role" example:"admin"` | ||||||
|  | 	CompanyID  int         `json:"companyId" example:"1"` | ||||||
|  | 	HourlyRate float64     `json:"hourlyRate" example:"50.0"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ActivityCreateRequest is a Swagger representation of ActivityCreateDto | ||||||
|  | type ActivityCreateRequest struct { | ||||||
|  | 	Name        string  `json:"name" example:"Development"` | ||||||
|  | 	BillingRate float64 `json:"billingRate" example:"100.0"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ActivityUpdateRequest is a Swagger representation of ActivityUpdateDto | ||||||
|  | type ActivityUpdateRequest struct { | ||||||
|  | 	Name        *string  `json:"name,omitempty" example:"Development"` | ||||||
|  | 	BillingRate *float64 `json:"billingRate,omitempty" example:"100.0"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UserCreateRequest is a Swagger representation of UserCreateDto | ||||||
|  | type UserCreateRequest struct { | ||||||
|  | 	Email      string  `json:"email" example:"user@example.com"` | ||||||
|  | 	Password   string  `json:"password" example:"SecurePassword123!"` | ||||||
|  | 	Role       string  `json:"role" example:"admin"` | ||||||
|  | 	CompanyID  int     `json:"companyId" example:"1"` | ||||||
|  | 	HourlyRate float64 `json:"hourlyRate" example:"50.0"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UserUpdateRequest is a Swagger representation of UserUpdateDto | ||||||
|  | type UserUpdateRequest struct { | ||||||
|  | 	Email      *string  `json:"email,omitempty" example:"user@example.com"` | ||||||
|  | 	Password   *string  `json:"password,omitempty" example:"SecurePassword123!"` | ||||||
|  | 	Role       *string  `json:"role,omitempty" example:"admin"` | ||||||
|  | 	CompanyID  *int     `json:"companyId,omitempty" example:"1"` | ||||||
|  | 	HourlyRate *float64 `json:"hourlyRate,omitempty" example:"50.0"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LoginRequest is a Swagger representation of LoginDto | ||||||
|  | type LoginRequest struct { | ||||||
|  | 	Email    string `json:"email" example:"user@example.com"` | ||||||
|  | 	Password string `json:"password" example:"SecurePassword123!"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TokenResponse is a Swagger representation of TokenDto | ||||||
|  | type TokenResponse struct { | ||||||
|  | 	Token string       `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` | ||||||
|  | 	User  UserResponse `json:"user"` | ||||||
|  | } | ||||||
| @ -1,6 +1,13 @@ | |||||||
| package dto | package dto | ||||||
| 
 | 
 | ||||||
| type AuthDto struct { | // LoginDto represents the login request | ||||||
|  | type LoginDto struct { | ||||||
| 	Email    string `json:"email"` | 	Email    string `json:"email"` | ||||||
| 	Password string `json:"password"` | 	Password string `json:"password"` | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // TokenDto represents the response after successful authentication | ||||||
|  | type TokenDto struct { | ||||||
|  | 	Token string  `json:"token"` | ||||||
|  | 	User  UserDto `json:"user"` | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user