Hayden Hargreaves c0b76506c4 (FEAT): Profile page APIs are complete!!!!
This also includes a shell.nix file for use just in case the flake
isn't.
2025-11-15 23:26:16 -07:00

220 lines
7.2 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/lib/pq"
)
type Server struct {
port int
Router *gin.Engine
config cors.Config
DB *sql.DB
deps domain.InjectedDependencies
}
// 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.Default(),
port: port,
config: cors.DefaultConfig(),
}
// Some stuff for templ rendering
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"}
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() {
s.Router.Run(fmt.Sprintf(":%d", s.port))
}
// TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy...
func (s *Server) Setup() *Server {
// SETUP THE ENVIRONMENT CONFIGURATION
cfg, err := domain.LoadEnvironment()
if err != nil {
panic(err.Error())
}
if cfg == nil {
panic("Environment configuration is nil, crashing.")
}
if cfg.Environment == "dev" {
gin.SetMode(gin.DebugMode)
} 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
// 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)
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
s.Router.Use(RecoveryMiddleware())
// NOTE: No longer running on every connection!
// s.Router.Use(JwtAuthMiddleWare(jwtSecret))
// 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/of-the-week", s.GetRecipeOfTheWeekHandlerV2)
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/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("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"})
})
return s
}