big ole refactor

This commit is contained in:
2026-02-14 19:48:59 +11:00
parent f0e7962af5
commit 8a79533de3
66 changed files with 989 additions and 1114 deletions

47
internal/server/auth.go Normal file
View File

@@ -0,0 +1,47 @@
package server
import (
"context"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/handlers"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
func setupAuth(
cfg *hwsauth.Config,
logger *hlog.Logger,
conn *db.DB,
server *hws.Server,
ignoredPaths []string,
) (*hwsauth.Authenticator[*db.User, bun.Tx], error) {
beginTx := func(ctx context.Context) (hwsauth.DBTransaction, error) {
tx, err := conn.BeginTx(ctx, nil)
return tx, err
}
auth, err := hwsauth.NewAuthenticator(
cfg,
db.GetUserByID,
server,
beginTx,
logger,
handlers.ErrorPage,
conn.DB.DB,
)
if err != nil {
return nil, errors.Wrap(err, "hwsauth.NewAuthenticator")
}
err = auth.IgnorePaths(ignoredPaths...)
if err != nil {
return nil, errors.Wrap(err, "auth.IgnorePaths")
}
db.CurrentUser = auth.CurrentModel
return auth, nil
}

View File

@@ -0,0 +1,169 @@
package server
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/contexts"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/discord"
"git.haelnorr.com/h/oslstats/internal/rbac"
"git.haelnorr.com/h/oslstats/internal/store"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
func addMiddleware(
server *hws.Server,
auth *hwsauth.Authenticator[*db.User, bun.Tx],
cfg *config.Config,
perms *rbac.Checker,
discordAPI *discord.APIClient,
store *store.Store,
conn *db.DB,
) error {
err := server.AddMiddleware(
auth.Authenticate(tokenRefresh(auth, discordAPI, store)),
rbac.LoadPreviewRoleMiddleware(server, conn),
perms.LoadPermissionsMiddleware(),
devMode(cfg),
)
if err != nil {
return errors.Wrap(err, "server.AddMiddleware")
}
return nil
}
func devMode(cfg *config.Config) hws.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if cfg.Flags.DevMode {
devInfo := contexts.DevInfo{
WebsocketBase: "ws://" + cfg.HWS.Host + ":" + strconv.FormatUint(cfg.HWS.Port, 10),
HTMXLog: true,
}
ctx := context.WithValue(r.Context(), contexts.DevModeKey, devInfo)
req := r.WithContext(ctx)
next.ServeHTTP(w, req)
return
}
next.ServeHTTP(w, r)
},
)
}
}
func tokenRefresh(
auth *hwsauth.Authenticator[*db.User, bun.Tx],
discordAPI *discord.APIClient,
store *store.Store,
) func(ctx context.Context, user *db.User, tx bun.Tx, w http.ResponseWriter, r *http.Request) (bool, *hws.HWSError) {
return func(ctx context.Context, user *db.User, tx bun.Tx, w http.ResponseWriter, r *http.Request) (bool, *hws.HWSError) {
success, err := refreshToken(ctx, store, discordAPI, user, tx)
if err != nil {
return false, &hws.HWSError{
Error: errors.Wrap(err, "refreshToken"),
Message: "Error refreshing discord token",
Level: hws.ErrorERROR,
RenderErrorPage: true,
StatusCode: http.StatusInternalServerError,
}
}
if !success {
err = auth.Logout(tx, w, r)
if err != nil {
return false, &hws.HWSError{
Error: errors.Wrap(err, "auth.Logout"),
Message: "Logout failed",
Level: hws.ErrorERROR,
RenderErrorPage: true,
StatusCode: http.StatusInternalServerError,
}
}
return false, nil
}
return true, nil
}
}
func refreshToken(
ctx context.Context,
store *store.Store,
discordAPI *discord.APIClient,
user *db.User,
tx bun.Tx,
) (bool, error) {
token := store.CheckToken(user)
if token != nil {
return true, nil
}
// Get the token
token, err := user.GetDiscordToken(ctx, tx)
if err != nil {
return false, errors.Wrap(err, "user.GetDiscordToken")
}
tokenstatus, err := tokenStatus(token)
if err != nil {
return false, errors.Wrap(err, "tokenStatus")
}
switch tokenstatus {
case "revoked":
return false, nil
case "expired", "expiring":
newtoken, err := discordAPI.RefreshToken(token.Convert())
if err != nil {
return false, errors.Wrap(err, "discordAPI.RefreshToken")
}
err = user.UpdateDiscordToken(ctx, tx, newtoken)
if err != nil {
return false, errors.Wrap(err, "user.UpdateDiscordToken")
}
err = store.NewTokenCheck(user, token)
if err != nil {
return false, errors.Wrap(err, "store.NewTokenCheck")
}
return true, nil
case "valid":
err = store.NewTokenCheck(user, token)
if err != nil {
return false, errors.Wrap(err, "store.NewTokenCheck")
}
return true, nil
default:
return false, errors.New("unexpected error occured validating discord token for user")
}
}
func tokenStatus(token *db.DiscordToken) (string, error) {
now := time.Now().Unix()
dayfromnow := now + int64(24*time.Hour/time.Second)
oauthtoken := token.Convert()
session, err := discord.NewOAuthSession(oauthtoken)
if err != nil {
return "", errors.Wrap(err, "discord.NewOAuthSession")
}
_, err = session.GetUser()
if err != nil {
if !strings.Contains(err.Error(), "HTTP 401") {
// Error not related to token status
return "", errors.Wrap(err, "session.GetUser")
}
// Token not valid
if token.ExpiresAt < now {
return "expired", nil
}
return "revoked", nil
}
if token.ExpiresAt < dayfromnow {
return "expiring", nil
}
return "valid", nil
}

