From a70138f2f7bc15d4f82441ad0c7fb40be6ee58c4 Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Sun, 29 Sep 2024 23:51:23 +0200 Subject: [PATCH] fix: restructure env handling for better testing capabillities #181 --- handler/auth.go | 35 +++++++------- handler/default.go | 11 +++-- handler/workout.go | 5 +- main.go | 21 +++++---- middleware/cors.go | 6 +-- service/auth.go | 52 ++++++++++---------- service/index_and_404.go | 7 +-- service/mail.go | 29 ++++++++++++ service/workout.go | 5 +- template/layout.templ | 6 +-- template/mail/register.templ | 9 ++-- template/mail/reset-password.templ | 9 ++-- types/settings.go | 76 ++++++++++++++++++++++++++++++ utils/env.go | 57 ---------------------- utils/mail.go | 15 ------ 15 files changed, 191 insertions(+), 152 deletions(-) create mode 100644 service/mail.go create mode 100644 types/settings.go delete mode 100644 utils/env.go delete mode 100644 utils/mail.go diff --git a/handler/auth.go b/handler/auth.go index 22630e1..0c3f79c 100644 --- a/handler/auth.go +++ b/handler/auth.go @@ -2,11 +2,12 @@ package handler import ( "me-fit/service" + "me-fit/types" "me-fit/utils" - "time" "database/sql" "net/http" + "time" ) type HandlerAuth interface { @@ -14,33 +15,35 @@ type HandlerAuth interface { } type HandlerAuthImpl struct { - db *sql.DB - service service.ServiceAuth + db *sql.DB + service service.ServiceAuth + serverSettings *types.ServerSettings } -func NewHandlerAuth(db *sql.DB, service service.ServiceAuth) HandlerAuth { +func NewHandlerAuth(db *sql.DB, service service.ServiceAuth, serverSettings *types.ServerSettings) HandlerAuth { return HandlerAuthImpl{ - db: db, - service: service, + db: db, + service: service, + serverSettings: serverSettings, } } func (handler HandlerAuthImpl) handle(router *http.ServeMux) { // Don't use auth middleware for these routes, as it makes redirecting very difficult, if the mail is not yet verified - router.Handle("/auth/signin", service.HandleSignInPage(handler.db)) - router.Handle("/auth/signup", service.HandleSignUpPage(handler.db)) - router.Handle("/auth/verify", service.HandleSignUpVerifyPage(handler.db)) // Hint for the user to verify their email - router.Handle("/auth/delete-account", service.HandleDeleteAccountPage(handler.db)) + router.Handle("/auth/signin", service.HandleSignInPage(handler.db, handler.serverSettings)) + router.Handle("/auth/signup", service.HandleSignUpPage(handler.db, handler.serverSettings)) + router.Handle("/auth/verify", service.HandleSignUpVerifyPage(handler.db, handler.serverSettings)) // Hint for the user to verify their email + router.Handle("/auth/delete-account", service.HandleDeleteAccountPage(handler.db, handler.serverSettings)) router.Handle("/auth/verify-email", service.HandleSignUpVerifyResponsePage(handler.db)) // The link contained in the email - router.Handle("/auth/change-password", service.HandleChangePasswordPage(handler.db)) - router.Handle("/auth/reset-password", service.HandleResetPasswordPage(handler.db)) - router.Handle("/api/auth/signup", service.HandleSignUpComp(handler.db)) + router.Handle("/auth/change-password", service.HandleChangePasswordPage(handler.db, handler.serverSettings)) + router.Handle("/auth/reset-password", service.HandleResetPasswordPage(handler.db, handler.serverSettings)) + router.Handle("/api/auth/signup", service.HandleSignUpComp(handler.db, handler.serverSettings)) router.Handle("/api/auth/signin", handler.handleSignIn()) router.Handle("/api/auth/signout", service.HandleSignOutComp(handler.db)) - router.Handle("/api/auth/delete-account", service.HandleDeleteAccountComp(handler.db)) - router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(handler.db)) + router.Handle("/api/auth/delete-account", service.HandleDeleteAccountComp(handler.db, handler.serverSettings)) + router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(handler.db, handler.serverSettings)) router.Handle("/api/auth/change-password", service.HandleChangePasswordComp(handler.db)) - router.Handle("/api/auth/reset-password", service.HandleResetPasswordComp(handler.db)) + router.Handle("/api/auth/reset-password", service.HandleResetPasswordComp(handler.db, handler.serverSettings)) router.Handle("/api/auth/reset-password-actual", service.HandleActualResetPasswordComp(handler.db)) } diff --git a/handler/default.go b/handler/default.go index 3810adc..3412f1d 100644 --- a/handler/default.go +++ b/handler/default.go @@ -4,26 +4,27 @@ import ( "me-fit/db" "me-fit/middleware" "me-fit/service" + "me-fit/types" "database/sql" "net/http" ) -func GetHandler(d *sql.DB) http.Handler { +func GetHandler(d *sql.DB, serverSettings *types.ServerSettings) http.Handler { var router = http.NewServeMux() - router.HandleFunc("/", service.HandleIndexAnd404(d)) + router.HandleFunc("/", service.HandleIndexAnd404(d, serverSettings)) - handlerAuth := NewHandlerAuth(d, service.NewServiceAuthImpl(db.NewDbAuthSqlite(d))) + handlerAuth := NewHandlerAuth(d, service.NewServiceAuthImpl(db.NewDbAuthSqlite(d)), serverSettings) // Serve static files (CSS, JS and images) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) - handleWorkout(d, router) + handleWorkout(d, router, serverSettings) handlerAuth.handle(router) - return middleware.Logging(middleware.EnableCors(router)) + return middleware.Logging(middleware.EnableCors(serverSettings, router)) } func auth(db *sql.DB, h http.Handler) http.Handler { diff --git a/handler/workout.go b/handler/workout.go index d78212c..504529d 100644 --- a/handler/workout.go +++ b/handler/workout.go @@ -2,13 +2,14 @@ package handler import ( "me-fit/service" + "me-fit/types" "database/sql" "net/http" ) -func handleWorkout(db *sql.DB, router *http.ServeMux) { - router.Handle("/workout", auth(db, service.HandleWorkoutPage(db))) +func handleWorkout(db *sql.DB, router *http.ServeMux, serverSettings *types.ServerSettings) { + router.Handle("/workout", auth(db, service.HandleWorkoutPage(db, serverSettings))) router.Handle("POST /api/workout", auth(db, service.HandleWorkoutNewComp(db))) router.Handle("GET /api/workout", auth(db, service.HandleWorkoutGetComp(db))) router.Handle("DELETE /api/workout/{id}", auth(db, service.HandleWorkoutDeleteComp(db))) diff --git a/main.go b/main.go index 475319a..44087fc 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,9 @@ package main import ( "me-fit/handler" + "me-fit/types" "me-fit/utils" + "os" "context" "database/sql" @@ -20,21 +22,22 @@ import ( ) func main() { - run(context.Background()) + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + run(context.Background(), os.Getenv) } -func run(ctx context.Context) { +func run(ctx context.Context, env func(string) string) { ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() slog.Info("Starting server...") - // init env - err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } - utils.MustInitEnv() + // init server settings + serverSettings := types.NewServerSettingsFromEnv(env) // init db db, err := sql.Open("sqlite3", "./data.db") @@ -51,7 +54,7 @@ func run(ctx context.Context) { } httpServer := &http.Server{ Addr: ":8080", - Handler: handler.GetHandler(db), + Handler: handler.GetHandler(db, serverSettings), } go startServer(prometheusServer) go startServer(httpServer) diff --git a/middleware/cors.go b/middleware/cors.go index decd71f..86bb71b 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -1,15 +1,15 @@ package middleware import ( - "me-fit/utils" + "me-fit/types" "net/http" ) -func EnableCors(next http.Handler) http.Handler { +func EnableCors(serverSettings *types.ServerSettings, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", utils.BaseUrl) + w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") if r.Method == "OPTIONS" { diff --git a/service/auth.go b/service/auth.go index e8b2eaa..24edf01 100644 --- a/service/auth.go +++ b/service/auth.go @@ -77,14 +77,14 @@ func (service ServiceAuthImpl) SignIn(email string, password string) (*User, err return NewUser(user), nil } -func HandleSignInPage(db *sql.DB) http.HandlerFunc { +func HandleSignInPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) if user == nil { userComp := UserInfoComp(nil) signIn := auth.SignInOrUpComp(true) - err := template.Layout(signIn, userComp).Render(r.Context(), w) + err := template.Layout(signIn, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render sign in page", err) @@ -99,14 +99,14 @@ func HandleSignInPage(db *sql.DB) http.HandlerFunc { } } -func HandleSignUpPage(db *sql.DB) http.HandlerFunc { +func HandleSignUpPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) if user == nil { userComp := UserInfoComp(nil) signUpComp := auth.SignInOrUpComp(false) - err := template.Layout(signUpComp, userComp).Render(r.Context(), w) + err := template.Layout(signUpComp, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render sign up page", err) @@ -121,7 +121,7 @@ func HandleSignUpPage(db *sql.DB) http.HandlerFunc { } } -func HandleSignUpVerifyPage(db *sql.DB) http.HandlerFunc { +func HandleSignUpVerifyPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) if user == nil { @@ -131,7 +131,7 @@ func HandleSignUpVerifyPage(db *sql.DB) http.HandlerFunc { } else { userComp := UserInfoComp(user) signIn := auth.VerifyComp() - err := template.Layout(signIn, userComp).Render(r.Context(), w) + err := template.Layout(signIn, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render verify page", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -140,7 +140,7 @@ func HandleSignUpVerifyPage(db *sql.DB) http.HandlerFunc { } } -func HandleDeleteAccountPage(db *sql.DB) http.HandlerFunc { +func HandleDeleteAccountPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // An unverified email should be able to delete their account user := utils.GetUserFromSession(db, r) @@ -149,7 +149,7 @@ func HandleDeleteAccountPage(db *sql.DB) http.HandlerFunc { } else { userComp := UserInfoComp(user) comp := auth.DeleteAccountComp() - err := template.Layout(comp, userComp).Render(r.Context(), w) + err := template.Layout(comp, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render delete account page", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -200,7 +200,7 @@ func HandleSignUpVerifyResponsePage(db *sql.DB) http.HandlerFunc { } } -func HandleChangePasswordPage(db *sql.DB) http.HandlerFunc { +func HandleChangePasswordPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { isPasswordReset := r.URL.Query().Has("token") @@ -211,7 +211,7 @@ func HandleChangePasswordPage(db *sql.DB) http.HandlerFunc { } else { userComp := UserInfoComp(user) comp := auth.ChangePasswordComp(isPasswordReset) - err := template.Layout(comp, userComp).Render(r.Context(), w) + err := template.Layout(comp, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render change password page", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -220,7 +220,7 @@ func HandleChangePasswordPage(db *sql.DB) http.HandlerFunc { } } -func HandleResetPasswordPage(db *sql.DB) http.HandlerFunc { +func HandleResetPasswordPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) @@ -229,7 +229,7 @@ func HandleResetPasswordPage(db *sql.DB) http.HandlerFunc { } else { userComp := UserInfoComp(nil) comp := auth.ResetPasswordComp() - err := template.Layout(comp, userComp).Render(r.Context(), w) + err := template.Layout(comp, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render change password page", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -247,7 +247,7 @@ func UserInfoComp(user *types.User) templ.Component { } } -func HandleSignUpComp(db *sql.DB) http.HandlerFunc { +func HandleSignUpComp(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var email = r.FormValue("email") var password = r.FormValue("password") @@ -304,7 +304,7 @@ func HandleSignUpComp(db *sql.DB) http.HandlerFunc { } // Send verification email as a goroutine - go sendVerificationEmail(db, userId.String(), email) + go sendVerificationEmail(db, userId.String(), email, serverSettings) utils.DoRedirect(w, r, "/auth/verify") } @@ -339,7 +339,8 @@ func HandleSignOutComp(db *sql.DB) http.HandlerFunc { } } -func HandleDeleteAccountComp(db *sql.DB) http.HandlerFunc { +func HandleDeleteAccountComp(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { + mailService := NewMailService(serverSettings) return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) if user == nil { @@ -399,13 +400,13 @@ func HandleDeleteAccountComp(db *sql.DB) http.HandlerFunc { return } - go utils.SendMail(user.Email, "Account deleted", "Your account has been deleted") + go mailService.SendMail(user.Email, "Account deleted", "Your account has been deleted") utils.DoRedirect(w, r, "/") } } -func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc { +func HandleVerifyResendComp(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) if user == nil || user.EmailVerified { @@ -413,7 +414,7 @@ func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc { return } - go sendVerificationEmail(db, user.Id.String(), user.Email) + go sendVerificationEmail(db, user.Id.String(), user.Email, serverSettings) w.Write([]byte("

