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