312
internal/server/routes.go Normal file
View File

@@ -0,0 +1,312 @@
package server
import (
"net/http"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/discord"
"git.haelnorr.com/h/oslstats/internal/handlers"
"git.haelnorr.com/h/oslstats/internal/permissions"
"git.haelnorr.com/h/oslstats/internal/rbac"
"git.haelnorr.com/h/oslstats/internal/store"
)
func addRoutes(
s *hws.Server,
staticFS *http.FileSystem,
cfg *config.Config,
conn *db.DB,
auth *hwsauth.Authenticator[*db.User, bun.Tx],
store *store.Store,
discordAPI *discord.APIClient,
perms *rbac.Checker,
) error {
// Create the routes
baseRoutes := []hws.Route{
{
Path: "/static/",
Method: hws.MethodGET,
Handler: http.StripPrefix("/static/", handlers.StaticFS(staticFS, s)),
},
{
Path: "/",
Method: hws.MethodGET,
Handler: handlers.Index(s),
},
}
authRoutes := []hws.Route{
{
Path: "/login",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: auth.LogoutReq(handlers.Login(s, conn, cfg, store, discordAPI)),
},
{
Path: "/auth/callback",
Method: hws.MethodGET,
Handler: auth.LogoutReq(handlers.Callback(s, auth, conn, cfg, store, discordAPI)),
},
{
Path: "/register",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: auth.LogoutReq(handlers.Register(s, auth, conn, cfg, store)),
},
{
Path: "/logout",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: auth.LoginReq(handlers.Logout(s, auth, conn, discordAPI)),
},
}
seasonRoutes := []hws.Route{
{
Path: "/seasons",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: handlers.SeasonsPage(s, conn),
},
{
Path: "/seasons/new",
Method: hws.MethodGET,
Handler: perms.RequirePermission(s, permissions.SeasonsCreate)(handlers.NewSeason(s)),
},
{
Path: "/seasons/new",
Method: hws.MethodPOST,
Handler: perms.RequirePermission(s, permissions.SeasonsCreate)(handlers.NewSeasonSubmit(s, conn)),
},
{
Path: "/seasons/{season_short_name}",
Method: hws.MethodGET,
Handler: handlers.SeasonPage(s, conn),
},
{
Path: "/seasons/{season_short_name}/edit",
Method: hws.MethodGET,
Handler: perms.RequirePermission(s, permissions.SeasonsUpdate)(handlers.SeasonEditPage(s, conn)),
},
{
Path: "/seasons/{season_short_name}/edit",
Method: hws.MethodPOST,
Handler: perms.RequirePermission(s, permissions.SeasonsUpdate)(handlers.SeasonEditSubmit(s, conn)),
},
{
Path: "/seasons/{season_short_name}/leagues/{league_short_name}",
Method: hws.MethodGET,
Handler: handlers.SeasonLeaguePage(s, conn),
},
{
Path: "/seasons/{season_short_name}/leagues/add/{league_short_name}",
Method: hws.MethodPOST,
Handler: perms.RequirePermission(s, permissions.SeasonsAddLeague)(handlers.SeasonAddLeague(s, conn)),
},
{
Path: "/seasons/{season_short_name}/leagues/{league_short_name}",
Method: hws.MethodDELETE,
Handler: perms.RequirePermission(s, permissions.SeasonsRemoveLeague)(handlers.SeasonRemoveLeague(s, conn)),
},
{
Path: "/seasons/{season_short_name}/leagues/{league_short_name}/teams/add",
Method: hws.MethodPOST,
Handler: perms.RequirePermission(s, permissions.TeamsAddToLeague)(handlers.SeasonLeagueAddTeam(s, conn)),
},
}
leagueRoutes := []hws.Route{
{
Path: "/leagues",
Method: hws.MethodGET,
Handler: handlers.LeaguesList(s, conn),
},
{
Path: "/leagues/new",
Method: hws.MethodGET,
Handler: perms.RequirePermission(s, permissions.LeaguesCreate)(handlers.NewLeague(s)),
},
{
Path: "/leagues/new",
Method: hws.MethodPOST,
Handler: perms.RequirePermission(s, permissions.LeaguesCreate)(handlers.NewLeagueSubmit(s, conn)),
},
}
teamRoutes := []hws.Route{
{
Path: "/teams",
Method: hws.MethodGET,
Handler: handlers.TeamsPage(s, conn),
},
{
Path: "/teams",
Method: hws.MethodPOST,
Handler: handlers.TeamsList(s, conn),
},
{
Path: "/teams/new",
Method: hws.MethodGET,
Handler: perms.RequirePermission(s, permissions.TeamsCreate)(handlers.NewTeamPage(s)),
},
{
Path: "/teams/new",
Method: hws.MethodPOST,
Handler: perms.RequirePermission(s, permissions.TeamsCreate)(handlers.NewTeamSubmit(s, conn)),
},
}
htmxRoutes := []hws.Route{
{
Path: "/htmx/isusernameunique",
Method: hws.MethodPOST,
Handler: handlers.IsUnique(s, conn, (*db.User)(nil), "username"),
},
{
Path: "/htmx/isseasonnameunique",
Method: hws.MethodPOST,
Handler: handlers.IsUnique(s, conn, (*db.Season)(nil), "name"),
},
{
Path: "/htmx/isseasonshortnameunique",
Method: hws.MethodPOST,
Handler: handlers.IsUnique(s, conn, (*db.Season)(nil), "short_name"),
},
{
Path: "/htmx/isleaguenameunique",
Method: hws.MethodPOST,
Handler: handlers.IsUnique(s, conn, (*db.League)(nil), "name"),
},
{
Path: "/htmx/isleagueshortnameunique",
Method: hws.MethodPOST,
Handler: handlers.IsUnique(s, conn, (*db.League)(nil), "short_name"),
},
{
Path: "/htmx/isteamnameunique",
Method: hws.MethodPOST,
Handler: handlers.IsUnique(s, conn, (*db.Team)(nil), "name"),
},
{
Path: "/htmx/isteamshortnamesunique",
Method: hws.MethodPOST,
Handler: handlers.IsTeamShortNamesUnique(s, conn),
},
}
wsRoutes := []hws.Route{
{
Path: "/ws/notifications",
Method: hws.MethodGET,
Handler: handlers.NotificationWS(s, cfg),
},
}
// Admin routes
adminRoutes := []hws.Route{
{
Path: "/notification-tester",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: perms.RequireAdmin(s)(handlers.NotifyTester(s)),
},
// Full page routes (for direct navigation and refreshes)
{
Path: "/admin",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminDashboard(s, conn)),
},
{
Path: "/admin/users",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: perms.RequireAdmin(s)(handlers.AdminUsersPage(s, conn)),
},
{
Path: "/admin/roles",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: perms.RequireAdmin(s)(handlers.AdminRoles(s, conn)),
},
{
Path: "/admin/audit",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminAuditLogsPage(s, conn)),
},
{
Path: "/admin/audit",
Method: hws.MethodPOST,
Handler: perms.RequireAdmin(s)(handlers.AdminAuditLogsList(s, conn)),
},
// Role management routes
{
Path: "/admin/roles/create",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminRoleCreateForm(s)),
},
{
Path: "/admin/roles/create",
Method: hws.MethodPOST,
Handler: perms.RequireAdmin(s)(handlers.AdminRoleCreate(s, conn)),
},
{
Path: "/admin/roles/{id}/manage",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminRoleManage(s, conn)),
},
{
Path: "/admin/roles/{id}",
Method: hws.MethodDELETE,
Handler: perms.RequireAdmin(s)(handlers.AdminRoleDelete(s, conn)),
},
{
Path: "/admin/roles/{id}/delete-confirm",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminRoleDeleteConfirm(s, conn)),
},
{
Path: "/admin/roles/{id}/permissions",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminRolePermissionsModal(s, conn)),
},
{
Path: "/admin/roles/{id}/permissions",
Method: hws.MethodPOST,
Handler: perms.RequireAdmin(s)(handlers.AdminRolePermissionsUpdate(s, conn)),
},
{
Path: "/admin/roles/{id}/preview-start",
Method: hws.MethodPOST,
Handler: perms.RequireAdmin(s)(handlers.AdminPreviewRoleStart(s, conn, cfg.HWSAuth.SSL)),
},
{
Path: "/admin/roles/preview-stop",
Method: hws.MethodPOST,
Handler: perms.RequireActualAdmin(s)(handlers.AdminPreviewRoleStop(s)),
},
{
Path: "/admin/audit/filter",
Method: hws.MethodPOST,
Handler: perms.RequireAdmin(s)(handlers.AdminAuditLogsFilter(s, conn)),
},
{
Path: "/admin/audit/{id}",
Method: hws.MethodGET,
Handler: perms.RequireAdmin(s)(handlers.AdminAuditLogDetail(s, conn)),
},
}
routes := append(baseRoutes, htmxRoutes...)
routes = append(routes, wsRoutes...)
routes = append(routes, authRoutes...)
routes = append(routes, adminRoutes...)
routes = append(routes, seasonRoutes...)
routes = append(routes, leagueRoutes...)
routes = append(routes, teamRoutes...)
// Register the routes with the server
err := s.AddRoutes(routes...)
if err != nil {
return errors.Wrap(err, "server.AddRoutes")
}
return nil
}

