modularised webserver and auth systems

This commit is contained in:
2026-01-04 01:14:06 +11:00
parent 4a21ba3821
commit 28b7ba34f0
36 changed files with 451 additions and 774 deletions

View File

@@ -6,13 +6,13 @@ import (
"net/http"
"time"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"projectreshoot/internal/models"
"projectreshoot/internal/view/component/account"
"projectreshoot/internal/view/page"
"projectreshoot/pkg/contexts"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hlog"
"github.com/pkg/errors"
)
@@ -45,7 +45,8 @@ func AccountSubpage() http.Handler {
// Handles a request to change the users username
func ChangeUsername(
logger *hlog.Logger,
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB,
) http.Handler {
return http.HandlerFunc(
@@ -56,8 +57,7 @@ func ChangeUsername(
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
logger.Warn().Err(err).Msg("Error updating username")
w.WriteHeader(http.StatusServiceUnavailable)
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Error updating username", err))
return
}
r.ParseForm()
@@ -65,8 +65,7 @@ func ChangeUsername(
unique, err := models.CheckUsernameUnique(tx, newUsername)
if err != nil {
tx.Rollback()
logger.Error().Err(err).Msg("Error updating username")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating username", err))
return
}
if !unique {
@@ -75,12 +74,11 @@ func ChangeUsername(
Render(r.Context(), w)
return
}
user := contexts.GetUser(r.Context())
user := auth.CurrentModel(r.Context())
err = user.ChangeUsername(tx, newUsername)
if err != nil {
tx.Rollback()
logger.Error().Err(err).Msg("Error updating username")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating username", err))
return
}
tx.Commit()
@@ -91,7 +89,8 @@ func ChangeUsername(
// Handles a request to change the users bio
func ChangeBio(
logger *hlog.Logger,
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB,
) http.Handler {
return http.HandlerFunc(
@@ -102,8 +101,7 @@ func ChangeBio(
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
logger.Warn().Err(err).Msg("Error updating bio")
w.WriteHeader(http.StatusServiceUnavailable)
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Error updating bio", err))
return
}
r.ParseForm()
@@ -115,12 +113,11 @@ func ChangeBio(
Render(r.Context(), w)
return
}
user := contexts.GetUser(r.Context())
user := auth.CurrentModel(r.Context())
err = user.ChangeBio(tx, newBio)
if err != nil {
tx.Rollback()
logger.Error().Err(err).Msg("Error updating bio")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating bio", err))
return
}
tx.Commit()
@@ -145,7 +142,8 @@ func validateChangePassword(
// Handles a request to change the users password
func ChangePassword(
logger *hlog.Logger,
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB,
) http.Handler {
return http.HandlerFunc(
@@ -156,8 +154,7 @@ func ChangePassword(
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
logger.Warn().Err(err).Msg("Error updating password")
w.WriteHeader(http.StatusServiceUnavailable)
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Error updating password", err))
return
}
newPass, err := validateChangePassword(r)
@@ -166,12 +163,11 @@ func ChangePassword(
account.ChangePassword(err.Error()).Render(r.Context(), w)
return
}
user := contexts.GetUser(r.Context())
user := auth.CurrentModel(r.Context())
err = user.SetPassword(tx, newPass)
if err != nil {
tx.Rollback()
logger.Error().Err(err).Msg("Error updating password")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating password", err))
return
}
tx.Commit()

View File

@@ -22,3 +22,21 @@ func ErrorPage(
page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
Render(r.Context(), w)
}
func NewErrorPage(
errorCode int,
w http.ResponseWriter,
r *http.Request,
) error {
message := map[int]string{
401: "You need to login to view this page.",
403: "You do not have permission to view this page.",
404: "The page or resource you have requested does not exist.",
500: `An error occured on the server. Please try again, and if this
continues to happen contact an administrator.`,
503: "The server is currently down for maintenance and should be back soon. =)",
}
w.WriteHeader(errorCode)
return page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
Render(r.Context(), w)
}

View File

@@ -4,16 +4,16 @@ import (
"context"
"database/sql"
"net/http"
"strings"
"time"
"projectreshoot/internal/config"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"projectreshoot/internal/models"
"projectreshoot/internal/view/component/form"
"projectreshoot/internal/view/page"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
@@ -32,6 +32,9 @@ func validateLogin(
err = user.CheckPassword(tx, formPassword)
if err != nil {
if !strings.Contains(err.Error(), "Username or password incorrect") {
return nil, errors.Wrap(err, "user.CheckPassword")
}
return nil, errors.New("Username or password incorrect")
}
return user, nil
@@ -51,10 +54,9 @@ func checkRememberMe(r *http.Request) bool {
// and on fail will return the login form again, passing the error to the
// template for user feedback
func LoginRequest(
config *config.Config,
logger *hlog.Logger,
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
@@ -64,8 +66,7 @@ func LoginRequest(
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
logger.Warn().Err(err).Msg("Failed to set token cookies")
w.WriteHeader(http.StatusServiceUnavailable)
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Login failed", err))
return
}
r.ParseForm()
@@ -73,8 +74,7 @@ func LoginRequest(
if err != nil {
tx.Rollback()
if err.Error() != "Username or password incorrect" {
logger.Warn().Caller().Err(err).Msg("Login request failed")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Login failed", err))
} else {
form.LoginForm(err.Error()).Render(r.Context(), w)
}
@@ -82,11 +82,10 @@ func LoginRequest(
}
rememberMe := checkRememberMe(r)
err = jwt.SetTokenCookies(w, r, tokenGen, user.ID, true, rememberMe, config.SSL)
err = auth.Login(w, r, user, rememberMe)
if err != nil {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
logger.Warn().Caller().Err(err).Msg("Failed to set token cookies")
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Login failed", err))
return
}

View File

@@ -3,83 +3,18 @@ package handler
import (
"context"
"database/sql"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"net/http"
"strings"
"projectreshoot/internal/models"
"time"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
func revokeAccess(
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
atStr string,
) error {
aT, err := tokenGen.ValidateAccess(tx, atStr)
if err != nil {
if strings.Contains(err.Error(), "Token is expired") ||
strings.Contains(err.Error(), "Token has been revoked") {
return nil // Token is expired, dont need to revoke it
}
return errors.Wrap(err, "jwt.ParseAccessToken")
}
err = aT.Revoke(tx)
if err != nil {
return errors.Wrap(err, "jwt.RevokeToken")
}
return nil
}
func revokeRefresh(
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
rtStr string,
) error {
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
if err != nil {
if strings.Contains(err.Error(), "Token is expired") ||
strings.Contains(err.Error(), "Token has been revoked") {
return nil // Token is expired, dont need to revoke it
}
return errors.Wrap(err, "jwt.ParseRefreshToken")
}
err = rT.Revoke(tx)
if err != nil {
return errors.Wrap(err, "jwt.RevokeToken")
}
return nil
}
// Retrieve and revoke the user's tokens
func revokeTokens(
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
r *http.Request,
) error {
// get the tokens from the cookies
atStr, rtStr := jwt.GetTokenCookies(r)
// revoke the refresh token first as the access token expires quicker
// only matters if there is an error revoking the tokens
err := revokeRefresh(tokenGen, tx, rtStr)
if err != nil {
return errors.Wrap(err, "revokeRefresh")
}
err = revokeAccess(tokenGen, tx, atStr)
if err != nil {
return errors.Wrap(err, "revokeAccess")
}
return nil
}
// Handle a logout request
func Logout(
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
logger *hlog.Logger,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
@@ -88,21 +23,17 @@ func Logout(
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
logger.Error().Err(err).Msg("Failed to start database transaction")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Logout failed", err))
return
}
defer tx.Rollback()
err = revokeTokens(tokenGen, tx, r)
err = auth.Logout(tx, w, r)
if err != nil {
logger.Error().Err(err).Msg("Error occured on user logout")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Logout failed", err))
return
}
tx.Commit()
cookies.DeleteCookie(w, "access", "/")
cookies.DeleteCookie(w, "refresh", "/")
w.Header().Set("HX-Redirect", "/login")
},
)

View File

@@ -11,8 +11,8 @@ import (
)
func Movie(
logger *hlog.Logger,
config *config.Config,
logger *hlog.Logger,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {

View File

@@ -11,8 +11,8 @@ import (
)
func SearchMovies(
logger *hlog.Logger,
config *config.Config,
logger *hlog.Logger,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {

View File

@@ -6,90 +6,23 @@ import (
"net/http"
"time"
"projectreshoot/internal/config"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"projectreshoot/internal/models"
"projectreshoot/internal/view/component/form"
"projectreshoot/pkg/contexts"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
// Get the tokens from the request
func getTokens(
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
r *http.Request,
) (*jwt.AccessToken, *jwt.RefreshToken, error) {
// get the existing tokens from the cookies
atStr, rtStr := jwt.GetTokenCookies(r)
aT, err := tokenGen.ValidateAccess(tx, atStr)
if err != nil {
return nil, nil, errors.Wrap(err, "tokenGen.ValidateAccess")
}
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
if err != nil {
return nil, nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
}
return aT, rT, nil
}
// Revoke the given token pair
func revokeTokenPair(
tx *sql.Tx,
aT *jwt.AccessToken,
rT *jwt.RefreshToken,
) error {
err := aT.Revoke(tx)
if err != nil {
return errors.Wrap(err, "aT.Revoke")
}
err = rT.Revoke(tx)
if err != nil {
return errors.Wrap(err, "rT.Revoke")
}
return nil
}
// Issue new tokens for the user, invalidating the old ones
func refreshTokens(
config *config.Config,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
r *http.Request,
) error {
aT, rT, err := getTokens(tokenGen, tx, r)
if err != nil {
return errors.Wrap(err, "getTokens")
}
rememberMe := map[string]bool{
"session": false,
"exp": true,
}[aT.TTL]
// issue new tokens for the user
user := contexts.GetUser(r.Context())
err = jwt.SetTokenCookies(w, r, tokenGen, user.ID, true, rememberMe, config.SSL)
if err != nil {
return errors.Wrap(err, "cookies.SetTokenCookies")
}
err = revokeTokenPair(tx, aT, rT)
if err != nil {
return errors.Wrap(err, "revokeTokenPair")
}
return nil
}
// Validate the provided password
func validatePassword(
auth *hwsauth.Authenticator[*models.User],
tx *sql.Tx,
r *http.Request,
) error {
r.ParseForm()
password := r.FormValue("password")
user := contexts.GetUser(r.Context())
user := auth.CurrentModel(r.Context())
err := user.CheckPassword(tx, password)
if err != nil {
return errors.Wrap(err, "user.CheckPassword")
@@ -99,10 +32,9 @@ func validatePassword(
// Handle request to reauthenticate (i.e. make token fresh again)
func Reauthenticate(
logger *hlog.Logger,
config *config.Config,
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
@@ -112,21 +44,19 @@ func Reauthenticate(
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
logger.Error().Err(err).Msg("Failed to start transaction")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Failed to start transaction", err))
return
}
defer tx.Rollback()
err = validatePassword(tx, r)
err = validatePassword(auth, tx, r)
if err != nil {
w.WriteHeader(445)
form.ConfirmPassword("Incorrect password").Render(r.Context(), w)
return
}
err = refreshTokens(config, tokenGen, tx, w, r)
err = auth.RefreshAuthTokens(tx, w, r)
if err != nil {
logger.Error().Err(err).Msg("Failed to refresh user tokens")
w.WriteHeader(http.StatusInternalServerError)
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Failed to refresh user tokens", err))
return
}
tx.Commit()

View File

@@ -48,9 +48,9 @@ func validateRegistration(
func RegisterRequest(
config *config.Config,
tokenGen *jwt.TokenGenerator,
logger *hlog.Logger,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
@@ -80,7 +80,7 @@ func RegisterRequest(
}
rememberMe := checkRememberMe(r)
err = jwt.SetTokenCookies(w, r, tokenGen, user.ID, true, rememberMe, config.SSL)
err = jwt.SetTokenCookies(w, r, tokenGen, user.ID(), true, rememberMe, config.SSL)
if err != nil {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)

View File

@@ -1,52 +1,23 @@
package handler
import (
"git.haelnorr.com/h/golib/hws"
"net/http"
"os"
"git.haelnorr.com/h/golib/hlog"
)
// Wrapper for default FileSystem
type justFilesFilesystem struct {
fs http.FileSystem
}
// Wrapper for default File
type neuteredReaddirFile struct {
http.File
}
// Modifies the behavior of FileSystem.Open to return the neutered version of File
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
// Check if the requested path is a directory
// and explicitly return an error to trigger a 404
fileInfo, err := f.Stat()
if err != nil {
return nil, err
}
if fileInfo.IsDir() {
return nil, os.ErrNotExist
}
return neuteredReaddirFile{f}, nil
}
// Overrides the Readdir method of File to always return nil
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, nil
}
// Handles requests for static files, without allowing access to the
// directory viewer and returning 404 if an exact file is not found
func StaticFS(staticFS *http.FileSystem) http.Handler {
func StaticFS(staticFS *http.FileSystem, logger *hlog.Logger) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
nfs := justFilesFilesystem{*staticFS}
fs := http.FileServer(nfs)
fs, err := hws.SafeFileServer(staticFS)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
logger.Error().Err(err).Msg("Failed to load file system")
return
}
fs.ServeHTTP(w, r)
},
)