From 103da78f0b4225d09d6f9173b784f9893b98cd76 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Tue, 17 Feb 2026 18:33:22 +1100 Subject: [PATCH] slapid and player now links when registering --- cmd/oslstats/run.go | 10 ++++- internal/db/auditlog.go | 2 - internal/db/getlist.go | 2 - internal/db/player.go | 41 +++++++++++++++++- internal/db/user.go | 8 ++-- internal/discord/steamid.go | 4 +- internal/handlers/fixtures.go | 2 - internal/handlers/register.go | 78 ++++++++++++++++++++++++++++++----- internal/server/routes.go | 4 +- internal/server/setup.go | 4 +- pkg/slapshotapi/slapid.go | 10 ++++- 11 files changed, 137 insertions(+), 28 deletions(-) diff --git a/cmd/oslstats/run.go b/cmd/oslstats/run.go index a400a80..f98ca88 100644 --- a/cmd/oslstats/run.go +++ b/cmd/oslstats/run.go @@ -17,6 +17,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/embedfs" "git.haelnorr.com/h/oslstats/internal/server" "git.haelnorr.com/h/oslstats/internal/store" + "git.haelnorr.com/h/oslstats/pkg/slapshotapi" ) // Initializes and runs the server @@ -47,8 +48,15 @@ func run(ctx context.Context, logger *hlog.Logger, cfg *config.Config) error { return errors.Wrap(err, "discord.NewAPIClient") } + // Setup Slapshot API + logger.Debug().Msg("Setting up Slapshot API client") + slapAPI, err := slapshotapi.NewSlapAPIClient(cfg.Slapshot) + if err != nil { + return errors.Wrap(err, "slapshotapi.NewSlapAPIClient") + } + logger.Debug().Msg("Setting up HTTP server") - httpServer, err := server.Setup(staticFS, cfg, logger, conn, store, discordAPI) + httpServer, err := server.Setup(staticFS, cfg, logger, conn, store, discordAPI, slapAPI) if err != nil { return errors.Wrap(err, "setupHttpServer") } diff --git a/internal/db/auditlog.go b/internal/db/auditlog.go index 8727c1c..f615e93 100644 --- a/internal/db/auditlog.go +++ b/internal/db/auditlog.go @@ -3,7 +3,6 @@ package db import ( "context" "encoding/json" - "fmt" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -78,7 +77,6 @@ func (a *AuditLogFilter) UserIDs(ids []int) *AuditLogFilter { } func (a *AuditLogFilter) Actions(actions []string) *AuditLogFilter { - fmt.Println(actions) if len(actions) > 0 { a.In("al.action", actions) } diff --git a/internal/db/getlist.go b/internal/db/getlist.go index 52033ae..f999b82 100644 --- a/internal/db/getlist.go +++ b/internal/db/getlist.go @@ -3,7 +3,6 @@ package db import ( "context" "database/sql" - "fmt" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -110,7 +109,6 @@ func (l *listgetter[T]) Filter(filters ...Filter) *listgetter[T] { l.q = l.q.Where("? ? ?", bun.Ident(filter.Field), bun.Safe(filter.Comparator), filter.Value) } } - fmt.Println(l.q.String()) return l } diff --git a/internal/db/player.go b/internal/db/player.go index 16ed46c..690cd37 100644 --- a/internal/db/player.go +++ b/internal/db/player.go @@ -11,13 +11,15 @@ type Player struct { bun.BaseModel `bun:"table:players,alias:p"` ID int `bun:"id,pk,autoincrement" json:"id"` - SlapID *string `bun:"slap_id,unique" json:"slap_id"` + SlapID *uint32 `bun:"slap_id,unique" json:"slap_id"` DiscordID string `bun:"discord_id,unique,notnull" json:"discord_id"` UserID *int `bun:"user_id,unique" json:"user_id"` User *User `bun:"rel:belongs-to,join:user_id=id" json:"-"` } +// NewPlayer creates a new player in the database. If there is an existing user with the same +// discordID, it will automatically link that user to the player func NewPlayer(ctx context.Context, tx bun.Tx, discordID string, audit *AuditMeta) (*Player, error) { player := &Player{DiscordID: discordID} user, err := GetUserByDiscordID(ctx, tx, discordID) @@ -35,11 +37,46 @@ func NewPlayer(ctx context.Context, tx bun.Tx, discordID string, audit *AuditMet return player, nil } +// ConnectPlayer links the user to an existing player, or creates a new player to link if not found +// Populates User.Player on success +func (u *User) ConnectPlayer(ctx context.Context, tx bun.Tx, audit *AuditMeta) error { + player, err := GetByField[Player](tx, "p.discord_id", u.DiscordID). + Relation("User").Get(ctx) + if err != nil { + if !IsBadRequest(err) { + // Unexpected error occured + return errors.Wrap(err, "GetByField") + } + // Player doesn't exist, create a new one + player, err = NewPlayer(ctx, tx, u.DiscordID, audit) + if err != nil { + return errors.Wrap(err, "NewPlayer") + } + // New player should automatically get linked to the user + u.Player = player + return nil + } + // Player was found + if player.UserID != nil { + if player.UserID == &u.ID { + return nil + } + return errors.New("player with that discord_id already linked to a user") + } + player.UserID = &u.ID + err = UpdateByID(tx, player.ID, player).Column("user_id").Exec(ctx) + if err != nil { + return errors.Wrap(err, "UpdateByID") + } + u.Player = player + return nil +} + func GetPlayer(ctx context.Context, tx bun.Tx, playerID int) (*Player, error) { return GetByID[Player](tx, playerID).Relation("User").Get(ctx) } -func UpdatePlayerSlapID(ctx context.Context, tx bun.Tx, playerID int, slapID string, audit *AuditMeta) error { +func UpdatePlayerSlapID(ctx context.Context, tx bun.Tx, playerID int, slapID uint32, audit *AuditMeta) error { player, err := GetPlayer(ctx, tx, playerID) if err != nil { return errors.Wrap(err, "GetPlayer") diff --git a/internal/db/user.go b/internal/db/user.go index 0ab5240..9b9b3d8 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -56,7 +56,7 @@ func CreateUser(ctx context.Context, tx bun.Tx, username string, discorduser *di // GetUserByID queries the database for a user matching the given ID // Returns a BadRequestNotFound error if no user is found func GetUserByID(ctx context.Context, tx bun.Tx, id int) (*User, error) { - return GetByID[User](tx, id).Get(ctx) + return GetByID[User](tx, id).Relation("Player").Get(ctx) } // GetUserByUsername queries the database for a user matching the given username @@ -65,7 +65,7 @@ func GetUserByUsername(ctx context.Context, tx bun.Tx, username string) (*User, if username == "" { return nil, errors.New("username not provided") } - return GetByField[User](tx, "username", username).Get(ctx) + return GetByField[User](tx, "username", username).Relation("Player").Get(ctx) } // GetUserByDiscordID queries the database for a user matching the given discord id @@ -74,7 +74,7 @@ func GetUserByDiscordID(ctx context.Context, tx bun.Tx, discordID string) (*User if discordID == "" { return nil, errors.New("discord_id not provided") } - return GetByField[User](tx, "discord_id", discordID).Get(ctx) + return GetByField[User](tx, "u.discord_id", discordID).Relation("Player").Get(ctx) } // GetRoles loads all the roles for this user @@ -142,7 +142,7 @@ func (u *User) IsAdmin(ctx context.Context, tx bun.Tx) (bool, error) { func GetUsers(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*List[User], error) { defaults := &PageOpts{1, 50, bun.OrderAsc, "id"} - return GetList[User](tx).GetPaged(ctx, pageOpts, defaults) + return GetList[User](tx).Relation("Player").GetPaged(ctx, pageOpts, defaults) } // GetUsersWithRoles queries the database for users with their roles preloaded diff --git a/internal/discord/steamid.go b/internal/discord/steamid.go index 76f491c..329f0b3 100644 --- a/internal/discord/steamid.go +++ b/internal/discord/steamid.go @@ -4,6 +4,8 @@ import ( "github.com/pkg/errors" ) +var ErrNoSteam error = errors.New("steam connection not found") + func (s *OAuthSession) GetSteamID() (string, error) { connections, err := s.UserConnections() if err != nil { @@ -14,5 +16,5 @@ func (s *OAuthSession) GetSteamID() (string, error) { return conn.ID, nil } } - return "", errors.New("steam connection not found") + return "", ErrNoSteam } diff --git a/internal/handlers/fixtures.go b/internal/handlers/fixtures.go index f65c4cb..aae2bda 100644 --- a/internal/handlers/fixtures.go +++ b/internal/handlers/fixtures.go @@ -2,7 +2,6 @@ package handlers import ( "context" - "fmt" "net/http" "strconv" @@ -93,7 +92,6 @@ func UpdateFixtures( } var valid bool fixtures, valid = updateFixtures(fixtures, updates) - fmt.Println(len(fixtures)) if !valid { notify.Warn(s, w, r, "Invalid game weeks", "A game week is missing or has no games", nil) return false, nil diff --git a/internal/handlers/register.go b/internal/handlers/register.go index f2b834b..f787f05 100644 --- a/internal/handlers/register.go +++ b/internal/handlers/register.go @@ -12,16 +12,20 @@ import ( "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/rbac" "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/store" "git.haelnorr.com/h/oslstats/internal/throw" authview "git.haelnorr.com/h/oslstats/internal/view/authview" + "git.haelnorr.com/h/oslstats/pkg/slapshotapi" ) func Register( s *hws.Server, auth *hwsauth.Authenticator[*db.User, bun.Tx], conn *db.DB, + slapAPI *slapshotapi.SlapAPI, cfg *config.Config, store *store.Store, ) http.Handler { @@ -56,6 +60,7 @@ func Register( username := r.FormValue("username") unique := false var user *db.User + audit := db.NewAudit(r.RemoteAddr, r.UserAgent(), user) if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { unique, err = db.IsUnique(ctx, tx, (*db.User)(nil), "username", username) if err != nil { @@ -64,19 +69,13 @@ func Register( if !unique { return true, nil } - user, err = db.CreateUser(ctx, tx, username, details.DiscordUser, db.NewAuditFromRequest(r)) + user, err = registerUser(ctx, tx, username, details, cfg.RBAC, audit) if err != nil { - return false, errors.Wrap(err, "db.CreateUser") + return false, errors.Wrap(err, "registerUser") } - err = user.UpdateDiscordToken(ctx, tx, details.Token) + err = connectSlapID(ctx, tx, user, details.Token, slapAPI, audit) if err != nil { - return false, errors.Wrap(err, "db.UpdateDiscordToken") - } - if shouldGrantAdmin(user, cfg.RBAC) { - err := ensureUserHasAdminRole(ctx, tx, user) - if err != nil { - return false, errors.Wrap(err, "ensureUserHasAdminRole") - } + return false, errors.Wrap(err, "connectSlapID") } return true, nil }); !ok { @@ -96,3 +95,62 @@ func Register( }, ) } + +func registerUser(ctx context.Context, tx bun.Tx, + username string, details *store.RegistrationSession, + rbac *rbac.Config, audit *db.AuditMeta, +) (*db.User, error) { + // Register the user + user, err := db.CreateUser(ctx, tx, username, details.DiscordUser, audit) + if err != nil { + return nil, errors.Wrap(err, "db.CreateUser") + } + err = user.UpdateDiscordToken(ctx, tx, details.Token) + if err != nil { + return nil, errors.Wrap(err, "db.UpdateDiscordToken") + } + err = user.ConnectPlayer(ctx, tx, audit) + if err != nil { + return nil, errors.Wrap(err, "db.ConnectPlayer") + } + // Check if they should be an admin + if shouldGrantAdmin(user, rbac) { + err := ensureUserHasAdminRole(ctx, tx, user) + if err != nil { + return nil, errors.Wrap(err, "ensureUserHasAdminRole") + } + } + return user, nil +} + +func connectSlapID(ctx context.Context, tx bun.Tx, user *db.User, + token *discord.Token, slapAPI *slapshotapi.SlapAPI, audit *db.AuditMeta, +) error { + // Attempt to setup their player/slapID from steam connection + // If fails due to no steam connection or no slapID, fail silently and proceed with registration + session, err := discord.NewOAuthSession(token) + if err != nil { + return errors.Wrap(err, "discord.NewOAuthSession") + } + steamID, err := session.GetSteamID() + if err != nil { + if err == discord.ErrNoSteam { + return nil + } + return errors.Wrap(err, "session.GetSteamID") + } + slapID, err := slapAPI.GetSlapID(ctx, steamID) + if err != nil { + if err == slapshotapi.ErrNoSlapID { + return nil + } + return errors.Wrap(err, "slapAPI.GetSlapID") + } + // slapID exists, we can update their player connection + err = db.UpdatePlayerSlapID(ctx, tx, user.Player.ID, slapID, audit) + if err != nil { + return errors.Wrap(err, "db.UpdatePlayerSlapID") + } + + return nil +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 4ad52c4..6ad3750 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -15,6 +15,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/permissions" "git.haelnorr.com/h/oslstats/internal/rbac" "git.haelnorr.com/h/oslstats/internal/store" + "git.haelnorr.com/h/oslstats/pkg/slapshotapi" ) func addRoutes( @@ -25,6 +26,7 @@ func addRoutes( auth *hwsauth.Authenticator[*db.User, bun.Tx], store *store.Store, discordAPI *discord.APIClient, + slapAPI *slapshotapi.SlapAPI, perms *rbac.Checker, ) error { // Create the routes @@ -55,7 +57,7 @@ func addRoutes( { Path: "/register", Methods: []hws.Method{hws.MethodGET, hws.MethodPOST}, - Handler: auth.LogoutReq(handlers.Register(s, auth, conn, cfg, store)), + Handler: auth.LogoutReq(handlers.Register(s, auth, conn, slapAPI, cfg, store)), }, { Path: "/logout", diff --git a/internal/server/setup.go b/internal/server/setup.go index 1fe44dc..19887af 100644 --- a/internal/server/setup.go +++ b/internal/server/setup.go @@ -15,6 +15,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/handlers" "git.haelnorr.com/h/oslstats/internal/rbac" "git.haelnorr.com/h/oslstats/internal/store" + "git.haelnorr.com/h/oslstats/pkg/slapshotapi" ) func Setup( @@ -24,6 +25,7 @@ func Setup( conn *db.DB, store *store.Store, discordAPI *discord.APIClient, + slapAPI *slapshotapi.SlapAPI, ) (server *hws.Server, err error) { if staticFS == nil { return nil, errors.New("No filesystem provided") @@ -67,7 +69,7 @@ func Setup( return nil, errors.Wrap(err, "rbac.NewChecker") } - err = addRoutes(httpServer, &fs, cfg, conn, auth, store, discordAPI, perms) + err = addRoutes(httpServer, &fs, cfg, conn, auth, store, discordAPI, slapAPI, perms) if err != nil { return nil, errors.Wrap(err, "addRoutes") } diff --git a/pkg/slapshotapi/slapid.go b/pkg/slapshotapi/slapid.go index 1a45d2b..1b16dad 100644 --- a/pkg/slapshotapi/slapid.go +++ b/pkg/slapshotapi/slapid.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/pkg/errors" ) @@ -30,7 +31,9 @@ type idresp struct { ID uint32 `json:"id"` } -// GetSlapID returns the slapshot ID of the steam user +var ErrNoSlapID error = errors.New("slapID not found") + +// GetSlapID returns the slapshot ID of the steam user. func (c *SlapAPI) GetSlapID( ctx context.Context, steamid string, @@ -38,7 +41,10 @@ func (c *SlapAPI) GetSlapID( endpoint := getEndpointSteamID(steamid) data, err := c.request(ctx, endpoint) if err != nil { - return 0, errors.Wrap(err, "slapapiReq") + if strings.Contains(err.Error(), "404") { + return 0, ErrNoSlapID + } + return 0, errors.Wrap(err, "c.request") } resp := idresp{} err = json.Unmarshal(data, &resp)