81
internal/server/setup.go Normal file
View File

@@ -0,0 +1,81 @@
// Package server provides setup utilities for the HTTP server
package server
import (
"io/fs"
"net/http"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
"github.com/pkg/errors"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/discord"
"git.haelnorr.com/h/oslstats/internal/handlers"
"git.haelnorr.com/h/oslstats/internal/rbac"
"git.haelnorr.com/h/oslstats/internal/store"
)
func Setup(
staticFS *fs.FS,
cfg *config.Config,
logger *hlog.Logger,
conn *db.DB,
store *store.Store,
discordAPI *discord.APIClient,
) (server *hws.Server, err error) {
if staticFS == nil {
return nil, errors.New("No filesystem provided")
}
fs := http.FS(*staticFS)
httpServer, err := hws.NewServer(cfg.HWS)
if err != nil {
return nil, errors.Wrap(err, "hws.NewServer")
}
ignoredPaths := []string{
"/static/*",
"/.well-known/*",
"/ws/notifications",
}
auth, err := setupAuth(
cfg.HWSAuth, logger, conn, httpServer, ignoredPaths)
if err != nil {
return nil, errors.Wrap(err, "setupAuth")
}
err = httpServer.AddErrorPage(handlers.ErrorPage)
if err != nil {
return nil, errors.Wrap(err, "httpServer.AddErrorPage")
}
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")
}
// Initialize permissions checker
perms, err := rbac.NewChecker(conn, httpServer)
if err != nil {
return nil, errors.Wrap(err, "rbac.NewChecker")
}
err = addRoutes(httpServer, &fs, cfg, conn, auth, store, discordAPI, perms)
if err != nil {
return nil, errors.Wrap(err, "addRoutes")
}
err = addMiddleware(httpServer, auth, cfg, perms, discordAPI, store, conn)
if err != nil {
return nil, errors.Wrap(err, "addMiddleware")
}
return httpServer, nil
}