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

@@ -0,0 +1,52 @@
package main
import (
"database/sql"
"projectreshoot/internal/config"
"projectreshoot/internal/handler"
"projectreshoot/internal/models"
"projectreshoot/pkg/contexts"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"github.com/pkg/errors"
)
func setupAuth(
config *config.Config,
logger *hlog.Logger,
conn *sql.DB,
server *hws.Server,
ignoredPaths []string,
) (*hwsauth.Authenticator[*models.User], error) {
auth, err := hwsauth.NewAuthenticator(
models.GetUserFromID,
server,
conn,
logger,
handler.NewErrorPage,
)
if err != nil {
return nil, errors.Wrap(err, "hwsauth.NewAuthenticator")
}
auth.SSL = config.SSL
auth.AccessTokenExpiry = config.AccessTokenExpiry
auth.RefreshTokenExpiry = config.RefreshTokenExpiry
auth.TokenFreshTime = config.TokenFreshTime
auth.TrustedHost = config.TrustedHost
auth.SecretKey = config.SecretKey
auth.LandingPage = "/profile"
auth.IgnorePaths(ignoredPaths...)
err = auth.Initialise()
if err != nil {
return nil, errors.Wrap(err, "auth.Initialise")
}
contexts.CurrentUser = auth.CurrentModel
return auth, nil
}

View File

