added auth service to go backend
This commit is contained in:
		
							parent
							
								
									98fb6942fe
								
							
						
					
					
						commit
						d0d88de15c
					
				@ -28,15 +28,17 @@ func main() {
 | 
			
		||||
	projectRepo := repository.NewProjectRepository(database.Projects())
 | 
			
		||||
	projectTaskRepo := repository.NewProjectTaskRepository(database.ProjectTasks())
 | 
			
		||||
	timeEntryRepo := repository.NewTimeEntryRepository(database.TimeEntries())
 | 
			
		||||
	authRepo := repository.NewInMemoryAuthRepository("secret")
 | 
			
		||||
 | 
			
		||||
	// Initialize services
 | 
			
		||||
	userService := services.NewUserService(userRepo)
 | 
			
		||||
	projectService := services.NewProjectService(projectRepo)
 | 
			
		||||
	projectTaskService := services.NewProjectTaskService(projectTaskRepo)
 | 
			
		||||
	timeEntryService := services.NewTimeEntryService(timeEntryRepo)
 | 
			
		||||
	authService := services.NewAuthService(authRepo, userRepo)
 | 
			
		||||
 | 
			
		||||
	// Initialize and start the server
 | 
			
		||||
	server := http.NewServer(cfg, userService, projectService, projectTaskService, timeEntryService)
 | 
			
		||||
	server := http.NewServer(cfg, userService, projectService, projectTaskService, timeEntryService, authService)
 | 
			
		||||
 | 
			
		||||
	fmt.Println("Starting ActaTempus server on port 8080...")
 | 
			
		||||
	if err := server.Start(); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ require (
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.23.0 // indirect
 | 
			
		||||
	github.com/goccy/go-json v0.10.4 // indirect
 | 
			
		||||
	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
 | 
			
		||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.4.0 // indirect
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,8 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL
 | 
			
		||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 | 
			
		||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
 | 
			
		||||
github.com/goccy/go-json v0.10.4/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=
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,122 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"actatempus_backend/internal/domain/app_error"
 | 
			
		||||
	"actatempus_backend/internal/domain/repository"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	E "github.com/IBM/fp-go/either"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type InMemoryAuthRepository struct {
 | 
			
		||||
	secretKey    string
 | 
			
		||||
	sessionCache map[string]string
 | 
			
		||||
	mu           sync.RWMutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewInMemoryAuthRepository(secretKey string) repository.AuthRepository {
 | 
			
		||||
	return &InMemoryAuthRepository{
 | 
			
		||||
		secretKey:    secretKey,
 | 
			
		||||
		sessionCache: make(map[string]string),
 | 
			
		||||
		mu:           sync.RWMutex{},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *InMemoryAuthRepository) GenerateToken(ctx context.Context) func(userID string) E.Either[error, string] {
 | 
			
		||||
	return func(userID string) E.Either[error, string] {
 | 
			
		||||
		token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
			"userID": userID,
 | 
			
		||||
			"exp":    time.Now().Add(24 * time.Hour).Unix(),
 | 
			
		||||
		}).SignedString([]byte(r.secretKey))
 | 
			
		||||
 | 
			
		||||
		// add token to cache
 | 
			
		||||
		r.mu.Lock()
 | 
			
		||||
		defer r.mu.Unlock() // unlock after function returns
 | 
			
		||||
		r.sessionCache[token] = userID
 | 
			
		||||
 | 
			
		||||
		return E.TryCatch(
 | 
			
		||||
			token,
 | 
			
		||||
			err,
 | 
			
		||||
			func(e error) error {
 | 
			
		||||
				return app_error.NewInternalError(fmt.Errorf("could not generate token: %w", e))
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *InMemoryAuthRepository) ValidateToken(ctx context.Context) func(token string) E.Either[error, string] {
 | 
			
		||||
	return func(token string) E.Either[error, string] {
 | 
			
		||||
		// Separate value and error generation for TryCatch
 | 
			
		||||
		userID, err := func() (string, error) {
 | 
			
		||||
			parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
 | 
			
		||||
				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 | 
			
		||||
					return nil, errors.New("invalid signing method")
 | 
			
		||||
				}
 | 
			
		||||
				return []byte(r.secretKey), nil
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
 | 
			
		||||
				userID, ok := claims["userID"].(string)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return "", app_error.NewAuthError(fmt.Sprintf("invalid token claims"))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Check the cache
 | 
			
		||||
				r.mu.RLock()
 | 
			
		||||
				defer r.mu.RUnlock()
 | 
			
		||||
				if cachedUserID, exists := r.sessionCache[token]; exists && cachedUserID == userID {
 | 
			
		||||
					return userID, nil
 | 
			
		||||
				}
 | 
			
		||||
				// print chache to see if it is working
 | 
			
		||||
				fmt.Println(r.sessionCache)
 | 
			
		||||
				return "", app_error.NewAuthError(fmt.Sprintf("token not found in cache"))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return "", app_error.NewAuthError(fmt.Sprintf("invalid token"))
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		// Use TryCatch with separated value and error
 | 
			
		||||
		return E.TryCatch(
 | 
			
		||||
			userID,
 | 
			
		||||
			err,
 | 
			
		||||
			func(e error) error {
 | 
			
		||||
				return e
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *InMemoryAuthRepository) RevokeToken(ctx context.Context) func(token string) E.Either[error, string] {
 | 
			
		||||
	return func(token string) E.Either[error, string] {
 | 
			
		||||
		// Separate value and error generation for TryCatch
 | 
			
		||||
		userID, err := func() (string, error) {
 | 
			
		||||
			r.mu.Lock()
 | 
			
		||||
			defer r.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
			if userID, exists := r.sessionCache[token]; exists {
 | 
			
		||||
				delete(r.sessionCache, token)
 | 
			
		||||
				return userID, nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return "", app_error.NewAuthError(fmt.Sprintf("token not found"))
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		// Use TryCatch with separated value and error
 | 
			
		||||
		return E.TryCatch(
 | 
			
		||||
			userID,
 | 
			
		||||
			err,
 | 
			
		||||
			func(e error) error {
 | 
			
		||||
				return e
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										118
									
								
								backend-go/internal/application/services/auth_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								backend-go/internal/application/services/auth_service.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,118 @@
 | 
			
		||||
package services
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"actatempus_backend/internal/application/services/dto"
 | 
			
		||||
	"actatempus_backend/internal/domain/app_error"
 | 
			
		||||
	"actatempus_backend/internal/domain/entities"
 | 
			
		||||
	"actatempus_backend/internal/domain/repository"
 | 
			
		||||
	"actatempus_backend/internal/infrastructure/data"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	E "github.com/IBM/fp-go/either"
 | 
			
		||||
	F "github.com/IBM/fp-go/function"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AuthService handles authentication-related HTTP requests.
 | 
			
		||||
type AuthService struct {
 | 
			
		||||
	authRepository repository.AuthRepository
 | 
			
		||||
	userRepository repository.UserRepository
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAuthService creates a new instance of AuthService.
 | 
			
		||||
func NewAuthService(authRepo repository.AuthRepository,
 | 
			
		||||
	userRepository repository.UserRepository) *AuthService {
 | 
			
		||||
	return &AuthService{
 | 
			
		||||
		authRepository: authRepo,
 | 
			
		||||
		userRepository: userRepository,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RegisterRoutes registers the authentication-related routes.
 | 
			
		||||
func (s *AuthService) RegisterRoutes(router *gin.RouterGroup) {
 | 
			
		||||
	router.POST("/login", s.Login)
 | 
			
		||||
	router.POST("/validate", s.Validate)
 | 
			
		||||
	router.POST("/revoke", s.Revoke)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login handles user login and token generation.
 | 
			
		||||
func (s *AuthService) Login(c *gin.Context) {
 | 
			
		||||
	var loginRequest dto.LoginRequestDTO
 | 
			
		||||
	if err := c.ShouldBindJSON(&loginRequest); err != nil {
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	F.Pipe4(
 | 
			
		||||
		loginRequest.Email,
 | 
			
		||||
		s.userRepository.FindByEmail(c.Request.Context()),
 | 
			
		||||
		E.Chain[error](
 | 
			
		||||
			func(user entities.User) E.Either[error, entities.User] {
 | 
			
		||||
				hashedPassword := data.GenerateSecureHash(loginRequest.Password)
 | 
			
		||||
				if user.Password != hashedPassword {
 | 
			
		||||
					return E.Left[entities.User, error](app_error.NewAuthError("Invalid password"))
 | 
			
		||||
				}
 | 
			
		||||
				return E.Right[error](user)
 | 
			
		||||
			},
 | 
			
		||||
		),
 | 
			
		||||
		E.Chain(func(user entities.User) E.Either[error, dto.TokenResponseDTO] {
 | 
			
		||||
			return F.Pipe2(
 | 
			
		||||
				user.ID,
 | 
			
		||||
				s.authRepository.GenerateToken(c.Request.Context()),
 | 
			
		||||
				E.Map[error](func(token string) dto.TokenResponseDTO {
 | 
			
		||||
					return dto.TokenResponseDTO{
 | 
			
		||||
						Token:  token,
 | 
			
		||||
						UserID: user.ID,
 | 
			
		||||
					}
 | 
			
		||||
				}),
 | 
			
		||||
			)
 | 
			
		||||
		}),
 | 
			
		||||
		E.Fold(
 | 
			
		||||
			HandleError(c),
 | 
			
		||||
			HandleSuccess[dto.TokenResponseDTO](c, http.StatusOK),
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate handles token validation.
 | 
			
		||||
func (s *AuthService) Validate(c *gin.Context) {
 | 
			
		||||
	var tokenRequest dto.TokenRequestDTO
 | 
			
		||||
	if err := c.ShouldBindJSON(&tokenRequest); err != nil {
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	F.Pipe2(
 | 
			
		||||
		s.authRepository.ValidateToken(c.Request.Context())(tokenRequest.Token),
 | 
			
		||||
		E.Map[error](func(userID string) dto.TokenResponseDTO {
 | 
			
		||||
			return dto.TokenResponseDTO{
 | 
			
		||||
				Token:  tokenRequest.Token,
 | 
			
		||||
				UserID: userID,
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
		E.Fold(
 | 
			
		||||
			HandleError(c),
 | 
			
		||||
			HandleSuccess[dto.TokenResponseDTO](c, http.StatusOK),
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Revoke handles token revocation.
 | 
			
		||||
func (s *AuthService) Revoke(c *gin.Context) {
 | 
			
		||||
	var tokenRequest dto.TokenRequestDTO
 | 
			
		||||
	if err := c.ShouldBindJSON(&tokenRequest); err != nil {
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	F.Pipe2(
 | 
			
		||||
		s.authRepository.RevokeToken(c.Request.Context())(tokenRequest.Token),
 | 
			
		||||
		E.Map[error](func(userID string) gin.H {
 | 
			
		||||
			return gin.H{"message": "Token revoked", "user_id": userID}
 | 
			
		||||
		}),
 | 
			
		||||
		E.Fold(
 | 
			
		||||
			HandleError(c),
 | 
			
		||||
			HandleSuccess[gin.H](c, http.StatusOK),
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								backend-go/internal/application/services/dto/auth_dto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend-go/internal/application/services/dto/auth_dto.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
// TokenResponseDTO represents the response for a token generation or validation.
 | 
			
		||||
type TokenResponseDTO struct {
 | 
			
		||||
	Token  string `json:"token"`
 | 
			
		||||
	UserID string `json:"user_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TokenRequestDTO represents a request for operations involving tokens.
 | 
			
		||||
type TokenRequestDTO struct {
 | 
			
		||||
	Token string `json:"token"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginRequestDTO represents the login request.
 | 
			
		||||
type LoginRequestDTO struct {
 | 
			
		||||
	Email    string `json:"email"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
@ -5,16 +5,17 @@ type ErrorCode string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// General errors
 | 
			
		||||
	InternalError      ErrorCode = "internal_error"
 | 
			
		||||
	ValidationError    ErrorCode = "validation_error"
 | 
			
		||||
	NotFoundError      ErrorCode = "not_found"
 | 
			
		||||
	UnauthorizedError  ErrorCode = "unauthorized"
 | 
			
		||||
	ForbiddenError     ErrorCode = "forbidden"
 | 
			
		||||
	ConflictError      ErrorCode = "conflict"
 | 
			
		||||
	DatabaseError      ErrorCode = "database_error"
 | 
			
		||||
	InternalError        ErrorCode = "internal_error"
 | 
			
		||||
	ValidationError      ErrorCode = "validation_error"
 | 
			
		||||
	NotFoundError        ErrorCode = "not_found"
 | 
			
		||||
	UnauthorizedError    ErrorCode = "unauthorized"
 | 
			
		||||
	ForbiddenError       ErrorCode = "forbidden"
 | 
			
		||||
	ConflictError        ErrorCode = "conflict"
 | 
			
		||||
	DatabaseError        ErrorCode = "database_error"
 | 
			
		||||
	ExternalServiceError ErrorCode = "external_service_error"
 | 
			
		||||
	AuthError            ErrorCode = "auth_error"
 | 
			
		||||
 | 
			
		||||
	// Specific errors
 | 
			
		||||
	UserNotFoundError   ErrorCode = "user_not_found"
 | 
			
		||||
	UserNotFoundError    ErrorCode = "user_not_found"
 | 
			
		||||
	ProjectNotFoundError ErrorCode = "project_not_found"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -23,3 +23,8 @@ func NewUnauthorizedError(message string) *AppError {
 | 
			
		||||
func NewInternalError(err error) *AppError {
 | 
			
		||||
	return Wrap(err, InternalError, "An internal server error occurred", http.StatusInternalServerError)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAuthError creates an authentication error.
 | 
			
		||||
func NewAuthError(message string) *AppError {
 | 
			
		||||
	return New(AuthError, message, http.StatusUnauthorized)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								backend-go/internal/domain/repository/auth_repository.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend-go/internal/domain/repository/auth_repository.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	E "github.com/IBM/fp-go/either"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthRepository interface {
 | 
			
		||||
	GenerateToken(ctx context.Context) func(userID string) E.Either[error, string]
 | 
			
		||||
	ValidateToken(ctx context.Context) func(userID string) E.Either[error, string]
 | 
			
		||||
	RevokeToken(ctx context.Context) func(userID string) E.Either[error, string]
 | 
			
		||||
}
 | 
			
		||||
@ -15,6 +15,7 @@ type Server struct {
 | 
			
		||||
	projectService     *services.ProjectService
 | 
			
		||||
	projectTaskService *services.ProjectTaskService
 | 
			
		||||
	timeEntryService   *services.TimeEntryService
 | 
			
		||||
	authService        *services.AuthService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewServer initializes the Server with its dependencies.
 | 
			
		||||
@ -24,6 +25,7 @@ func NewServer(
 | 
			
		||||
	projectService *services.ProjectService,
 | 
			
		||||
	projectTaskService *services.ProjectTaskService,
 | 
			
		||||
	timeEntryService *services.TimeEntryService,
 | 
			
		||||
	authService *services.AuthService,
 | 
			
		||||
) *Server {
 | 
			
		||||
	return &Server{
 | 
			
		||||
		cfg:                cfg,
 | 
			
		||||
@ -31,6 +33,7 @@ func NewServer(
 | 
			
		||||
		projectService:     projectService,
 | 
			
		||||
		projectTaskService: projectTaskService,
 | 
			
		||||
		timeEntryService:   timeEntryService,
 | 
			
		||||
		authService:        authService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -62,6 +65,9 @@ func (s *Server) Start() error {
 | 
			
		||||
 | 
			
		||||
		timeEntryRouter := api.Group("/time-entries")
 | 
			
		||||
		s.timeEntryService.RegisterRoutes(timeEntryRouter)
 | 
			
		||||
 | 
			
		||||
		authRouter := api.Group("/auth")
 | 
			
		||||
		s.authService.RegisterRoutes(authRouter)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	port := s.cfg.Port
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user