Hayden Hargreaves dd43845138 (FEAT): Logger is implemented!
However, its not used everywhere and the ENV needs work. Lots of work...
2026-01-26 22:41:40 -07:00

270 lines
9.9 KiB
Go

package server
import (
"database/sql"
"fmt"
"net/http"
"strings"
"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/haydenhargreaves/Potion/internal/app/service"
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers"
_ "github.com/lib/pq"
)
type Server struct {
port int
Router *gin.Engine
config cors.Config
DB *sql.DB
deps domain.InjectedDependencies
logs []logging.Logger
}
// Init initializes the server with the provided port. CORS settings are defined here.
// A pointer to a server object is returned which allows for method chaining.
func Init(port int) *Server {
server := &Server{
Router: gin.New(), // Not default anymore, to allow for custom logger
port: port,
config: cors.DefaultConfig(),
logs: []logging.Logger{},
}
// Default logger which logs everything
server.logs = append(server.logs, loggers.NewConsoleLogger(logging.LogLevelTrace))
// Some stuff for templ rendering
// TODO: Remove this
htmlRenderer := server.Router.HTMLRender
server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}
// Disable proxy warnings
server.Router.SetTrustedProxies(nil)
// Setup the CORS settings and active them
// server.config.AllowAllOrigins = true
server.config.AllowOrigins = []string{"http://localhost:5173", "https://potion.gophernest.net"}
server.config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
server.config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
server.config.AllowCredentials = true
server.Router.Use(cors.New(server.config))
return server
}
// Start starts the server on the port provided when the server was initialized
func (s *Server) Start() {
logging.LogAll(s.logs, logging.LogLevelDebug, "Server started on :%d\n", s.port)
s.Router.Run(fmt.Sprintf(":%d", s.port))
}
// TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy...
// TODO: (1/26/2026) Abstract these functions and cleanup. This is fucking messy... still
func (s *Server) Setup() *Server {
// SETUP THE ENVIRONMENT CONFIGURATION
cfg, err := domain.LoadEnvironment(s.logs)
if err != nil {
logging.LogAll(s.logs, logging.LogLevelFatal, err.Error())
panic(err.Error())
}
if cfg == nil {
logging.LogAll(s.logs, logging.LogLevelFatal, "Environment configuration is nil, crashing.")
panic("Environment configuration is nil, crashing.")
}
// TODO: Using release on them all? Def need to clean this shitty environment up
if cfg.Environment == "dev" {
gin.SetMode(gin.ReleaseMode)
} else if cfg.Environment == "prod" {
gin.SetMode(gin.ReleaseMode)
} else {
gin.SetMode(gin.TestMode)
}
// SETUP GOOGLE AUTH
var (
// NOTE: USING V2 NOW
redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK_V2)
clientId string = cfg.GoogleClientId
clientSecret string = cfg.GoogleClientSecret
scope []string = []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
}
)
// Setup Google OAuth
auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)
// SETUP DATABASE
db, err := sql.Open("postgres", cfg.DatabaseUrl)
if err != nil {
panic("Could not connect to database: " + err.Error())
}
if err = db.Ping(); err != nil {
panic("Error pinging database: " + err.Error())
}
s.DB = db
// TODO: Implement environment here for logging file
path := "./logs.log"
fileLogger, cleanup, err := loggers.NewFileLogger(path, logging.LogLevelDebug)
if err != nil {
logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error())
} else {
s.logs = append(s.logs, fileLogger)
defer cleanup()
}
databaseLogger, err := loggers.NewDatabaseLogger(s.DB, "logs", logging.LogLevelInfo)
if err != nil {
logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create database logger. %s\n", err.Error())
} else {
s.logs = append(s.logs, databaseLogger)
}
// SETUP JWT
jwtSecret := []byte(cfg.JwtSecret)
// Initialize and inject dependencies
userRepo := repository.NewUserRepository(s.DB)
recipeRepo := repository.NewRecipeRepository(s.DB)
engagementRepo := repository.NewEngagementRepository(s.DB)
userService := service.NewUserService(userRepo)
authService := service.NewAuthService(userRepo, jwtSecret, s.logs)
recipeService := service.NewRecipeService(recipeRepo, engagementRepo)
engagementService := service.NewEngagementService(engagementRepo, recipeRepo)
s.deps = domain.InjectedDependencies{
UserService: userService,
AuthService: authService,
RecipeService: recipeService,
EngagementService: engagementService,
EnvironmentConfig: *cfg,
}
// Apply middleware
// TODO: Review the recovery middleware
s.Router.Use(gin.Recovery(), RecoveryMiddleware(s.logs), LoggingMiddleware(s.logs))
// Redirect index to home page: Update this as needed
s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
// Wrap all routes with a version
router_v1 := s.Router.Group(domain.VERSION_1)
router_v2 := s.Router.Group(domain.VERSION_2)
// Domain specific routers
router_web := router_v1.Group(domain.WEB)
router_api := router_v1.Group(domain.API)
router_state := router_web.Group("state")
// Static routes
router_web.Static("/static", "./web/static")
// API router endpoints
router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) })
// WEB router endpoints
router_web.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
router_web.GET("/login", s.LoginPageHandler)
router_web.GET("/home", s.HomePageHandler)
router_web.GET("/favorites", s.FavoritesPageHandler)
router_web.GET("/create", s.CreatePageHandler)
router_web.GET("/profile", s.ProfilePageHandler)
router_web.GET("/list", s.ListPageHandler)
router_web.GET("/recipe/:id", s.RecipePageHandler)
router_web.GET("/search", s.SearchPageHandler)
router_web.GET("/404", s.NotFoundPageHandler)
// WEB state endpoints
router_state.POST("/tags", s.NewTagHandler)
router_state.POST("/tags/delete", s.DeleteTagHandler)
// Authentication
router_api.GET("/auth/login", s.GoogleLoginHandler)
router_api.GET("/auth/callback", s.GoogleCallbackHandler)
router_api.GET("/auth/logout", s.LogoutHandler)
// Recipe endpoints
router_api.POST("/recipe", s.CreateRecipeHandler)
router_api.POST("/recipe/search", s.SearchRecipesHandler)
router_api.POST("/recipe/search/favorites", s.SearchRecipesFavoritesHandler)
router_api.GET("/user/recipes", s.GetUserFavoriteRecipesHandler)
router_api.GET("/user/favorites", s.GetUserFavoriteRecipesHandler)
// Engagement endpoints
router_api.POST("/engagement/view/:id", s.EngagementViewRecipeHandler)
router_api.POST("/engagement/share/:id", s.EngagementShareRecipeHandler)
router_api.POST("/engagement/favorite/:id", s.EngagementFavoriteRecipeHandler)
router_api.POST("/engagement/make/:id", s.EngagementMakeRecipeHandler)
// Catch un-routed URLS
s.Router.NoRoute(func(ctx *gin.Context) {
path := ctx.Request.URL.Path
// TODO: Use constants for errors?
if strings.HasPrefix(path, domain.VERSION_1+domain.API) {
ctx.JSON(http.StatusNotFound, gin.H{
"status": http.StatusNotFound,
"error": "API_NOT_FOUND",
"message": "The request endpoint does not exist.",
"path": path,
})
return
}
ctx.Redirect(http.StatusSeeOther, domain.WEB_NOT_FOUND)
})
// ---- VERSION 2 ROUTES ---- //
router_api_v2 := router_v2.Group(domain.API)
router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2)
router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2)
router_api_v2.POST("/recipe/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2)
router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2)
router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2)
router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2)
router_api_v2.GET("/auth/logout", s.LogoutHandlerV2)
router_api_v2.GET("/user", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenticatedUserHandlerV2)
router_api_v2.GET("/user/:id", s.GetUserV2)
router_api_v2.GET("/user/recipes", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserRecipesV2)
router_api_v2.GET("/user/favorites", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserFavoritesV2)
router_api_v2.GET("/user/engagement", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserEngagementV2)
router_api_v2.GET("/user/recipes/made", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserMadeRecipesV2)
router_api_v2.GET("/user/recipes/viewed", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserViewedRecipesV2)
router_api_v2.POST("/engagement/view/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementViewRecipeHandlerV2)
router_api_v2.POST("/engagement/share/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementShareRecipeHandlerV2)
router_api_v2.POST("/engagement/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2)
router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2)
if cfg.Environment == "dev" {
s.debugDisplayRoutes()
}
return s
}
func (s *Server) debugDisplayRoutes() {
for _, route := range s.Router.Routes() {
format := "%-8s %s"
logging.LogAll(s.logs, logging.LogLevelDebug, format, route.Method, route.Path)
}
}