@@ -0,0 +1,69 @@
package main
import (
"database/sql"
"git.haelnorr.com/h/golib/hws"
"io/fs"
"net/http"
"projectreshoot/internal/config"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
func setupHttpServer(
staticFS *fs.FS,
config *config.Config,
logger *hlog.Logger,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
) (server *hws.Server, err error) {
if staticFS == nil {
return nil, errors.New("No filesystem provided")
}
fs := http.FS(*staticFS)
httpServer, err := hws.NewServer(
config.Host,
config.Port,
config.ReadHeaderTimeout,
config.WriteTimeout,
config.IdleTimeout,
config.GZIP,
)
if err != nil {
return nil, errors.Wrap(err, "hws.NewServer")
}
ignoredPaths := []string{
"/static/css/output.css",
"/static/favicon.ico",
}
auth, err := setupAuth(config, logger, conn, httpServer, ignoredPaths)
if err != nil {
return nil, errors.Wrap(err, "setupAuth")
}
err = httpServer.AddLogger(logger)
if err != nil {
return nil, errors.Wrap(err, "httpServer.AddLogger")
}
err = httpServer.LoggerIgnorePaths(ignoredPaths...)
if err != nil {
return nil, errors.Wrap(err, "httpServer.LoggerIgnorePaths")
}
err = addRoutes(httpServer, &fs, config, logger, conn, tokenGen, auth)
if err != nil {
return nil, errors.Wrap(err, "addRoutes")
}
err = addMiddleware(httpServer, auth)
if err != nil {
return nil, errors.Wrap(err, "httpServer.AddMiddleware")
}
return httpServer, nil
}

View File

@@ -0,0 +1,23 @@
package main
import (
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"projectreshoot/internal/models"
"github.com/pkg/errors"
)
func addMiddleware(
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
) error {
err := server.AddMiddleware(
auth.Authenticate(),
)
if err != nil {
return errors.Wrap(err, "server.AddMiddleware")
}
return nil
}

View File

@@ -0,0 +1,127 @@
package main
import (
"database/sql"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"net/http"
"projectreshoot/internal/config"
"projectreshoot/internal/handler"
"projectreshoot/internal/models"
"projectreshoot/internal/view/page"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
func addRoutes(
server *hws.Server,
staticFS *http.FileSystem,
config *config.Config,
logger *hlog.Logger,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
auth *hwsauth.Authenticator[*models.User],
) error {
// Create the routes
routes := []hws.Route{
{
Path: "/static/",
Method: hws.MethodGET,
Handler: http.StripPrefix("/static/", handler.StaticFS(staticFS, logger)),
},
{
Path: "/",
Method: hws.MethodGET,
Handler: handler.Root(),
},
{
Path: "/about",
Method: hws.MethodGET,
Handler: handler.HandlePage(page.About()),
},
{
Path: "/login",
Method: hws.MethodGET,
Handler: auth.LogoutReq(handler.LoginPage(config.TrustedHost)),
},
{
Path: "/login",
Method: hws.MethodPOST,
Handler: auth.LogoutReq(handler.LoginRequest(server, auth, conn)),
},
{
Path: "/register",
Method: hws.MethodGET,
Handler: auth.LogoutReq(handler.RegisterPage(config.TrustedHost)),
},
{
Path: "/register",
Method: hws.MethodPOST,
Handler: auth.LogoutReq(handler.RegisterRequest(config, logger, conn, tokenGen)),
},
{
Path: "/logout",
Method: hws.MethodPOST,
Handler: handler.Logout(server, auth, conn),
},
{
Path: "/reauthenticate",
Method: hws.MethodPOST,
Handler: auth.LoginReq(handler.Reauthenticate(server, auth, conn)),
},
{
Path: "/profile",
Method: hws.MethodGET,
Handler: auth.LoginReq(handler.ProfilePage()),
},
{
Path: "/account",
Method: hws.MethodGET,
Handler: auth.LoginReq(handler.AccountPage()),
},
{
Path: "/account-select-page",
Method: hws.MethodPOST,
Handler: auth.LoginReq(handler.AccountSubpage()),
},
{
Path: "/change-username",
Method: hws.MethodPOST,
Handler: auth.LoginReq(auth.FreshReq(handler.ChangeUsername(server, auth, conn))),
},
{
Path: "/change-password",
Method: hws.MethodPOST,
Handler: auth.LoginReq(auth.FreshReq(handler.ChangePassword(server, auth, conn))),
},
{
Path: "/change-bio",
Method: hws.MethodPOST,
Handler: auth.LoginReq(handler.ChangeBio(server, auth, conn)),
},
{
Path: "/movies",
Method: hws.MethodGET,
Handler: handler.MoviesPage(),
},
{
Path: "/search-movies",
Method: hws.MethodPOST,
Handler: handler.SearchMovies(config, logger),
},
{
Path: "/movie/{movie_id}",
Method: hws.MethodGET,
Handler: handler.Movie(config, logger),
},
}
// Register the routes with the server
err := server.AddRoutes(routes...)
if err != nil {
return errors.Wrap(err, "server.AddRoutes")
}
return nil
}

View File

@@ -4,11 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"os/signal" "os/signal"
"projectreshoot/internal/config" "projectreshoot/internal/config"
"projectreshoot/internal/httpserver"
"projectreshoot/pkg/embedfs" "projectreshoot/pkg/embedfs"
"sync" "sync"
"time" "time"
@@ -62,16 +60,17 @@ func run(ctx context.Context, w io.Writer, args map[string]string, config *confi
) )
logger.Debug().Msg("Setting up HTTP server") logger.Debug().Msg("Setting up HTTP server")
httpServer := httpserver.NewServer(config, logger, conn, tokenGen, &staticFS) httpServer, err := setupHttpServer(&staticFS, config, logger, conn, tokenGen)
if err != nil {
return errors.Wrap(err, "setupHttpServer")
}
// Runs the http server // Runs the http server
logger.Debug().Msg("Starting up the HTTP server") logger.Debug().Msg("Starting up the HTTP server")
go func() { err = httpServer.Start()
logger.Info().Str("address", httpServer.Addr).Msg("Listening for requests") if err != nil {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { return errors.Wrap(err, "httpServer.Start")
logger.Error().Err(err).Msg("Error listening and serving") }
}
}()
// Handles graceful shutdown // Handles graceful shutdown
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -80,9 +79,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string, config *confi
shutdownCtx := context.Background() shutdownCtx := context.Background()
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer cancel() defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil { httpServer.Shutdown(shutdownCtx)
logger.Error().Err(err).Msg("Error shutting down server")
}
}) })
wg.Wait() wg.Wait()
logger.Info().Msg("Shutting down") logger.Info().Msg("Shutting down")

2
go.mod
View File

