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())
|
projectRepo := repository.NewProjectRepository(database.Projects())
|
||||||
projectTaskRepo := repository.NewProjectTaskRepository(database.ProjectTasks())
|
projectTaskRepo := repository.NewProjectTaskRepository(database.ProjectTasks())
|
||||||
timeEntryRepo := repository.NewTimeEntryRepository(database.TimeEntries())
|
timeEntryRepo := repository.NewTimeEntryRepository(database.TimeEntries())
|
||||||
|
authRepo := repository.NewInMemoryAuthRepository("secret")
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
userService := services.NewUserService(userRepo)
|
userService := services.NewUserService(userRepo)
|
||||||
projectService := services.NewProjectService(projectRepo)
|
projectService := services.NewProjectService(projectRepo)
|
||||||
projectTaskService := services.NewProjectTaskService(projectTaskRepo)
|
projectTaskService := services.NewProjectTaskService(projectTaskRepo)
|
||||||
timeEntryService := services.NewTimeEntryService(timeEntryRepo)
|
timeEntryService := services.NewTimeEntryService(timeEntryRepo)
|
||||||
|
authService := services.NewAuthService(authRepo, userRepo)
|
||||||
|
|
||||||
// Initialize and start the server
|
// 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...")
|
fmt.Println("Starting ActaTempus server on port 8080...")
|
||||||
if err := server.Start(); err != nil {
|
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/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // 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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // 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/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 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
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 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=
|
||||||
|
@ -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"`
|
||||||
|
}
|
@ -13,6 +13,7 @@ const (
|
|||||||
ConflictError ErrorCode = "conflict"
|
ConflictError ErrorCode = "conflict"
|
||||||
DatabaseError ErrorCode = "database_error"
|
DatabaseError ErrorCode = "database_error"
|
||||||
ExternalServiceError ErrorCode = "external_service_error"
|
ExternalServiceError ErrorCode = "external_service_error"
|
||||||
|
AuthError ErrorCode = "auth_error"
|
||||||
|
|
||||||
// Specific errors
|
// Specific errors
|
||||||
UserNotFoundError ErrorCode = "user_not_found"
|
UserNotFoundError ErrorCode = "user_not_found"
|
||||||
|
@ -23,3 +23,8 @@ func NewUnauthorizedError(message string) *AppError {
|
|||||||
func NewInternalError(err error) *AppError {
|
func NewInternalError(err error) *AppError {
|
||||||
return Wrap(err, InternalError, "An internal server error occurred", http.StatusInternalServerError)
|
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
|
projectService *services.ProjectService
|
||||||
projectTaskService *services.ProjectTaskService
|
projectTaskService *services.ProjectTaskService
|
||||||
timeEntryService *services.TimeEntryService
|
timeEntryService *services.TimeEntryService
|
||||||
|
authService *services.AuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer initializes the Server with its dependencies.
|
// NewServer initializes the Server with its dependencies.
|
||||||
@ -24,6 +25,7 @@ func NewServer(
|
|||||||
projectService *services.ProjectService,
|
projectService *services.ProjectService,
|
||||||
projectTaskService *services.ProjectTaskService,
|
projectTaskService *services.ProjectTaskService,
|
||||||
timeEntryService *services.TimeEntryService,
|
timeEntryService *services.TimeEntryService,
|
||||||
|
authService *services.AuthService,
|
||||||
) *Server {
|
) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -31,6 +33,7 @@ func NewServer(
|
|||||||
projectService: projectService,
|
projectService: projectService,
|
||||||
projectTaskService: projectTaskService,
|
projectTaskService: projectTaskService,
|
||||||
timeEntryService: timeEntryService,
|
timeEntryService: timeEntryService,
|
||||||
|
authService: authService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +65,9 @@ func (s *Server) Start() error {
|
|||||||
|
|
||||||
timeEntryRouter := api.Group("/time-entries")
|
timeEntryRouter := api.Group("/time-entries")
|
||||||
s.timeEntryService.RegisterRoutes(timeEntryRouter)
|
s.timeEntryService.RegisterRoutes(timeEntryRouter)
|
||||||
|
|
||||||
|
authRouter := api.Group("/auth")
|
||||||
|
s.authService.RegisterRoutes(authRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
port := s.cfg.Port
|
port := s.cfg.Port
|
||||||
|
Loading…
x
Reference in New Issue
Block a user