updated docs

2026-01-11 23:32:31 +11:00
parent 7675e47d3c
commit be3f7f4d32
2 changed files with 156 additions and 187 deletions

58
HWS.md

@@ -700,18 +700,20 @@ server.LoggerIgnorePaths("/static/", "/assets/", "/favicon.ico")
### Starting the Server ### Starting the Server
```go ```go
ctx := context.Background() // Start the server (runs in background goroutine)
logger.Debug().Msg("Starting up the HTTP server")
err := server.Start(ctx) err := server.Start(ctx)
if err != nil { if err != nil {
return err return errors.Wrap(err, "server.Start")
} }
``` ```
The `Start` method: The `Start` method:
1. Validates that routes have been added 1. Validates that routes have been added
2. Applies middleware if not already done 2. Applies middleware if not already done
3. Starts the HTTP server in a goroutine 3. Starts the HTTP server in a background goroutine
4. Begins polling for server readiness 4. Returns immediately, allowing your code to continue
5. Server listens for context cancellation to shutdown
### Checking Server Status ### Checking Server Status
@@ -732,30 +734,44 @@ addr := server.Addr() // e.g., "127.0.0.1:3000"
```go ```go
import ( import (
"context"
"os" "os"
"os/signal" "os/signal"
"syscall" "sync"
"time"
) )
func main() { func run() error {
// ... server setup ... // Create context that listens for interrupt signals
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
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)
defer cancel() defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil { // ... server setup ...
logger.Fatal("Shutdown failed", err)
// 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) - [Issue Tracker](https://git.haelnorr.com/h/golib/hws/issues)
- [Examples](https://git.haelnorr.com/h/golib/hws/tree/master/examples) - [Examples](https://git.haelnorr.com/h/golib/hws/tree/master/examples)

@@ -4,7 +4,9 @@ JWT-based authentication middleware for the hws web framework.
## Overview ## 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 ## Installation
@@ -337,116 +339,107 @@ Function to create database transactions.
## Complete Examples ## 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 ### Route Setup with LoginReq, LogoutReq, and FreshReq
```go ```go
func setupRoutes( routes := []hws.Route{
server *hws.Server, // Public routes - no authentication required
auth *hwsauth.Authenticator[*User, bun.Tx], {
db *bun.DB, Path: "/",
) error { Method: hws.MethodGET,
routes := []hws.Route{ Handler: homeHandler,
// Public routes - no authentication required },
{
Path: "/", // LogoutReq - requires user to NOT be logged in
Method: hws.MethodGET, // Redirects authenticated users away
Handler: homeHandler, {
}, Path: "/login",
Method: hws.MethodGET,
// LogoutReq - requires user to NOT be logged in Handler: auth.LogoutReq(loginPageHandler),
// Redirects authenticated users away },
{ {
Path: "/login", Path: "/login",
Method: hws.MethodGET, Method: hws.MethodPOST,
Handler: auth.LogoutReq(loginPageHandler), Handler: auth.LogoutReq(loginSubmitHandler(auth, db)),
}, },
{ {
Path: "/login", Path: "/register",
Method: hws.MethodPOST, Method: hws.MethodGET,
Handler: auth.LogoutReq(loginSubmitHandler(server, auth, db)), Handler: auth.LogoutReq(registerPageHandler),
}, },
{ {
Path: "/register", Path: "/register",
Method: hws.MethodGET, Method: hws.MethodPOST,
Handler: auth.LogoutReq(registerPageHandler), Handler: auth.LogoutReq(registerSubmitHandler(auth, db)),
}, },
{
Path: "/register", // Logout - accessible to anyone
Method: hws.MethodPOST, {
Handler: auth.LogoutReq(registerSubmitHandler(server, auth, db)), Path: "/logout",
}, Method: hws.MethodPOST,
Handler: logoutHandler(auth, db),
// Logout - accessible to anyone },
{
Path: "/logout", // LoginReq - requires user to be authenticated
Method: hws.MethodPOST, {
Handler: logoutHandler(server, auth, db), Path: "/dashboard",
}, Method: hws.MethodGET,
Handler: auth.LoginReq(dashboardHandler),
// LoginReq - requires user to be authenticated },
{ {
Path: "/dashboard", Path: "/profile",
Method: hws.MethodGET, Method: hws.MethodGET,
Handler: auth.LoginReq(dashboardHandler), Handler: auth.LoginReq(profileHandler),
}, },
{
Path: "/profile", // Reauthentication endpoint for sensitive operations
Method: hws.MethodGET, {
Handler: auth.LoginReq(profileHandler), Path: "/reauthenticate",
}, Method: hws.MethodPOST,
Handler: auth.LoginReq(reauthenticateHandler(auth, db)),
// Reauthentication endpoint for sensitive operations },
{
Path: "/reauthenticate", // FreshReq - requires fresh authentication (recent login)
Method: hws.MethodPOST, // Used for sensitive operations like changing password/username
Handler: auth.LoginReq(reauthenticateHandler(server, auth, db)), {
}, Path: "/change-password",
Method: hws.MethodPOST,
// FreshReq - requires fresh authentication (recent login) Handler: auth.LoginReq(auth.FreshReq(changePasswordHandler(auth, db))),
// Used for sensitive operations like changing password/username },
{ {
Path: "/change-password", Path: "/change-username",
Method: hws.MethodPOST, Method: hws.MethodPOST,
Handler: auth.LoginReq(auth.FreshReq(changePasswordHandler(server, auth, db))), Handler: auth.LoginReq(auth.FreshReq(changeUsernameHandler(auth, db))),
}, },
{
Path: "/change-username", // Regular authenticated routes (no fresh token required)
Method: hws.MethodPOST, {
Handler: auth.LoginReq(auth.FreshReq(changeUsernameHandler(server, auth, db))), Path: "/change-bio",
}, Method: hws.MethodPOST,
Handler: auth.LoginReq(changeBioHandler(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...)
} }
server.AddRoutes(routes...)
``` ```
### Login Handler with Transaction Management ### Login Handler with Transaction Management
```go ```go
func loginSubmitHandler( func loginSubmitHandler(
server *hws.Server,
auth *hwsauth.Authenticator[*User, bun.Tx], auth *hwsauth.Authenticator[*User, bun.Tx],
db *bun.DB, db *bun.DB,
) http.Handler { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() defer cancel()
// Start database transaction // Start database transaction
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Login failed", http.StatusServiceUnavailable)
StatusCode: http.StatusServiceUnavailable,
Message: "Login failed",
Error: err,
})
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -475,19 +468,13 @@ func loginSubmitHandler(
// Login user - sets authentication cookies // Login user - sets authentication cookies
err = auth.Login(w, r, user, rememberMe) err = auth.Login(w, r, user, rememberMe)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Login failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Login failed",
Error: err,
})
return return
} }
tx.Commit() tx.Commit()
// Redirect to dashboard or previous page
http.Redirect(w, r, "/dashboard", http.StatusSeeOther) http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}) }
} }
``` ```
@@ -495,21 +482,16 @@ func loginSubmitHandler(
```go ```go
func registerSubmitHandler( func registerSubmitHandler(
server *hws.Server,
auth *hwsauth.Authenticator[*User, bun.Tx], auth *hwsauth.Authenticator[*User, bun.Tx],
db *bun.DB, db *bun.DB,
) http.Handler { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel() defer cancel()
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Registration failed", http.StatusServiceUnavailable)
StatusCode: http.StatusServiceUnavailable,
Message: "Registration failed",
Error: err,
})
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -528,11 +510,7 @@ func registerSubmitHandler(
// Check if username is unique // Check if username is unique
exists, err := usernameExists(ctx, tx, username) exists, err := usernameExists(ctx, tx, username)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Registration failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Registration failed",
Error: err,
})
return return
} }
if exists { if exists {
@@ -543,11 +521,7 @@ func registerSubmitHandler(
// Create user // Create user
user, err := createUser(ctx, tx, username, password) user, err := createUser(ctx, tx, username, password)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Registration failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Registration failed",
Error: err,
})
return return
} }
@@ -555,17 +529,13 @@ func registerSubmitHandler(
rememberMe := r.FormValue("remember-me") == "on" rememberMe := r.FormValue("remember-me") == "on"
err = auth.Login(w, r, user, rememberMe) err = auth.Login(w, r, user, rememberMe)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Login failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Login failed",
Error: err,
})
return return
} }
tx.Commit() tx.Commit()
http.Redirect(w, r, "/dashboard", http.StatusSeeOther) http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}) }
} }
``` ```
@@ -573,21 +543,16 @@ func registerSubmitHandler(
```go ```go
func logoutHandler( func logoutHandler(
server *hws.Server,
auth *hwsauth.Authenticator[*User, bun.Tx], auth *hwsauth.Authenticator[*User, bun.Tx],
db *bun.DB, db *bun.DB,
) http.Handler { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel() defer cancel()
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Logout failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Logout failed",
Error: err,
})
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -595,17 +560,13 @@ func logoutHandler(
// Logout - clears cookies and revokes tokens in database // Logout - clears cookies and revokes tokens in database
err = auth.Logout(tx, w, r) err = auth.Logout(tx, w, r)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Logout failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Logout failed",
Error: err,
})
return return
} }
tx.Commit() tx.Commit()
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
}) }
} }
``` ```
@@ -613,21 +574,16 @@ func logoutHandler(
```go ```go
func reauthenticateHandler( func reauthenticateHandler(
server *hws.Server,
auth *hwsauth.Authenticator[*User, bun.Tx], auth *hwsauth.Authenticator[*User, bun.Tx],
db *bun.DB, db *bun.DB,
) http.Handler { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel() defer cancel()
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Reauthentication failed", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Reauthentication failed",
Error: err,
})
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -648,17 +604,13 @@ func reauthenticateHandler(
// Refresh tokens to make them "fresh" // Refresh tokens to make them "fresh"
err = auth.RefreshAuthTokens(tx, w, r) err = auth.RefreshAuthTokens(tx, w, r)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Failed to refresh tokens", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Failed to refresh tokens",
Error: err,
})
return return
} }
tx.Commit() tx.Commit()
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}) }
} }
``` ```
@@ -688,21 +640,16 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) {
```go ```go
func changePasswordHandler( func changePasswordHandler(
server *hws.Server,
auth *hwsauth.Authenticator[*User, bun.Tx], auth *hwsauth.Authenticator[*User, bun.Tx],
db *bun.DB, db *bun.DB,
) http.Handler { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel() defer cancel()
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Failed to change password", http.StatusServiceUnavailable)
StatusCode: http.StatusServiceUnavailable,
Message: "Failed to change password",
Error: err,
})
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -722,17 +669,13 @@ func changePasswordHandler(
// Update password // Update password
err = user.UpdatePassword(ctx, tx, newPassword) err = user.UpdatePassword(ctx, tx, newPassword)
if err != nil { if err != nil {
server.ThrowError(w, r, hws.HWSError{ http.Error(w, "Failed to change password", http.StatusInternalServerError)
StatusCode: http.StatusInternalServerError,
Message: "Failed to change password",
Error: err,
})
return return
} }
tx.Commit() tx.Commit()
http.Redirect(w, r, "/profile", http.StatusSeeOther) http.Redirect(w, r, "/profile", http.StatusSeeOther)
}) }
} }
``` ```
@@ -773,3 +716,11 @@ func changePasswordHandler(
- [Issue Tracker](https://git.haelnorr.com/h/golib/hwsauth/issues) - [Issue Tracker](https://git.haelnorr.com/h/golib/hwsauth/issues)
- [Examples](https://git.haelnorr.com/h/golib/hwsauth/tree/master/examples) - [Examples](https://git.haelnorr.com/h/golib/hwsauth/tree/master/examples)