@@ -6,6 +6,8 @@ require (
git.haelnorr.com/h/golib/cookies v0.9.0 git.haelnorr.com/h/golib/cookies v0.9.0
git.haelnorr.com/h/golib/env v0.9.0 git.haelnorr.com/h/golib/env v0.9.0
git.haelnorr.com/h/golib/hlog v0.9.0 git.haelnorr.com/h/golib/hlog v0.9.0
git.haelnorr.com/h/golib/hws v0.1.0
git.haelnorr.com/h/golib/hwsauth v0.2.0
git.haelnorr.com/h/golib/jwt v0.9.2 git.haelnorr.com/h/golib/jwt v0.9.2
git.haelnorr.com/h/golib/tmdb v0.8.0 git.haelnorr.com/h/golib/tmdb v0.8.0
github.com/a-h/templ v0.3.977 github.com/a-h/templ v0.3.977

4
go.sum
View File

@@ -4,6 +4,10 @@ git.haelnorr.com/h/golib/env v0.9.0 h1:Ahqr3PbHy7HdWEHUhylzIZy6Gg8mST5UdgKlU2RAh
git.haelnorr.com/h/golib/env v0.9.0/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg= git.haelnorr.com/h/golib/env v0.9.0/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
git.haelnorr.com/h/golib/hlog v0.9.0 h1:ib8n2MdmiRK2TF067p220kXmhDe9aAnlcsgpuv+QpvE= git.haelnorr.com/h/golib/hlog v0.9.0 h1:ib8n2MdmiRK2TF067p220kXmhDe9aAnlcsgpuv+QpvE=
git.haelnorr.com/h/golib/hlog v0.9.0/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk= git.haelnorr.com/h/golib/hlog v0.9.0/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
git.haelnorr.com/h/golib/hws v0.1.0 h1:+0eNq1uGWrGfbS5AgHeGoGDjVfCWuaVu+1wBxgPqyOY=
git.haelnorr.com/h/golib/hws v0.1.0/go.mod h1:b2pbkMaebzmck9TxqGBGzTJPEcB5TWcEHwFknLE7dqM=
git.haelnorr.com/h/golib/hwsauth v0.2.0 h1:rLfTtxo0lBUMuWzEdoS1Y4i8/UiCzDZ5DS+6WC/C974=
git.haelnorr.com/h/golib/hwsauth v0.2.0/go.mod h1:d1oXUstDHqKwCXzcEMdHGC8yoT2S2gwpJkrEo8daCMs=
git.haelnorr.com/h/golib/jwt v0.9.2 h1:l1Ow7DPGACAU54CnMP/NlZjdc4nRD1wr3xZ8a7taRvU= git.haelnorr.com/h/golib/jwt v0.9.2 h1:l1Ow7DPGACAU54CnMP/NlZjdc4nRD1wr3xZ8a7taRvU=
git.haelnorr.com/h/golib/jwt v0.9.2/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4= git.haelnorr.com/h/golib/jwt v0.9.2/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
git.haelnorr.com/h/golib/tmdb v0.8.0 h1:OQ6M2TB8FHm8fJD7/ebfWm63Duzfp0kmFX9genEig34= git.haelnorr.com/h/golib/tmdb v0.8.0 h1:OQ6M2TB8FHm8fJD7/ebfWm63Duzfp0kmFX9genEig34=

View File

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

View File

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

View File

@@ -3,83 +3,18 @@ package handler
import ( import (
"context" "context"
"database/sql" "database/sql"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"net/http" "net/http"
"strings" "projectreshoot/internal/models"
"time" "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 // Handle a logout request
func Logout( func Logout(
server *hws.Server,
auth *hwsauth.Authenticator[*models.User],
conn *sql.DB, conn *sql.DB,
tokenGen *jwt.TokenGenerator,
logger *hlog.Logger,
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
@@ -88,21 +23,17 @@ func Logout(
tx, err := conn.BeginTx(ctx, nil) tx, err := conn.BeginTx(ctx, nil)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("Failed to start database transaction") server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Logout failed", err))
w.WriteHeader(http.StatusInternalServerError)
return return
} }
defer tx.Rollback() defer tx.Rollback()
err = revokeTokens(tokenGen, tx, r) err = auth.Logout(tx, w, r)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("Error occured on user logout") server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Logout failed", err))
w.WriteHeader(http.StatusInternalServerError)
return return
} }
tx.Commit() tx.Commit()
cookies.DeleteCookie(w, "access", "/")
cookies.DeleteCookie(w, "refresh", "/")
w.Header().Set("HX-Redirect", "/login") w.Header().Set("HX-Redirect", "/login")
}, },
) )

View File

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

View File

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

View File

