diff --git a/cmd/main.go b/cmd/main.go index 22cf02e..3328c92 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" - "github.com/gin-gonic/gin" "github.com/isd-sgcu/oph66-backend/di" "github.com/isd-sgcu/oph66-backend/docs" swaggerFiles "github.com/swaggo/files" @@ -27,12 +26,8 @@ func main() { docs.SwaggerInfo.Host = container.Config.AppConfig.Host - if !container.Config.AppConfig.IsDevelopment() { - gin.SetMode(gin.ReleaseMode) - } - r := gin.Default() + r := container.Router - r.Use(gin.HandlerFunc(container.CorsHandler)) r.GET("/_hc", container.HcHandler.HealthCheck) r.GET("/live", container.FeatureflagHandler.GetLivestreamInfo) r.GET("/events", container.EventHandler.GetAllEvents) @@ -42,7 +37,7 @@ func main() { r.GET("/auth/login", container.AuthHandler.GoogleLogin) r.GET("/auth/callback", container.AuthHandler.GoogleCallback) - if container.Config.AppConfig.Env == "development" { + if container.Config.AppConfig.IsDevelopment() { r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } if err := r.Run(fmt.Sprintf(":%v", container.Config.AppConfig.Port)); err != nil { diff --git a/di/wire.go b/di/wire.go index f098b66..5e0e3ef 100644 --- a/di/wire.go +++ b/di/wire.go @@ -12,6 +12,8 @@ import ( event "github.com/isd-sgcu/oph66-backend/internal/event" featureflag "github.com/isd-sgcu/oph66-backend/internal/feature_flag" healthcheck "github.com/isd-sgcu/oph66-backend/internal/health_check" + "github.com/isd-sgcu/oph66-backend/internal/middleware" + "github.com/isd-sgcu/oph66-backend/internal/router" "github.com/isd-sgcu/oph66-backend/logger" "go.uber.org/zap" ) @@ -24,9 +26,19 @@ type Container struct { Config *cfgldr.Config Logger *zap.Logger CorsHandler cfgldr.CorsHandler + Router *router.Router } -func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, featureflagHandler featureflag.Handler, authHandler auth.Handler, config *cfgldr.Config, logger *zap.Logger, corsHandler cfgldr.CorsHandler) Container { +func newContainer( + eventHandler event.Handler, + hcHandler healthcheck.Handler, + featureflagHandler featureflag.Handler, + authHandler auth.Handler, + config *cfgldr.Config, + logger *zap.Logger, + corsHandler cfgldr.CorsHandler, + router *router.Router, +) Container { return Container{ eventHandler, hcHandler, @@ -35,6 +47,7 @@ func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, fea config, logger, corsHandler, + router, } } @@ -58,6 +71,8 @@ func Init() (Container, error) { auth.NewService, auth.NewRepository, logger.InitLogger, + router.NewRouter, + middleware.NewAuthMiddleware, ) return Container{}, nil diff --git a/di/wire_gen.go b/di/wire_gen.go index d355efd..cd867e1 100644 --- a/di/wire_gen.go +++ b/di/wire_gen.go @@ -14,6 +14,8 @@ import ( "github.com/isd-sgcu/oph66-backend/internal/event" "github.com/isd-sgcu/oph66-backend/internal/feature_flag" "github.com/isd-sgcu/oph66-backend/internal/health_check" + "github.com/isd-sgcu/oph66-backend/internal/middleware" + "github.com/isd-sgcu/oph66-backend/internal/router" "github.com/isd-sgcu/oph66-backend/logger" "go.uber.org/zap" ) @@ -47,7 +49,9 @@ func Init() (Container, error) { authService := auth.NewService(authRepository, zapLogger, config) authHandler := auth.NewHandler(authService, zapLogger) corsHandler := cfgldr.MakeCorsConfig(config) - container := newContainer(handler, healthcheckHandler, featureflagHandler, authHandler, config, zapLogger, corsHandler) + authMiddleware := middleware.NewAuthMiddleware(authRepository, config) + routerRouter := router.NewRouter(config, corsHandler, authMiddleware) + container := newContainer(handler, healthcheckHandler, featureflagHandler, authHandler, config, zapLogger, corsHandler, routerRouter) return container, nil } @@ -61,14 +65,22 @@ type Container struct { Config *cfgldr.Config Logger *zap.Logger CorsHandler cfgldr.CorsHandler + Router *router.Router } -func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, featureflagHandler featureflag.Handler, authHandler auth.Handler, config *cfgldr.Config, logger2 *zap.Logger, corsHandler cfgldr.CorsHandler) Container { +func newContainer( + eventHandler event.Handler, + hcHandler healthcheck.Handler, + featureflagHandler featureflag.Handler, + authHandler auth.Handler, + config *cfgldr.Config, logger2 *zap.Logger, + corsHandler cfgldr.CorsHandler, router2 *router.Router, +) Container { return Container{ eventHandler, hcHandler, featureflagHandler, authHandler, - config, logger2, corsHandler, + config, logger2, corsHandler, router2, } } diff --git a/internal/auth/auth.handler.go b/internal/auth/auth.handler.go index 6be68ff..e6aae9d 100644 --- a/internal/auth/auth.handler.go +++ b/internal/auth/auth.handler.go @@ -2,7 +2,6 @@ package auth import ( "net/http" - "strings" "github.com/gin-gonic/gin" "github.com/isd-sgcu/oph66-backend/apperror" @@ -84,23 +83,25 @@ func (h *handlerImpl) GoogleCallback(c *gin.Context) { func (h *handlerImpl) Register(c *gin.Context) { var data RegisterRequestDTO var user model.User - authHeader := c.GetHeader("Authorization") - if authHeader == "" { + emailRaw, exist := c.Get("email") + if !exist { utils.ReturnError(c, apperror.Unauthorized) return } - if !strings.HasPrefix(authHeader, "Bearer ") { - utils.ReturnError(c, apperror.InvalidToken) + + email, ok := emailRaw.(string) + if !ok { + h.logger.Error("email string assertion failed", zap.Any("emailRaw", emailRaw)) + utils.ReturnError(c, apperror.InternalError) return } - token := strings.Replace(authHeader, "Bearer ", "", 1) if err := c.ShouldBindJSON(&data); err != nil { utils.ReturnError(c, apperror.BadRequest) return } - apperr := h.svc.Register(c, &data, token, &user) + apperr := h.svc.Register(email, &data, &user) if apperr != nil { utils.ReturnError(c, apperr) return @@ -125,20 +126,21 @@ func (h *handlerImpl) Register(c *gin.Context) { // @Failure 401 {object} auth.GetProfileUnauthorized // @Failure 404 {object} auth.GetProfileUserNotFound func (h *handlerImpl) GetProfile(c *gin.Context) { - var user model.User - authHeader := c.GetHeader("Authorization") - if authHeader == "" { + emailRaw, exist := c.Get("email") + if !exist { utils.ReturnError(c, apperror.Unauthorized) return } - if !strings.HasPrefix(authHeader, "Bearer ") { - utils.ReturnError(c, apperror.InvalidToken) + + email, ok := emailRaw.(string) + if !ok { + h.logger.Error("email string assertion failed", zap.Any("emailRaw", emailRaw)) + utils.ReturnError(c, apperror.InternalError) return } - token := strings.Replace(authHeader, "Bearer ", "", 1) - - apperr := h.svc.GetUserFromJWTToken(c, token, &user) + var user model.User + apperr := h.svc.GetUserFromJWTToken(email, &user) if apperr != nil { utils.ReturnError(c, apperr) return diff --git a/internal/auth/auth.service.go b/internal/auth/auth.service.go index 9a380fc..a61b79d 100644 --- a/internal/auth/auth.service.go +++ b/internal/auth/auth.service.go @@ -2,6 +2,7 @@ package auth import ( "context" + "errors" "github.com/isd-sgcu/oph66-backend/apperror" "github.com/isd-sgcu/oph66-backend/cfgldr" @@ -9,14 +10,14 @@ import ( "go.uber.org/zap" "golang.org/x/oauth2" "golang.org/x/oauth2/google" - "google.golang.org/api/idtoken" + "gorm.io/gorm" ) type Service interface { GoogleLogin() (url string) GoogleCallback(ctx context.Context, code string) (idToken string, appErr *apperror.AppError) - Register(ctx context.Context, data *RegisterRequestDTO, tokenString string, user *model.User) *apperror.AppError - GetUserFromJWTToken(ctx context.Context, tokenString string, user *model.User) *apperror.AppError + Register(email string, data *RegisterRequestDTO, user *model.User) *apperror.AppError + GetUserFromJWTToken(email string, user *model.User) *apperror.AppError } func NewService(repo Repository, logger *zap.Logger, cfg *cfgldr.Config) Service { @@ -63,51 +64,24 @@ func (s *serviceImpl) GoogleCallback(ctx context.Context, code string) (idToken return rawIdToken.(string), nil } -func (s *serviceImpl) Register(ctx context.Context, data *RegisterRequestDTO, token string, user *model.User) *apperror.AppError { - email, apperr := getEmailFromToken(ctx, token, s.cfg.OAuth2Config.ClientID) - if apperr != nil { - return apperr - } - - err := s.repo.GetUserByEmail(user, email) - if err != nil { - user = ConvertRegisterRequestDTOToUser(data, email) - err = s.repo.CreateUser(user) - if err != nil { - s.logger.Error("Failed to create user", zap.Error(err)) - return apperror.InternalError - } - } else { +func (s *serviceImpl) Register(email string, data *RegisterRequestDTO, user *model.User) *apperror.AppError { + user = ConvertRegisterRequestDTOToUser(data, email) + err := s.repo.CreateUser(user) + if errors.Is(err, gorm.ErrDuplicatedKey) { return apperror.DuplicateEmail + } else if err != nil { + s.logger.Error("Failed to create user", zap.Error(err)) + return apperror.InternalError } return nil } -func (s *serviceImpl) GetUserFromJWTToken(ctx context.Context, token string, user *model.User) *apperror.AppError { - email, apperr := getEmailFromToken(ctx, token, s.cfg.OAuth2Config.ClientID) - if apperr != nil { - return apperr - } - - err := s.repo.GetUserByEmail(user, email) +func (s *serviceImpl) GetUserFromJWTToken(email string, result *model.User) *apperror.AppError { + err := s.repo.GetUserByEmail(result, email) if err != nil { return apperror.UserNotFound } return nil } - -func getEmailFromToken(ctx context.Context, tokenString string, clientID string) (email string, appErr *apperror.AppError) { - token, err := idtoken.Validate(ctx, tokenString, clientID) - if err != nil { - return "", apperror.InvalidToken - } - - email, ok := token.Claims["email"].(string) - if !ok || email == "" { - return "", apperror.InvalidToken - } - - return email, nil -} diff --git a/internal/middleware/auth.middleware.go b/internal/middleware/auth.middleware.go new file mode 100644 index 0000000..e3a9043 --- /dev/null +++ b/internal/middleware/auth.middleware.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/cfgldr" + "github.com/isd-sgcu/oph66-backend/internal/auth" + "github.com/isd-sgcu/oph66-backend/utils" + "google.golang.org/api/idtoken" +) + +type AuthMiddleware gin.HandlerFunc + +func NewAuthMiddleware(userRepo auth.Repository, cfg *cfgldr.Config) AuthMiddleware { + + return func(c *gin.Context) { + + authHeader := c.GetHeader("Authorization") + + if authHeader == "" { + + c.Next() + + return + + } + + if !strings.HasPrefix(authHeader, "Bearer ") { + + utils.ReturnError(c, apperror.InvalidToken) + + c.Abort() + + return + + } + + tokenString := strings.Replace(authHeader, "Bearer ", "", 1) + + token, err := idtoken.Validate(c, tokenString, cfg.OAuth2Config.ClientID) + + if err != nil { + + utils.ReturnError(c, apperror.InvalidToken) + + c.Abort() + + return + + } + + if email, ok := token.Claims["email"].(string); ok { + + c.Set("email", email) + + c.Next() + + return + + } else { + + utils.ReturnError(c, apperror.ServiceUnavailable) + + c.Abort() + + return + + } + + } + +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..75c12ca --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,29 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/isd-sgcu/oph66-backend/cfgldr" + "github.com/isd-sgcu/oph66-backend/internal/middleware" +) + +type Router struct { + *gin.Engine +} + +func NewRouter(config *cfgldr.Config, corsHandler cfgldr.CorsHandler, authMiddleware middleware.AuthMiddleware) *Router { + + if !config.AppConfig.IsDevelopment() { + + gin.SetMode(gin.ReleaseMode) + + } + + r := gin.Default() + + r.Use(gin.HandlerFunc(corsHandler)) + + r.Use(gin.HandlerFunc(authMiddleware)) + + return &Router{r} + +}