feat: Add authentication DTOs and setup API routes for user and activity management
This commit is contained in:
		
							parent
							
								
									aa5c7e77fc
								
							
						
					
					
						commit
						558ee70c21
					
				| @ -9,47 +9,62 @@ import ( | ||||
| 	swaggerFiles "github.com/swaggo/files" | ||||
| 	ginSwagger "github.com/swaggo/gin-swagger" | ||||
| 	_ "github.com/timetracker/backend/docs" // This line is important for swag to work | ||||
| 	"github.com/timetracker/backend/internal/api/routes" | ||||
| 	"github.com/timetracker/backend/internal/models" | ||||
| 	_ "gorm.io/driver/postgres" | ||||
| 	// GORM IMPORTS MARKER | ||||
| ) | ||||
| 
 | ||||
| // @title Time Tracker API | ||||
| // @version 1.0 | ||||
| // @description This is a simple time tracker API. | ||||
| // @host localhost:8080 | ||||
| // @BasePath / | ||||
| //	@title						Time Tracker API | ||||
| //	@version					1.0 | ||||
| //	@description				This is a simple time tracker API. | ||||
| //	@host						localhost:8080 | ||||
| //	@BasePath					/api | ||||
| //	@securityDefinitions.apikey	BearerAuth | ||||
| //	@in							header | ||||
| //	@name						Authorization | ||||
| 
 | ||||
| // @Summary Say hello | ||||
| // @Description Get a hello message | ||||
| // @ID hello | ||||
| // @Produce  plain | ||||
| // @Success 200 {string} string "Hello from the Time Tracker Backend!" | ||||
| // @Router / [get] | ||||
| //	@x-extension	ulid.ULID string | ||||
| 
 | ||||
| //	@Summary		Say hello | ||||
| //	@Description	Get a hello message | ||||
| //	@ID				hello | ||||
| //	@Produce		plain | ||||
| //	@Success		200	{string}	string	"Hello from the Time Tracker Backend!" | ||||
| //	@Router			/ [get] | ||||
| func helloHandler(c *gin.Context) { | ||||
| 	c.String(http.StatusOK, "Hello from the Time Tracker Backend!") | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 
 | ||||
| 	// Configure database | ||||
| 	dbConfig := models.DatabaseConfig{ | ||||
| 		Host:     "localhost", | ||||
| 		Port:     5432, | ||||
| 		User:     "postgres", | ||||
| 		Password: "password", | ||||
| 		DBName:   "mydatabase", | ||||
| 		SSLMode:  "disable", // Für Entwicklungsumgebung | ||||
| 		SSLMode:  "disable", // For development environment | ||||
| 	} | ||||
| 
 | ||||
| 	// Datenbank initialisieren | ||||
| 	// Initialize database | ||||
| 	if err := models.InitDB(dbConfig); err != nil { | ||||
| 		log.Fatalf("Fehler bei der DB-Initialisierung: %v", err) | ||||
| 		log.Fatalf("Error initializing database: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Create Gin router | ||||
| 	r := gin.Default() | ||||
| 
 | ||||
| 	// Basic route for health check | ||||
| 	r.GET("/", helloHandler) | ||||
| 
 | ||||
| 	// Setup API routes | ||||
| 	routes.SetupRouter(r) | ||||
| 
 | ||||
| 	// Swagger documentation | ||||
| 	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) | ||||
| 
 | ||||
| 	// Start server | ||||
| 	fmt.Println("Server listening on port 8080") | ||||
| 	r.Run(":8080") // Use Gin's Run method | ||||
| 	r.Run(":8080") | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,7 @@ require ( | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/golang-jwt/jwt/v5 v5.2.1 | ||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||
| 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect | ||||
| 	github.com/jackc/pgx/v5 v5.5.5 // indirect | ||||
|  | ||||
| @ -37,6 +37,8 @@ github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0 | ||||
| github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= | ||||
| github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | ||||
| github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
|  | ||||
							
								
								
									
										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 | ||||
| 
 | ||||
| type AuthDto struct { | ||||
| // LoginDto represents the login request | ||||
| type LoginDto struct { | ||||
| 	Email    string `json:"email"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
| 
 | ||||
| // TokenDto represents the response after successful authentication | ||||
| type TokenDto struct { | ||||
| 	Token string  `json:"token"` | ||||
| 	User  UserDto `json:"user"` | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user