@@ -6,90 +6,23 @@ import (
"net/http" "net/http"
"time" "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/component/form"
"projectreshoot/pkg/contexts"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors" "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 // Validate the provided password
func validatePassword( func validatePassword(
auth *hwsauth.Authenticator[*models.User],
tx *sql.Tx, tx *sql.Tx,
r *http.Request, r *http.Request,
) error { ) error {
r.ParseForm() r.ParseForm()
password := r.FormValue("password") password := r.FormValue("password")
user := contexts.GetUser(r.Context()) user := auth.CurrentModel(r.Context())
err := user.CheckPassword(tx, password) err := user.CheckPassword(tx, password)
if err != nil { if err != nil {
return errors.Wrap(err, "user.CheckPassword") return errors.Wrap(err, "user.CheckPassword")
@@ -99,10 +32,9 @@ func validatePassword(
// Handle request to reauthenticate (i.e. make token fresh again) // Handle request to reauthenticate (i.e. make token fresh again)
func Reauthenticate( func Reauthenticate(
logger *hlog.Logger, server *hws.Server,
config *config.Config, auth *hwsauth.Authenticator[*models.User],
conn *sql.DB, conn *sql.DB,
tokenGen *jwt.TokenGenerator,
) http.Handler { ) http.Handler {
return http.HandlerFunc( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
@@ -112,21 +44,19 @@ func Reauthenticate(
// Start the transaction // Start the transaction
tx, err := conn.BeginTx(ctx, nil) tx, err := conn.BeginTx(ctx, nil)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("Failed to start transaction") server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Failed to start transaction", err))
w.WriteHeader(http.StatusInternalServerError)
return return
} }
defer tx.Rollback() defer tx.Rollback()
err = validatePassword(tx, r) err = validatePassword(auth, tx, r)
if err != nil { if err != nil {
w.WriteHeader(445) w.WriteHeader(445)
form.ConfirmPassword("Incorrect password").Render(r.Context(), w) form.ConfirmPassword("Incorrect password").Render(r.Context(), w)
return return
} }
err = refreshTokens(config, tokenGen, tx, w, r) err = auth.RefreshAuthTokens(tx, w, r)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("Failed to refresh user tokens") server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Failed to refresh user tokens", err))
w.WriteHeader(http.StatusInternalServerError)
return return
} }
tx.Commit() tx.Commit()

View File

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

View File

@@ -1,52 +1,23 @@
package handler package handler
import ( import (
"git.haelnorr.com/h/golib/hws"
"net/http" "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 // Handles requests for static files, without allowing access to the
// directory viewer and returning 404 if an exact file is not found // 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( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
nfs := justFilesFilesystem{*staticFS} fs, err := hws.SafeFileServer(staticFS)
fs := http.FileServer(nfs) if err != nil {
w.WriteHeader(http.StatusInternalServerError)
logger.Error().Err(err).Msg("Failed to load file system")
return
}
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
}, },
) )

View File

@@ -1,72 +0,0 @@
package httpserver
import (
"database/sql"
"net/http"
"projectreshoot/internal/config"
"projectreshoot/internal/handler"
"projectreshoot/internal/middleware"
"projectreshoot/internal/view/page"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
)
// Add all the handled routes to the mux
func addRoutes(
mux *http.ServeMux,
logger *hlog.Logger,
config *config.Config,
tokenGen *jwt.TokenGenerator,
conn *sql.DB,
staticFS *http.FileSystem,
) {
route := mux.Handle
loggedIn := middleware.LoginReq
loggedOut := middleware.LogoutReq
fresh := middleware.FreshReq
// Health check
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
// Static files
route("GET /static/", http.StripPrefix("/static/", handler.StaticFS(staticFS)))
// Index page and unhandled catchall (404)
route("GET /", handler.Root())
// Static content, unprotected pages
route("GET /about", handler.HandlePage(page.About()))
// Login page and handlers
route("GET /login", loggedOut(handler.LoginPage(config.TrustedHost)))
route("POST /login", loggedOut(handler.LoginRequest(config, logger, conn, tokenGen)))
// Register page and handlers
route("GET /register", loggedOut(handler.RegisterPage(config.TrustedHost)))
route("POST /register", loggedOut(handler.RegisterRequest(config, tokenGen, logger, conn)))
// Logout
route("POST /logout", handler.Logout(conn, tokenGen, logger))
// Reauthentication request
route("POST /reauthenticate", loggedIn(handler.Reauthenticate(logger, config, conn, tokenGen)))
// Profile page
route("GET /profile", loggedIn(handler.ProfilePage()))
// Account page
route("GET /account", loggedIn(handler.AccountPage()))
route("POST /account-select-page", loggedIn(handler.AccountSubpage()))
route("POST /change-username", loggedIn(fresh(handler.ChangeUsername(logger, conn))))
route("POST /change-bio", loggedIn(handler.ChangeBio(logger, conn)))
route("POST /change-password", loggedIn(fresh(handler.ChangePassword(logger, conn))))
// Movies Search
route("GET /movies", handler.MoviesPage())
route("POST /search-movies", handler.SearchMovies(logger, config))
// Movie page
route("GET /movie/{movie_id}", handler.Movie(logger, config))
}

View File

@@ -1,65 +0,0 @@
package httpserver
import (
"database/sql"
"io/fs"
"net"
"net/http"
"time"
"projectreshoot/internal/config"
"projectreshoot/internal/middleware"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
)
func NewServer(
config *config.Config,
logger *hlog.Logger,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
staticFS *fs.FS,
) *http.Server {
fs := http.FS(*staticFS)
srv := createServer(config, logger, conn, tokenGen, &fs)
httpServer := &http.Server{
Addr: net.JoinHostPort(config.Host, config.Port),
Handler: srv,
ReadHeaderTimeout: config.ReadHeaderTimeout * time.Second,
WriteTimeout: config.WriteTimeout * time.Second,
IdleTimeout: config.IdleTimeout * time.Second,
}
return httpServer
}
// Returns a new http.Handler with all the routes and middleware added
func createServer(
config *config.Config,
logger *hlog.Logger,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
staticFS *http.FileSystem,
) http.Handler {
mux := http.NewServeMux()
addRoutes(
mux,
logger,
config,
tokenGen,
conn,
staticFS,
)
var handler http.Handler = mux
// Add middleware here, must be added in reverse order of execution
// i.e. First in list will get executed last during the request handling
handler = middleware.Logging(logger, handler)
handler = middleware.Authentication(logger, config, conn, tokenGen, handler)
// Gzip
handler = middleware.Gzip(handler, config.GZIP)
// Start the timer for the request chain so logger can have accurate info
handler = middleware.StartTimer(handler)
return handler
}

View File

@@ -1,143 +0,0 @@
package middleware
import (
"context"
"database/sql"
"net/http"
"time"
"projectreshoot/internal/config"
"projectreshoot/internal/handler"
"projectreshoot/internal/models"
"projectreshoot/pkg/contexts"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
// Attempt to use a valid refresh token to generate a new token pair
func refreshAuthTokens(
config *config.Config,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
req *http.Request,
ref *jwt.RefreshToken,
) (*models.User, error) {
user, err := models.GetUserFromID(tx, ref.SUB)
if err != nil {
return nil, errors.Wrap(err, "models.GetUser")
}
rememberMe := map[string]bool{
"session": false,
"exp": true,
}[ref.TTL]
// Set fresh to true because new tokens coming from refresh request
err = jwt.SetTokenCookies(w, req, tokenGen, user.ID, false, rememberMe, config.SSL)
if err != nil {
return nil, errors.Wrap(err, "cookies.SetTokenCookies")
}
// New tokens sent, revoke the used refresh token
err = ref.Revoke(tx)
if err != nil {
return nil, errors.Wrap(err, "ref.Revoke")
}
// Return the authorized user
return user, nil
}
// Check the cookies for token strings and attempt to authenticate them
func getAuthenticatedUser(
config *config.Config,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
r *http.Request,
) (*contexts.AuthenticatedUser, error) {
// Get token strings from cookies
atStr, rtStr := jwt.GetTokenCookies(r)
if atStr == "" && rtStr == "" {
return nil, errors.New("No token strings provided")
}
// Attempt to parse the access token
aT, err := tokenGen.ValidateAccess(tx, atStr)
if err != nil {
// Access token invalid, attempt to parse refresh token
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
if err != nil {
return nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
}
// Refresh token valid, attempt to get a new token pair
user, err := refreshAuthTokens(config, tokenGen, tx, w, r, rT)
if err != nil {
return nil, errors.Wrap(err, "refreshAuthTokens")
}
// New token pair sent, return the authorized user
authUser := contexts.AuthenticatedUser{
User: user,
Fresh: time.Now().Unix(),
}
return &authUser, nil
}
// Access token valid
user, err := models.GetUserFromID(tx, aT.SUB)
if err != nil {
return nil, errors.Wrap(err, "models.GetUser")
}
authUser := contexts.AuthenticatedUser{
User: user,
Fresh: aT.Fresh,
}
return &authUser, nil
}
// Attempt to authenticate the user and add their account details
// to the request context
func Authentication(
logger *hlog.Logger,
config *config.Config,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
next http.Handler,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/static/css/output.css" ||
r.URL.Path == "/static/favicon.ico" {
next.ServeHTTP(w, r)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
// Failed to start transaction, skip auth
logger.Warn().Err(err).
Msg("Skipping Auth - unable to start a transaction")
handler.ErrorPage(http.StatusServiceUnavailable, w, r)
return
}
user, err := getAuthenticatedUser(config, tokenGen, tx, w, r)
if err != nil {
tx.Rollback()
// User auth failed, delete the cookies to avoid repeat requests
cookies.DeleteCookie(w, "access", "/")
cookies.DeleteCookie(w, "refresh", "/")
logger.Debug().
Str("remote_addr", r.RemoteAddr).
Err(err).
Msg("Failed to authenticate user")
next.ServeHTTP(w, r)
return
}
tx.Commit()
uctx := contexts.SetUser(r.Context(), user)
newReq := r.WithContext(uctx)
next.ServeHTTP(w, newReq)
})
}

View File

@@ -1,32 +0,0 @@
package middleware
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
func Gzip(next http.Handler, useGzip bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") ||
!useGzip {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
next.ServeHTTP(gzw, r)
})
}
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}

View File

@@ -1,50 +0,0 @@
package middleware
import (
"net/http"
"projectreshoot/internal/handler"
"projectreshoot/pkg/contexts"
"time"
"git.haelnorr.com/h/golib/hlog"
)
// Wraps the http.ResponseWriter, adding a statusCode field
type wrappedWriter struct {
http.ResponseWriter
statusCode int
}
// Extends WriteHeader to the ResponseWriter to add the status code
func (w *wrappedWriter) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
}
// Middleware to add logs to console with details of the request
func Logging(logger *hlog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/static/css/output.css" ||
r.URL.Path == "/static/favicon.ico" {
next.ServeHTTP(w, r)
return
}
start, err := contexts.GetStartTime(r.Context())
if err != nil {
handler.ErrorPage(http.StatusInternalServerError, w, r)
return
}
wrapped := &wrappedWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(wrapped, r)
logger.Info().
Int("status", wrapped.statusCode).
Str("method", r.Method).
Str("resource", r.URL.Path).
Dur("time_elapsed", time.Since(start)).
Str("remote_addr", r.Header.Get("X-Forwarded-For")).
Msg("Served")
})
}

