From ec4c7a53e57025f65fffef6441e194f574f84110 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Sun, 15 Jun 2025 14:09:48 -0700 Subject: [PATCH] Refactoring for structure --- Procfile | 2 +- air.toml | 2 +- cmd/systemdesigngame/main.go | 33 +++++ main.go => internal/auth/auth.go | 133 +++---------------- {internals => internal}/db/progress.go | 0 {internals => internal}/db/subscriptions.go | 0 {internals => internal}/db/users.go | 0 {internals => internal}/design/design.go | 0 {internals => internal}/level/level.go | 0 {internals => internal}/level/levels_test.go | 0 internal/server/server.go | 51 +++++++ router/handlers/home.go | 17 +++ router/handlers/play.go | 47 +++++++ router/router.go | 20 +++ 14 files changed, 190 insertions(+), 115 deletions(-) create mode 100644 cmd/systemdesigngame/main.go rename main.go => internal/auth/auth.go (55%) rename {internals => internal}/db/progress.go (100%) rename {internals => internal}/db/subscriptions.go (100%) rename {internals => internal}/db/users.go (100%) rename {internals => internal}/design/design.go (100%) rename {internals => internal}/level/level.go (100%) rename {internals => internal}/level/levels_test.go (100%) create mode 100644 internal/server/server.go create mode 100644 router/handlers/home.go create mode 100644 router/handlers/play.go create mode 100644 router/router.go diff --git a/Procfile b/Procfile index e83a81f..0c1f7a4 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: go run main.go +web: go run ./cmd/systemdesigngame diff --git a/air.toml b/air.toml index 320de53..77661c5 100644 --- a/air.toml +++ b/air.toml @@ -1,6 +1,6 @@ # .air.toml [build] - cmd = "go build -o ./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/systemdesigngame" bin = "tmp/main" full_bin = "tmp/main" include_ext = ["go", "tpl", "tmpl", "html", "js"] diff --git a/cmd/systemdesigngame/main.go b/cmd/systemdesigngame/main.go new file mode 100644 index 0000000..94a21aa --- /dev/null +++ b/cmd/systemdesigngame/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "github.com/joho/godotenv" + "html/template" + "net/http" + "os" + "systemdesigngame/internals/auth" + "systemdesigngame/internals/router" + "systemdesigngame/internals/server" +) + +func main() { + err := godotenv.Load() + if err != nil { + panic("failed to load .env") + } + + // set up JWT secret used for authentication + auth.JwtSecret = []byte(os.Getenv("JWT_SECRET")) + tmpl := template.Must(template.ParseGlob("static/*.html")) + + server.InitApp() + + mux := app.SetupRoutes(tmpl) + + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + } + + server.GracefulShutdown(srv) +} diff --git a/main.go b/internal/auth/auth.go similarity index 55% rename from main.go rename to internal/auth/auth.go index 1428516..09c494f 100644 --- a/main.go +++ b/internal/auth/auth.go @@ -1,126 +1,33 @@ -package main +package auth import ( "context" "encoding/json" "fmt" - "html/template" "net/http" "net/url" "os" - "os/signal" "strings" - "systemdesigngame/internals/level" "time" "github.com/golang-jwt/jwt/v5" - "github.com/joho/godotenv" ) -var jwtSecret []byte - -var tmpl = template.Must(template.ParseGlob("static/*.html")) - -type contextKey string +var JwtSecret []byte const ( - userIDKey = contextKey("userID") - userLoginKey = contextKey("userLogin") - userAvatarKey = contextKey("userAvatar") + UserIDKey = contextKey("userID") + UserLoginKey = contextKey("userLogin") + UserAvatarKey = contextKey("userAvatar") ) -func main() { - err := godotenv.Load() - if err != nil { - panic("failed to load .env") - } - - jwtSecret = []byte(os.Getenv("JWT_SECRET")) - - mux := http.NewServeMux() - mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - mux.HandleFunc("/", index) - mux.HandleFunc("/play/", requireAuth(play)) - mux.HandleFunc("/login", loginHandler) - mux.HandleFunc("/callback", callbackHandler) - - srv := &http.Server{ - Addr: ":8080", - Handler: mux, - } - - levels, err := level.LoadLevels("data/levels.json") - if err != nil { - panic("failed to load levels: " + err.Error()) - } - level.InitRegistry(levels) - - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - - go func() { - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - fmt.Println("Server failed: " + err.Error()) - } - }() - - <-ctx.Done() - stop() - - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - srv.Shutdown(shutdownCtx) -} - -func index(w http.ResponseWriter, r *http.Request) { - data := struct { - Title string - }{ - Title: "Title", - } - - if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil { - http.Error(w, "Template Error", http.StatusInternalServerError) - } -} - -func play(w http.ResponseWriter, r *http.Request) { - levelName := r.URL.Path[len("/play/"):] - levelName, err := url.PathUnescape(levelName) - avatar := r.Context().Value(userAvatarKey).(string) - username := r.Context().Value(userLoginKey).(string) - if err != nil { - http.Error(w, "Invalid level name", http.StatusBadRequest) - return - } - - lvl, err := level.GetLevel(strings.ToLower(levelName), level.DifficultyEasy) - if err != nil { - http.Error(w, "Level not found"+err.Error(), http.StatusNotFound) - return - } - allLevels := level.AllLevels() - data := struct { - Levels []level.Level - Level *level.Level - Avatar string - Username string - }{ - Levels: allLevels, - Level: lvl, - Avatar: avatar, - Username: username, - } - - tmpl.ExecuteTemplate(w, "game.html", data) -} +type contextKey string -func loginHandler(w http.ResponseWriter, r *http.Request) { +func LoginHandler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("auth_token") if err == nil { token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) { - return jwtSecret, nil + return JwtSecret, nil }) if err != nil && token.Valid { @@ -140,7 +47,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, url, http.StatusFound) } -func callbackHandler(w http.ResponseWriter, r *http.Request) { +func CallbackHandler(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") if code == "" { http.Error(w, "No code in query", http.StatusBadRequest) @@ -200,7 +107,7 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) { // Generate JWT fmt.Printf("generating jwt: %s, %s, %s\n", fmt.Sprintf("%d", userInfo.ID), userInfo.Login, userInfo.AvatarUrl) - jwtToken, err := generateJWT(fmt.Sprintf("%d", userInfo.ID), userInfo.Login, userInfo.AvatarUrl) + jwtToken, err := GenerateJWT(fmt.Sprintf("%d", userInfo.ID), userInfo.Login, userInfo.AvatarUrl) if err != nil { http.Error(w, "Failed to generate token", http.StatusInternalServerError) return @@ -218,7 +125,7 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/play", http.StatusFound) } -func generateJWT(userID string, login string, avatarUrl string) (string, error) { +func GenerateJWT(userID string, login string, avatarUrl string) (string, error) { claims := jwt.MapClaims{ "sub": userID, "login": login, @@ -227,11 +134,11 @@ func generateJWT(userID string, login string, avatarUrl string) (string, error) "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(jwtSecret) + return token.SignedString(JwtSecret) } -func requireAuth(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func RequireAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("auth_token") if err != nil { http.Redirect(w, r, "/login", http.StatusFound) @@ -239,7 +146,7 @@ func requireAuth(next http.HandlerFunc) http.HandlerFunc { } token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) { - return jwtSecret, nil + return JwtSecret, nil }) if err != nil || !token.Valid { @@ -261,9 +168,9 @@ func requireAuth(next http.HandlerFunc) http.HandlerFunc { return } - ctx := context.WithValue(r.Context(), userIDKey, userID) - ctx = context.WithValue(ctx, userLoginKey, login) - ctx = context.WithValue(ctx, userAvatarKey, avatar) - next(w, r.WithContext(ctx)) - } + ctx := context.WithValue(r.Context(), UserIDKey, userID) + ctx = context.WithValue(ctx, UserLoginKey, login) + ctx = context.WithValue(ctx, UserAvatarKey, avatar) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/internals/db/progress.go b/internal/db/progress.go similarity index 100% rename from internals/db/progress.go rename to internal/db/progress.go diff --git a/internals/db/subscriptions.go b/internal/db/subscriptions.go similarity index 100% rename from internals/db/subscriptions.go rename to internal/db/subscriptions.go diff --git a/internals/db/users.go b/internal/db/users.go similarity index 100% rename from internals/db/users.go rename to internal/db/users.go diff --git a/internals/design/design.go b/internal/design/design.go similarity index 100% rename from internals/design/design.go rename to internal/design/design.go diff --git a/internals/level/level.go b/internal/level/level.go similarity index 100% rename from internals/level/level.go rename to internal/level/level.go diff --git a/internals/level/levels_test.go b/internal/level/levels_test.go similarity index 100% rename from internals/level/levels_test.go rename to internal/level/levels_test.go diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..ae53cfc --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,51 @@ +package server + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "systemdesigngame/internals/auth" + "systemdesigngame/internals/level" + "time" + + "github.com/joho/godotenv" +) + +func InitApp() { + if err := godotenv.Load(); err != nil { + log.Fatal("failed to load .env") + } + + auth.JwtSecret = []byte(os.Getenv("JWT_SECRET")) + + levels, err := level.LoadLevels("data/levels.json") + if err != nil { + log.Fatal("failed to load levels: " + err.Error()) + } + level.InitRegistry(levels) +} + +func GracefulShutdown(srv *http.Server) { + // setup graceful shutdown + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + // start http server in a separate goroutine + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Println("Server failed: " + err.Error()) + } + }() + + // wait for shutdown signal + <-ctx.Done() + stop() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + srv.Shutdown(shutdownCtx) +} diff --git a/router/handlers/home.go b/router/handlers/home.go new file mode 100644 index 0000000..a6d9ad3 --- /dev/null +++ b/router/handlers/home.go @@ -0,0 +1,17 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +type HomeHandler struct { + Tmpl *template.Template +} + +func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + data := struct{ Title string }{Title: "Title"} + if err := h.Tmpl.ExecuteTemplate(w, "index.html", data); err != nil { + http.Error(w, "Template Error", http.StatusInternalServerError) + } +} diff --git a/router/handlers/play.go b/router/handlers/play.go new file mode 100644 index 0000000..9c83806 --- /dev/null +++ b/router/handlers/play.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "html/template" + "net/http" + "net/url" + "strings" + "systemdesigngame/internals/auth" + "systemdesigngame/internals/level" +) + +type PlayHandler struct { + Tmpl *template.Template +} + +func (h *PlayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + levelName := strings.TrimPrefix(r.URL.Path, "/play/") + levelName, err := url.PathUnescape(levelName) + if err != nil { + http.Error(w, "Invalid level name", http.StatusBadRequest) + return + } + + username := r.Context().Value(auth.UserLoginKey).(string) + avatar := r.Context().Value(auth.UserAvatarKey).(string) + + lvl, err := level.GetLevel(strings.ToLower(levelName), level.DifficultyEasy) + if err != nil { + http.Error(w, "Level not found: "+err.Error(), http.StatusNotFound) + return + } + + allLevels := level.AllLevels() + data := struct { + Levels []level.Level + Level *level.Level + Avatar string + Username string + }{ + Levels: allLevels, + Level: lvl, + Avatar: avatar, + Username: username, + } + + h.Tmpl.ExecuteTemplate(w, "game.html", data) +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..38f786a --- /dev/null +++ b/router/router.go @@ -0,0 +1,20 @@ +package app + +import ( + "html/template" + "net/http" + "systemdesigngame/internal/auth" + "systemdesigngame/router/handlers" +) + +func SetupRoutes(tmpl *template.Template) *http.ServeMux { + // initialize http routes and handlers + mux := http.NewServeMux() + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + mux.Handle("/", &handlers.HomeHandler{Tmpl: tmpl}) + mux.Handle("/play/", auth.RequireAuth(&handlers.PlayHandler{Tmpl: tmpl})) + mux.HandleFunc("/login", auth.LoginHandler) + mux.HandleFunc("/callback", auth.CallbackHandler) + + return mux +}