From be3f7f4d32c2e4cc652ebc0724048d67e809febc Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Sun, 11 Jan 2026 23:32:31 +1100 Subject: [PATCH] updated docs --- HWS.md | 58 +++++++---- HWSAuth.md | 285 ++++++++++++++++++++++------------------------------- 2 files changed, 156 insertions(+), 187 deletions(-) diff --git a/HWS.md b/HWS.md index 13279da..fe4aa70 100644 --- a/HWS.md +++ b/HWS.md @@ -700,18 +700,20 @@ server.LoggerIgnorePaths("/static/", "/assets/", "/favicon.ico") ### Starting the Server ```go -ctx := context.Background() +// Start the server (runs in background goroutine) +logger.Debug().Msg("Starting up the HTTP server") err := server.Start(ctx) if err != nil { - return err + return errors.Wrap(err, "server.Start") } ``` The `Start` method: 1. Validates that routes have been added 2. Applies middleware if not already done -3. Starts the HTTP server in a goroutine -4. Begins polling for server readiness +3. Starts the HTTP server in a background goroutine +4. Returns immediately, allowing your code to continue +5. Server listens for context cancellation to shutdown ### Checking Server Status @@ -732,30 +734,44 @@ addr := server.Addr() // e.g., "127.0.0.1:3000" ```go import ( + "context" "os" "os/signal" - "syscall" + "sync" + "time" ) -func main() { - // ... server setup ... - - server.Start(ctx) - - // Wait for interrupt signal - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - <-sigChan - - // Shutdown with timeout - shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +func run() error { + // Create context that listens for interrupt signals + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - if err := server.Shutdown(shutdownCtx); err != nil { - logger.Fatal("Shutdown failed", err) + // ... server setup ... + + // Start server + err := server.Start(ctx) + if err != nil { + return err } - logger.Info("Server shutdown complete") + logger.Info().Msgf("Server started on %s", server.Addr()) + + // Handle graceful shutdown + var wg sync.WaitGroup + wg.Go(func() { + <-ctx.Done() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := server.Shutdown(shutdownCtx); err != nil { + logger.Error().Err(err).Msg("Graceful shutdown failed") + } + }) + + wg.Wait() + logger.Info().Msg("Server shutdown complete") + return nil } ``` @@ -1160,3 +1176,5 @@ Check that: - [Issue Tracker](https://git.haelnorr.com/h/golib/hws/issues) - [Examples](https://git.haelnorr.com/h/golib/hws/tree/master/examples) + + diff --git a/HWSAuth.md b/HWSAuth.md index df158cb..e1281da 100644 --- a/HWSAuth.md +++ b/HWSAuth.md @@ -4,7 +4,9 @@ JWT-based authentication middleware for the hws web framework. ## Overview -`hwsauth` provides a complete authentication solution for hws web applications using JSON Web Tokens (JWT). It handles access tokens, refresh tokens, automatic token rotation, and integrates seamlessly with any database or ORM. +`hwsauth` provides a complete authentication solution for HWS web applications using JSON Web Tokens (JWT). It handles access tokens, refresh tokens, automatic token rotation, and integrates seamlessly with any database or ORM. + +**Database Flexibility**: hwsauth works with any database backend - from the standard library's `database/sql` to popular ORMs like GORM, Bun, SQLC, and Ent. The examples below use Bun ORM, but you can easily adapt them to your preferred database solution. See the [ORM Integration](#orm-integration) section for examples with different backends. ## Installation @@ -337,116 +339,107 @@ Function to create database transactions. ## Complete Examples +The examples below focus on HWSAuth-specific functionality using basic error handling. For complete HWS server setup, middleware configuration, and advanced error handling patterns, see the [HWS documentation](./HWS.md). + ### Route Setup with LoginReq, LogoutReq, and FreshReq ```go -func setupRoutes( - server *hws.Server, - auth *hwsauth.Authenticator[*User, bun.Tx], - db *bun.DB, -) error { - routes := []hws.Route{ - // Public routes - no authentication required - { - Path: "/", - Method: hws.MethodGET, - Handler: homeHandler, - }, - - // LogoutReq - requires user to NOT be logged in - // Redirects authenticated users away - { - Path: "/login", - Method: hws.MethodGET, - Handler: auth.LogoutReq(loginPageHandler), - }, - { - Path: "/login", - Method: hws.MethodPOST, - Handler: auth.LogoutReq(loginSubmitHandler(server, auth, db)), - }, - { - Path: "/register", - Method: hws.MethodGET, - Handler: auth.LogoutReq(registerPageHandler), - }, - { - Path: "/register", - Method: hws.MethodPOST, - Handler: auth.LogoutReq(registerSubmitHandler(server, auth, db)), - }, - - // Logout - accessible to anyone - { - Path: "/logout", - Method: hws.MethodPOST, - Handler: logoutHandler(server, auth, db), - }, - - // LoginReq - requires user to be authenticated - { - Path: "/dashboard", - Method: hws.MethodGET, - Handler: auth.LoginReq(dashboardHandler), - }, - { - Path: "/profile", - Method: hws.MethodGET, - Handler: auth.LoginReq(profileHandler), - }, - - // Reauthentication endpoint for sensitive operations - { - Path: "/reauthenticate", - Method: hws.MethodPOST, - Handler: auth.LoginReq(reauthenticateHandler(server, auth, db)), - }, - - // FreshReq - requires fresh authentication (recent login) - // Used for sensitive operations like changing password/username - { - Path: "/change-password", - Method: hws.MethodPOST, - Handler: auth.LoginReq(auth.FreshReq(changePasswordHandler(server, auth, db))), - }, - { - Path: "/change-username", - Method: hws.MethodPOST, - Handler: auth.LoginReq(auth.FreshReq(changeUsernameHandler(server, auth, db))), - }, - - // Regular authenticated routes (no fresh token required) - { - Path: "/change-bio", - Method: hws.MethodPOST, - Handler: auth.LoginReq(changeBioHandler(server, auth, db)), - }, - } - - return server.AddRoutes(routes...) +routes := []hws.Route{ + // Public routes - no authentication required + { + Path: "/", + Method: hws.MethodGET, + Handler: homeHandler, + }, + + // LogoutReq - requires user to NOT be logged in + // Redirects authenticated users away + { + Path: "/login", + Method: hws.MethodGET, + Handler: auth.LogoutReq(loginPageHandler), + }, + { + Path: "/login", + Method: hws.MethodPOST, + Handler: auth.LogoutReq(loginSubmitHandler(auth, db)), + }, + { + Path: "/register", + Method: hws.MethodGET, + Handler: auth.LogoutReq(registerPageHandler), + }, + { + Path: "/register", + Method: hws.MethodPOST, + Handler: auth.LogoutReq(registerSubmitHandler(auth, db)), + }, + + // Logout - accessible to anyone + { + Path: "/logout", + Method: hws.MethodPOST, + Handler: logoutHandler(auth, db), + }, + + // LoginReq - requires user to be authenticated + { + Path: "/dashboard", + Method: hws.MethodGET, + Handler: auth.LoginReq(dashboardHandler), + }, + { + Path: "/profile", + Method: hws.MethodGET, + Handler: auth.LoginReq(profileHandler), + }, + + // Reauthentication endpoint for sensitive operations + { + Path: "/reauthenticate", + Method: hws.MethodPOST, + Handler: auth.LoginReq(reauthenticateHandler(auth, db)), + }, + + // FreshReq - requires fresh authentication (recent login) + // Used for sensitive operations like changing password/username + { + Path: "/change-password", + Method: hws.MethodPOST, + Handler: auth.LoginReq(auth.FreshReq(changePasswordHandler(auth, db))), + }, + { + Path: "/change-username", + Method: hws.MethodPOST, + Handler: auth.LoginReq(auth.FreshReq(changeUsernameHandler(auth, db))), + }, + + // Regular authenticated routes (no fresh token required) + { + Path: "/change-bio", + Method: hws.MethodPOST, + Handler: auth.LoginReq(changeBioHandler(auth, db)), + }, } + +server.AddRoutes(routes...) ``` ### Login Handler with Transaction Management ```go func loginSubmitHandler( - server *hws.Server, auth *hwsauth.Authenticator[*User, bun.Tx], db *bun.DB, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() // Start database transaction tx, err := db.BeginTx(ctx, nil) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusServiceUnavailable, - Message: "Login failed", - Error: err, - }) + http.Error(w, "Login failed", http.StatusServiceUnavailable) return } defer tx.Rollback() @@ -475,19 +468,13 @@ func loginSubmitHandler( // Login user - sets authentication cookies err = auth.Login(w, r, user, rememberMe) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Login failed", - Error: err, - }) + http.Error(w, "Login failed", http.StatusInternalServerError) return } tx.Commit() - - // Redirect to dashboard or previous page http.Redirect(w, r, "/dashboard", http.StatusSeeOther) - }) + } } ``` @@ -495,21 +482,16 @@ func loginSubmitHandler( ```go func registerSubmitHandler( - server *hws.Server, auth *hwsauth.Authenticator[*User, bun.Tx], db *bun.DB, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := db.BeginTx(ctx, nil) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusServiceUnavailable, - Message: "Registration failed", - Error: err, - }) + http.Error(w, "Registration failed", http.StatusServiceUnavailable) return } defer tx.Rollback() @@ -528,11 +510,7 @@ func registerSubmitHandler( // Check if username is unique exists, err := usernameExists(ctx, tx, username) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Registration failed", - Error: err, - }) + http.Error(w, "Registration failed", http.StatusInternalServerError) return } if exists { @@ -543,11 +521,7 @@ func registerSubmitHandler( // Create user user, err := createUser(ctx, tx, username, password) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Registration failed", - Error: err, - }) + http.Error(w, "Registration failed", http.StatusInternalServerError) return } @@ -555,17 +529,13 @@ func registerSubmitHandler( rememberMe := r.FormValue("remember-me") == "on" err = auth.Login(w, r, user, rememberMe) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Login failed", - Error: err, - }) + http.Error(w, "Login failed", http.StatusInternalServerError) return } tx.Commit() http.Redirect(w, r, "/dashboard", http.StatusSeeOther) - }) + } } ``` @@ -573,21 +543,16 @@ func registerSubmitHandler( ```go func logoutHandler( - server *hws.Server, auth *hwsauth.Authenticator[*User, bun.Tx], db *bun.DB, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := db.BeginTx(ctx, nil) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Logout failed", - Error: err, - }) + http.Error(w, "Logout failed", http.StatusInternalServerError) return } defer tx.Rollback() @@ -595,17 +560,13 @@ func logoutHandler( // Logout - clears cookies and revokes tokens in database err = auth.Logout(tx, w, r) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Logout failed", - Error: err, - }) + http.Error(w, "Logout failed", http.StatusInternalServerError) return } tx.Commit() http.Redirect(w, r, "/login", http.StatusSeeOther) - }) + } } ``` @@ -613,21 +574,16 @@ func logoutHandler( ```go func reauthenticateHandler( - server *hws.Server, auth *hwsauth.Authenticator[*User, bun.Tx], db *bun.DB, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := db.BeginTx(ctx, nil) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Reauthentication failed", - Error: err, - }) + http.Error(w, "Reauthentication failed", http.StatusInternalServerError) return } defer tx.Rollback() @@ -648,17 +604,13 @@ func reauthenticateHandler( // Refresh tokens to make them "fresh" err = auth.RefreshAuthTokens(tx, w, r) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Failed to refresh tokens", - Error: err, - }) + http.Error(w, "Failed to refresh tokens", http.StatusInternalServerError) return } tx.Commit() w.WriteHeader(http.StatusOK) - }) + } } ``` @@ -688,21 +640,16 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) { ```go func changePasswordHandler( - server *hws.Server, auth *hwsauth.Authenticator[*User, bun.Tx], db *bun.DB, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := db.BeginTx(ctx, nil) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusServiceUnavailable, - Message: "Failed to change password", - Error: err, - }) + http.Error(w, "Failed to change password", http.StatusServiceUnavailable) return } defer tx.Rollback() @@ -722,17 +669,13 @@ func changePasswordHandler( // Update password err = user.UpdatePassword(ctx, tx, newPassword) if err != nil { - server.ThrowError(w, r, hws.HWSError{ - StatusCode: http.StatusInternalServerError, - Message: "Failed to change password", - Error: err, - }) + http.Error(w, "Failed to change password", http.StatusInternalServerError) return } tx.Commit() http.Redirect(w, r, "/profile", http.StatusSeeOther) - }) + } } ``` @@ -773,3 +716,11 @@ func changePasswordHandler( - [Issue Tracker](https://git.haelnorr.com/h/golib/hwsauth/issues) - [Examples](https://git.haelnorr.com/h/golib/hwsauth/tree/master/examples) + + + + + + + +