View File

@@ -1,32 +0,0 @@
package middleware
import (
"net/http"
"projectreshoot/internal/handler"
"projectreshoot/pkg/contexts"
)
// Checks if the user is set in the context and shows 401 page if not logged in
func LoginReq(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := contexts.GetUser(r.Context())
if user == nil {
handler.ErrorPage(http.StatusUnauthorized, w, r)
return
}
next.ServeHTTP(w, r)
})
}
// Checks if the user is set in the context and redirects them to profile if
// they are logged in
func LogoutReq(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := contexts.GetUser(r.Context())
if user != nil {
http.Redirect(w, r, "/profile", http.StatusFound)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -1,21 +0,0 @@
package middleware
import (
"net/http"
"projectreshoot/pkg/contexts"
"time"
)
func FreshReq(
next http.Handler,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := contexts.GetUser(r.Context())
isFresh := time.Now().Before(time.Unix(user.Fresh, 0))
if !isFresh {
w.WriteHeader(444)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -1,18 +0,0 @@
package middleware
import (
"net/http"
"projectreshoot/pkg/contexts"
"time"
)
func StartTimer(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ctx := contexts.SetStart(r.Context(), start)
newReq := r.WithContext(ctx)
next.ServeHTTP(w, newReq)
},
)
}

View File

@@ -8,12 +8,16 @@ import (
) )
type User struct { type User struct {
ID int // Integer ID (index primary key) id int // Integer ID (index primary key)
Username string // Username (unique) Username string // Username (unique)
Created_at int64 // Epoch timestamp when the user was added to the database Created_at int64 // Epoch timestamp when the user was added to the database
Bio string // Short byline set by the user Bio string // Short byline set by the user
} }
func (u User) ID() int {
return u.id
}
// Uses bcrypt to set the users Password_hash from the given password // Uses bcrypt to set the users Password_hash from the given password
func (user *User) SetPassword( func (user *User) SetPassword(
tx *sql.Tx, tx *sql.Tx,
@@ -25,7 +29,7 @@ func (user *User) SetPassword(
} }
newPassword := string(hashedPassword) newPassword := string(hashedPassword)
query := `UPDATE users SET password_hash = ? WHERE id = ?` query := `UPDATE users SET password_hash = ? WHERE id = ?`
_, err = tx.Exec(query, newPassword, user.ID) _, err = tx.Exec(query, newPassword, user.id)
if err != nil { if err != nil {
return errors.Wrap(err, "tx.Exec") return errors.Wrap(err, "tx.Exec")
} }
@@ -35,8 +39,8 @@ func (user *User) SetPassword(
// Uses bcrypt to check if the given password matches the users Password_hash // Uses bcrypt to check if the given password matches the users Password_hash
func (user *User) CheckPassword(tx *sql.Tx, password string) error { func (user *User) CheckPassword(tx *sql.Tx, password string) error {
query := `SELECT password_hash FROM users WHERE id = ? LIMIT 1` query := `SELECT password_hash FROM users WHERE id = ? LIMIT 1`
row := tx.QueryRow(query, user.ID) row := tx.QueryRow(query, user.id)
hashedPassword := "" var hashedPassword string
err := row.Scan(&hashedPassword) err := row.Scan(&hashedPassword)
if err != nil { if err != nil {
return errors.Wrap(err, "row.Scan") return errors.Wrap(err, "row.Scan")
@@ -51,7 +55,7 @@ func (user *User) CheckPassword(tx *sql.Tx, password string) error {
// Change the user's username // Change the user's username
func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error { func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error {
query := `UPDATE users SET username = ? WHERE id = ?` query := `UPDATE users SET username = ? WHERE id = ?`
_, err := tx.Exec(query, newUsername, user.ID) _, err := tx.Exec(query, newUsername, user.id)
if err != nil { if err != nil {
return errors.Wrap(err, "tx.Exec") return errors.Wrap(err, "tx.Exec")
} }
@@ -61,7 +65,7 @@ func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error {
// Change the user's bio // Change the user's bio
func (user *User) ChangeBio(tx *sql.Tx, newBio string) error { func (user *User) ChangeBio(tx *sql.Tx, newBio string) error {
query := `UPDATE users SET bio = ? WHERE id = ?` query := `UPDATE users SET bio = ? WHERE id = ?`
_, err := tx.Exec(query, newBio, user.ID) _, err := tx.Exec(query, newBio, user.id)
if err != nil { if err != nil {
return errors.Wrap(err, "tx.Exec") return errors.Wrap(err, "tx.Exec")
} }

View File

@@ -59,7 +59,7 @@ func scanUserRow(user *User, rows *sql.Rows) error {
return errors.New("User not found") return errors.New("User not found")
} }
err := rows.Scan( err := rows.Scan(
&user.ID, &user.id,
&user.Username, &user.Username,
&user.Created_at, &user.Created_at,
&user.Bio, &user.Bio,

View File

@@ -3,11 +3,11 @@ package account
import "projectreshoot/pkg/contexts" import "projectreshoot/pkg/contexts"
templ ChangeBio(err string, bio string) { templ ChangeBio(err string, bio string) {
{{ user := contexts.CurrentUser(ctx) }}
{{ {{
user := contexts.GetUser(ctx) if bio == "" {
if bio == "" { bio = user.Bio
bio = user.Bio }
}
}} }}
<form <form
hx-post="/change-bio" hx-post="/change-bio"
@@ -16,40 +16,39 @@ templ ChangeBio(err string, bio string) {
x-data={ templ.JSFuncCall("bioComponent", bio, user.Bio, err).CallInline } x-data={ templ.JSFuncCall("bioComponent", bio, user.Bio, err).CallInline }
> >
<script> <script>
function bioComponent(newBio, oldBio, err) { function bioComponent(newBio, oldBio, err) {
return { return {
bio: newBio, bio: newBio,
initialBio: oldBio, initialBio: oldBio,
err: err, err: err,
bioLenText: '', bioLenText: "",
updateTextArea() { updateTextArea() {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.bio) { if (this.$refs.bio) {
this.$refs.bio.style.height = 'auto'; this.$refs.bio.style.height = "auto";
this.$refs.bio.style.height = ` this.$refs.bio.style.height = `
${this.$refs.bio.scrollHeight+20}px`; ${this.$refs.bio.scrollHeight + 20}px`;
}; }
this.bioLenText = `${this.bio.length}/128`; this.bioLenText = `${this.bio.length}/128`;
}); });
}, },
resetBio() { resetBio() {
this.bio = this.initialBio; this.bio = this.initialBio;
this.err = "", ((this.err = ""), this.updateTextArea());
this.updateTextArea(); },
}, init() {
init() { this.$nextTick(() => {
this.$nextTick(() => { // this timeout makes sure the textarea resizes on
// this timeout makes sure the textarea resizes on // page render correctly. seems 20ms is the sweet
// page render correctly. seems 20ms is the sweet // spot between a noticable delay and not working
// spot between a noticable delay and not working setTimeout(() => {
setTimeout(() => { this.updateTextArea();
this.updateTextArea(); }, 20);
}, 20); });
}); },
} };
}; }
} </script>
</script>
<div <div
class="flex flex-col" class="flex flex-col"
> >

View File

@@ -3,11 +3,11 @@ package account
import "projectreshoot/pkg/contexts" import "projectreshoot/pkg/contexts"
templ ChangeUsername(err string, username string) { templ ChangeUsername(err string, username string) {
{{ user := contexts.CurrentUser(ctx) }}
{{ {{
user := contexts.GetUser(ctx) if username == "" {
if username == "" { username = user.Username
username = user.Username }
}
}} }}
<form <form
hx-post="/change-username" hx-post="/change-username"
@@ -18,18 +18,18 @@ templ ChangeUsername(err string, username string) {
).CallInline } ).CallInline }
> >
<script> <script>
function usernameComponent(newUsername, oldUsername, err) { function usernameComponent(newUsername, oldUsername, err) {
return { return {
username: newUsername, username: newUsername,
initialUsername: oldUsername, initialUsername: oldUsername,
err: err, err: err,
resetUsername() { resetUsername() {
this.username = this.initialUsername; this.username = this.initialUsername;
this.err = ""; this.err = "";
}, },
}; };
} }
</script> </script>
<div <div
class="flex flex-col sm:flex-row" class="flex flex-col sm:flex-row"
> >

View File

@@ -8,7 +8,7 @@ templ AccountContainer(subpage string) {
@resize.window="big = window.innerWidth >= 768" @resize.window="big = window.innerWidth >= 768"
> >
@SelectMenu(subpage) @SelectMenu(subpage)
<div class="mt-5 w-full md:ml-[200px] ml-[40px] transition-all duration-300"> <div class="mt-5 w-full md:ml-[200px] ml-10 transition-all duration-300">
<div <div
class="pl-5 text-2xl text-subtext1 border-b class="pl-5 text-2xl text-subtext1 border-b
border-overlay0 w-[90%] mx-auto" border-overlay0 w-[90%] mx-auto"

View File

@@ -21,7 +21,7 @@ templ Navbar() {
<div x-data="{ open: false }"> <div x-data="{ open: false }">
<header class="bg-crust"> <header class="bg-crust">
<div <div
class="mx-auto flex h-16 max-w-screen-xl items-center gap-8 class="mx-auto flex h-16 max-w-7xl items-center gap-8
px-4 sm:px-6 lg:px-8" px-4 sm:px-6 lg:px-8"
> >
<a class="block" href="/"> <a class="block" href="/">

View File

@@ -23,7 +23,7 @@ func getProfileItems() []ProfileItem {
// Returns the right portion of the navbar // Returns the right portion of the navbar
templ navRight() { templ navRight() {
{{ user := contexts.GetUser(ctx) }} {{ user := contexts.CurrentUser(ctx) }}
{{ items := getProfileItems() }} {{ items := getProfileItems() }}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="sm:flex sm:gap-2"> <div class="sm:flex sm:gap-2">

View File

@@ -4,7 +4,7 @@ import "projectreshoot/pkg/contexts"
// Returns the mobile version of the navbar thats only visible when activated // Returns the mobile version of the navbar thats only visible when activated
templ sideNav(navItems []NavItem) { templ sideNav(navItems []NavItem) {
{{ user := contexts.GetUser(ctx) }} {{ user := contexts.CurrentUser(ctx) }}
<div <div
x-show="open" x-show="open"
x-transition x-transition

View File

@@ -4,7 +4,7 @@ import "projectreshoot/internal/view/layout"
import "projectreshoot/pkg/contexts" import "projectreshoot/pkg/contexts"
templ Profile() { templ Profile() {
{{ user := contexts.GetUser(ctx) }} {{ user := contexts.CurrentUser(ctx) }}
@layout.Global("Profile - " + user.Username) { @layout.Global("Profile - " + user.Username) {
<div class=""> <div class="">
Hello, { user.Username } Hello, { user.Username }

View File

@@ -0,0 +1,9 @@
package contexts
import (
"projectreshoot/internal/models"
"git.haelnorr.com/h/golib/hwsauth"
)
var CurrentUser hwsauth.ContextLoader[*models.User]

View File

@@ -1,25 +0,0 @@
package contexts
import (
"context"
"projectreshoot/internal/models"
)
type AuthenticatedUser struct {
*models.User
Fresh int64
}
// Return a new context with the user added in
func SetUser(ctx context.Context, u *AuthenticatedUser) context.Context {
return context.WithValue(ctx, contextKeyAuthorizedUser, u)
}
// Retrieve a user from the given context. Returns nil if not set
func GetUser(ctx context.Context) *AuthenticatedUser {
user, ok := ctx.Value(contextKeyAuthorizedUser).(*AuthenticatedUser)
if !ok {
return nil
}
return user
}

View File

@@ -15,6 +15,7 @@
--breakpoint-xl: 80rem; --breakpoint-xl: 80rem;
--container-md: 28rem; --container-md: 28rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-7xl: 80rem;
--text-xs: 0.75rem; --text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75); --text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem; --text-sm: 0.875rem;
@@ -326,8 +327,8 @@
.ml-0 { .ml-0 {
margin-left: calc(var(--spacing) * 0); margin-left: calc(var(--spacing) * 0);
} }
.ml-\[40px\] { .ml-10 {
margin-left: 40px; margin-left: calc(var(--spacing) * 10);
} }
.ml-auto { .ml-auto {
margin-left: auto; margin-left: auto;
@@ -424,6 +425,9 @@
.max-w-4xl { .max-w-4xl {
max-width: var(--container-4xl); max-width: var(--container-4xl);
} }
.max-w-7xl {
max-width: var(--container-7xl);
}
.max-w-100 { .max-w-100 {
max-width: calc(var(--spacing) * 100); max-width: calc(var(--spacing) * 100);
} }