Verification email sent

")) } @@ -535,7 +536,8 @@ func HandleActualResetPasswordComp(db *sql.DB) http.HandlerFunc { utils.TriggerToast(w, r, "success", "Password changed") } } -func HandleResetPasswordComp(db *sql.DB) http.HandlerFunc { +func HandleResetPasswordComp(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { + mailService := NewMailService(serverSettings) return func(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") @@ -571,19 +573,20 @@ func HandleResetPasswordComp(db *sql.DB) http.HandlerFunc { if i != 0 { var mail strings.Builder - err = tempMail.ResetPassword(token).Render(context.Background(), &mail) + err = tempMail.ResetPassword(serverSettings.BaseUrl, token).Render(context.Background(), &mail) if err != nil { utils.LogError("Could not render reset password email", err) utils.TriggerToast(w, r, "error", "Internal Server Error") return } - utils.SendMail(email, "Reset Password", mail.String()) + mailService.SendMail(email, "Reset Password", mail.String()) } utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent") } } -func sendVerificationEmail(db *sql.DB, userId string, email string) { + +func sendVerificationEmail(db *sql.DB, userId string, email string, serverSettings *types.ServerSettings) { var token string err := db.QueryRow("SELECT token FROM user_token WHERE user_uuid = ? AND type = 'email_verify'", userId).Scan(&token) @@ -607,12 +610,13 @@ func sendVerificationEmail(db *sql.DB, userId string, email string) { } var w strings.Builder - err = tempMail.Register(token).Render(context.Background(), &w) + err = tempMail.Register(serverSettings.BaseUrl, token).Render(context.Background(), &w) if err != nil { utils.LogError("Could not render welcome email", err) return } - utils.SendMail(email, "Welcome to ME-FIT", w.String()) + mailService := NewMailService(serverSettings) + mailService.SendMail(email, "Welcome to ME-FIT", w.String()) } func TryCreateSessionAndSetCookie(r *http.Request, w http.ResponseWriter, db *sql.DB, user_uuid uuid.UUID) error { diff --git a/service/index_and_404.go b/service/index_and_404.go index 480ca6d..085f032 100644 --- a/service/index_and_404.go +++ b/service/index_and_404.go @@ -3,13 +3,14 @@ package service import ( "database/sql" "me-fit/template" + "me-fit/types" "me-fit/utils" "net/http" "github.com/a-h/templ" ) -func HandleIndexAnd404(db *sql.DB) http.HandlerFunc { +func HandleIndexAnd404(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUserFromSession(db, r) @@ -17,10 +18,10 @@ func HandleIndexAnd404(db *sql.DB) http.HandlerFunc { userComp := UserInfoComp(user) if r.URL.Path != "/" { - comp = template.Layout(template.NotFound(), userComp) + comp = template.Layout(template.NotFound(), userComp, serverSettings.Environment) w.WriteHeader(http.StatusNotFound) } else { - comp = template.Layout(template.Index(), userComp) + comp = template.Layout(template.Index(), userComp, serverSettings.Environment) } err := comp.Render(r.Context(), w) diff --git a/service/mail.go b/service/mail.go new file mode 100644 index 0000000..f60d0b4 --- /dev/null +++ b/service/mail.go @@ -0,0 +1,29 @@ +package service + +import ( + "fmt" + "me-fit/types" + "net/smtp" +) + +type MailService struct { + serverSettings *types.ServerSettings +} + +func NewMailService(serverSettings *types.ServerSettings) MailService { + return MailService{serverSettings: serverSettings} +} + +func (m MailService) SendMail(to string, subject string, message string) error { + if m.serverSettings.Smtp == nil { + return nil + } + + s := m.serverSettings.Smtp + + auth := smtp.PlainAuth("", s.User, s.Pass, s.Host) + + msg := fmt.Sprintf("From: %v <%v>\nTo: %v\nSubject: %v\nMIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n%v", s.FromName, s.FromMail, to, subject, message) + + return smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg)) +} diff --git a/service/workout.go b/service/workout.go index 40818b0..8ea45de 100644 --- a/service/workout.go +++ b/service/workout.go @@ -4,6 +4,7 @@ import ( "log/slog" "me-fit/template" "me-fit/template/workout" + "me-fit/types" "me-fit/utils" "database/sql" @@ -12,7 +13,7 @@ import ( "time" ) -func HandleWorkoutPage(db *sql.DB) http.HandlerFunc { +func HandleWorkoutPage(db *sql.DB, serverSettings *types.ServerSettings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := utils.GetUser(r) if user == nil { @@ -23,7 +24,7 @@ func HandleWorkoutPage(db *sql.DB) http.HandlerFunc { currentDate := time.Now().Format("2006-01-02") inner := workout.WorkoutComp(currentDate) userComp := UserInfoComp(user) - err := template.Layout(inner, userComp).Render(r.Context(), w) + err := template.Layout(inner, userComp, serverSettings.Environment).Render(r.Context(), w) if err != nil { utils.LogError("Failed to render workout page", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) diff --git a/template/layout.templ b/template/layout.templ index 7949a66..ad163a4 100644 --- a/template/layout.templ +++ b/template/layout.templ @@ -1,8 +1,6 @@ package template -import "me-fit/utils" - -templ Layout(slot templ.Component, user templ.Component) { +templ Layout(slot templ.Component, user templ.Component, environment string) { @@ -11,7 +9,7 @@ templ Layout(slot templ.Component, user templ.Component) { - if utils.Environment == "prod" { + if environment == "prod" { } diff --git a/template/mail/register.templ b/template/mail/register.templ index 3d52cf6..2c15dfc 100644 --- a/template/mail/register.templ +++ b/template/mail/register.templ @@ -1,11 +1,8 @@ package mail; -import ( - "me-fit/utils" - "net/url" -) +import "net/url" -templ Register(token string) { +templ Register(baseUrl string, token string) { @@ -15,7 +12,7 @@ templ Register(token string) {

Thank you for Sign Up!

-

Click @@ -15,7 +12,7 @@ templ ResetPassword(token string) {

Reset your password

-

Click \nTo: %v\nSubject: %v\nMIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n%v", SmtpFromName, SmtpFromMail, to, subject, message) - - return smtp.SendMail(SmtpHost+":"+SmtpPort, auth, SmtpFromMail, []string{to}, []byte(msg)) -}