feat: Add authentication DTOs and setup API routes for user and activity management

This commit is contained in:
Jean Jacques Avril 2025-03-10 21:02:41 +00:00
parent aa5c7e77fc
commit 558ee70c21
10 changed files with 1043 additions and 17 deletions

View File

@ -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")
}

View File

@ -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

View File

@ -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=

View 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
}

View 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
}

View 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
}

View 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.)
}
}

View 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)
}

View 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"`
}

View File

@ -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"`
}