added players

This commit is contained in:
2026-02-16 21:31:02 +11:00
parent bb3bed3e89
commit f8090aa0cc
17 changed files with 168 additions and 39 deletions

View File

@@ -7,15 +7,18 @@ import (
) )
type AuditMeta struct { type AuditMeta struct {
r *http.Request ipAddress string
u *User userAgent string
u *User
} }
func NewAudit(r *http.Request, u *User) *AuditMeta { func NewAudit(ipAdd, agent string, user *User) *AuditMeta {
if u == nil { return &AuditMeta{ipAdd, agent, user}
u = CurrentUser(r.Context()) }
}
return &AuditMeta{r, u} func NewAuditFromRequest(r *http.Request) *AuditMeta {
u := CurrentUser(r.Context())
return &AuditMeta{r.RemoteAddr, r.UserAgent(), u}
} }
// AuditInfo contains metadata for audit logging // AuditInfo contains metadata for audit logging
@@ -45,7 +48,10 @@ func extractTableName[T any]() string {
if bunTag != "" { if bunTag != "" {
// Parse tag: "table:seasons,alias:s" -> "seasons" // Parse tag: "table:seasons,alias:s" -> "seasons"
for part := range strings.SplitSeq(bunTag, ",") { for part := range strings.SplitSeq(bunTag, ",") {
part, _ := strings.CutPrefix(part, "table:") part, match := strings.CutPrefix(part, "table:")
if match {
return part
}
return part return part
} }
} }
@@ -56,6 +62,38 @@ func extractTableName[T any]() string {
return strings.ToLower(t.Name()) + "s" return strings.ToLower(t.Name()) + "s"
} }
// extractTableName gets the bun table alias from a model type using reflection
// Example: Season with `bun:"table:seasons,alias:s"` returns "s"
func extractTableAlias[T any]() string {
var model T
t := reflect.TypeOf(model)
// Handle pointer types
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
// Look for bun.BaseModel field with table tag
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Type.Name() == "BaseModel" {
bunTag := field.Tag.Get("bun")
if bunTag != "" {
// Parse tag: "table:seasons,alias:s" -> "seasons"
for part := range strings.SplitSeq(bunTag, ",") {
part, match := strings.CutPrefix(part, "alias:")
if match {
return part
}
}
}
}
}
// Fallback: use struct name in lowercase + "s"
return strings.ToLower(t.Name()) + "s"
}
// extractResourceType converts a table name to singular resource type // extractResourceType converts a table name to singular resource type
// Example: "seasons" -> "season", "users" -> "user" // Example: "seasons" -> "season", "users" -> "user"
func extractResourceType(tableName string) string { func extractResourceType(tableName string) string {

View File

@@ -49,9 +49,6 @@ func log(
if meta.u == nil { if meta.u == nil {
return errors.New("user cannot be nil for audit logging") return errors.New("user cannot be nil for audit logging")
} }
if meta.r == nil {
return errors.New("request cannot be nil for audit logging")
}
// Convert resourceID to string // Convert resourceID to string
var resourceIDStr *string var resourceIDStr *string
@@ -70,18 +67,14 @@ func log(
detailsJSON = jsonBytes detailsJSON = jsonBytes
} }
// Extract IP and User-Agent from request
ipAddress := meta.r.RemoteAddr
userAgent := meta.r.UserAgent()
log := &AuditLog{ log := &AuditLog{
UserID: meta.u.ID, UserID: meta.u.ID,
Action: info.Action, Action: info.Action,
ResourceType: info.ResourceType, ResourceType: info.ResourceType,
ResourceID: resourceIDStr, ResourceID: resourceIDStr,
Details: detailsJSON, Details: detailsJSON,
IPAddress: ipAddress, IPAddress: meta.ipAddress,
UserAgent: userAgent, UserAgent: meta.userAgent,
Result: result, Result: result,
ErrorMessage: errorMessage, ErrorMessage: errorMessage,
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),

View File

@@ -37,6 +37,10 @@ func (g *fieldgetter[T]) Get(ctx context.Context) (*T, error) {
return g.get(ctx) return g.get(ctx)
} }
func (g *fieldgetter[T]) String() string {
return g.q.String()
}
func (g *fieldgetter[T]) Relation(name string, apply ...func(*bun.SelectQuery) *bun.SelectQuery) *fieldgetter[T] { func (g *fieldgetter[T]) Relation(name string, apply ...func(*bun.SelectQuery) *bun.SelectQuery) *fieldgetter[T] {
g.q = g.q.Relation(name, apply...) g.q = g.q.Relation(name, apply...)
return g return g
@@ -66,5 +70,6 @@ func GetByID[T any](
tx bun.Tx, tx bun.Tx,
id int, id int,
) *fieldgetter[T] { ) *fieldgetter[T] {
return GetByField[T](tx, "id", id) prefix := extractTableAlias[T]()
return GetByField[T](tx, prefix+".id", id)
} }

View File

@@ -0,0 +1,37 @@
package migrations
import (
"context"
"git.haelnorr.com/h/oslstats/internal/db"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(
// UP migration
func(ctx context.Context, conn *bun.DB) error {
// Add your migration code here
_, err := conn.NewCreateTable().
Model((*db.Player)(nil)).
IfNotExists().
Exec(ctx)
if err != nil {
return err
}
return nil
},
// DOWN migration
func(ctx context.Context, conn *bun.DB) error {
// Add your rollback code here
_, err := conn.NewDropTable().
Model((*db.Player)(nil)).
IfExists().
Exec(ctx)
if err != nil {
return err
}
return nil
},
)
}

54
internal/db/player.go Normal file
View File

@@ -0,0 +1,54 @@
package db
import (
"context"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
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"`
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:"-"`
}
func NewPlayer(ctx context.Context, tx bun.Tx, discordID string, audit *AuditMeta) (*Player, error) {
player := &Player{DiscordID: discordID}
user, err := GetUserByDiscordID(ctx, tx, discordID)
if err != nil && !IsBadRequest(err) {
return nil, errors.Wrap(err, "GetUserByDiscordID")
}
if user != nil {
player.UserID = &user.ID
}
err = Insert(tx, player).
WithAudit(audit, nil).Exec(ctx)
if err != nil {
return nil, errors.Wrap(err, "Insert")
}
return player, 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 {
player, err := GetPlayer(ctx, tx, playerID)
if err != nil {
return errors.Wrap(err, "GetPlayer")
}
player.SlapID = &slapID
err = UpdateByID(tx, player.ID, player).Column("slap_id").
WithAudit(audit, nil).Exec(ctx)
if err != nil {
return errors.Wrap(err, "UpdateByID")
}
return nil
}

View File

@@ -33,6 +33,7 @@ func (db *DB) RegisterModels() []any {
(*Permission)(nil), (*Permission)(nil),
(*AuditLog)(nil), (*AuditLog)(nil),
(*Fixture)(nil), (*Fixture)(nil),
(*Player)(nil),
} }
db.RegisterModel(models...) db.RegisterModel(models...)
return models return models

View File

@@ -20,7 +20,8 @@ type User struct {
CreatedAt int64 `bun:"created_at" json:"created_at"` CreatedAt int64 `bun:"created_at" json:"created_at"`
DiscordID string `bun:"discord_id,unique" json:"discord_id"` DiscordID string `bun:"discord_id,unique" json:"discord_id"`
Roles []*Role `bun:"m2m:user_roles,join:User=Role" json:"-"` Roles []*Role `bun:"m2m:user_roles,join:User=Role" json:"-"`
Player *Player `bun:"rel:has-one,join:id=user_id"`
} }
func (u *User) GetID() int { func (u *User) GetID() int {

View File

@@ -84,7 +84,7 @@ func AdminRoleCreate(s *hws.Server, conn *db.DB) http.Handler {
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),
} }
err := db.CreateRole(ctx, tx, newRole, db.NewAudit(r, nil)) err := db.CreateRole(ctx, tx, newRole, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "db.CreateRole") return false, errors.Wrap(err, "db.CreateRole")
} }
@@ -196,7 +196,7 @@ func AdminRoleDelete(s *hws.Server, conn *db.DB) http.Handler {
} }
// Delete the role with audit logging // Delete the role with audit logging
err = db.DeleteRole(ctx, tx, roleID, db.NewAudit(r, nil)) err = db.DeleteRole(ctx, tx, roleID, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.NotFound(w, err) respond.NotFound(w, err)
@@ -320,7 +320,7 @@ func AdminRolePermissionsUpdate(s *hws.Server, conn *db.DB) http.Handler {
} }
return false, errors.Wrap(err, "db.GetRoleByID") return false, errors.Wrap(err, "db.GetRoleByID")
} }
err = role.UpdatePermissions(ctx, tx, permissionIDs, db.NewAudit(r, nil)) err = role.UpdatePermissions(ctx, tx, permissionIDs, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "role.UpdatePermissions") return false, errors.Wrap(err, "role.UpdatePermissions")
} }

View File

@@ -36,7 +36,7 @@ func GenerateFixtures(
var league *db.League var league *db.League
var fixtures []*db.Fixture var fixtures []*db.Fixture
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
_, err := db.NewRound(ctx, tx, seasonShortName, leagueShortName, round, db.NewAudit(r, nil)) _, err := db.NewRound(ctx, tx, seasonShortName, leagueShortName, round, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.BadRequest(w, errors.Wrap(err, "db.NewRound")) respond.BadRequest(w, errors.Wrap(err, "db.NewRound"))
@@ -98,7 +98,7 @@ func UpdateFixtures(
notify.Warn(s, w, r, "Invalid game weeks", "A game week is missing or has no games", nil) notify.Warn(s, w, r, "Invalid game weeks", "A game week is missing or has no games", nil)
return false, nil return false, nil
} }
err = db.UpdateFixtureGameWeeks(ctx, tx, fixtures, db.NewAudit(r, nil)) err = db.UpdateFixtureGameWeeks(ctx, tx, fixtures, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.BadRequest(w, errors.Wrap(err, "db.UpdateFixtureGameWeeks")) respond.BadRequest(w, errors.Wrap(err, "db.UpdateFixtureGameWeeks"))
@@ -125,7 +125,7 @@ func DeleteFixture(
return return
} }
if !conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { if !conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
err := db.DeleteFixture(ctx, tx, fixtureID, db.NewAudit(r, nil)) err := db.DeleteFixture(ctx, tx, fixtureID, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.NotFound(w, errors.Wrap(err, "db.DeleteFixture")) respond.NotFound(w, errors.Wrap(err, "db.DeleteFixture"))

View File

@@ -61,7 +61,7 @@ func NewLeagueSubmit(
if !nameUnique || !shortNameUnique { if !nameUnique || !shortNameUnique {
return true, nil return true, nil
} }
league, err = db.NewLeague(ctx, tx, name, shortname, description, db.NewAudit(r, nil)) league, err = db.NewLeague(ctx, tx, name, shortname, description, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "db.NewLeague") return false, errors.Wrap(err, "db.NewLeague")
} }

View File

@@ -64,7 +64,7 @@ func Register(
if !unique { if !unique {
return true, nil return true, nil
} }
user, err = db.CreateUser(ctx, tx, username, details.DiscordUser, db.NewAudit(r, nil)) user, err = db.CreateUser(ctx, tx, username, details.DiscordUser, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "db.CreateUser") return false, errors.Wrap(err, "db.CreateUser")
} }

View File

@@ -86,7 +86,7 @@ func SeasonEditSubmit(
} }
return false, errors.Wrap(err, "db.GetSeason") return false, errors.Wrap(err, "db.GetSeason")
} }
err = season.Update(ctx, tx, version, start, end, finalsStart, finalsEnd, db.NewAudit(r, nil)) err = season.Update(ctx, tx, version, start, end, finalsStart, finalsEnd, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "season.Update") return false, errors.Wrap(err, "season.Update")
} }

View File

@@ -18,8 +18,8 @@ func SeasonLeagueAddTeam(
conn *db.DB, conn *db.DB,
) http.Handler { ) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seasonStr := r.PathValue("season_short_name") seasonShortName := r.PathValue("season_short_name")
leagueStr := r.PathValue("league_short_name") leagueShortName := r.PathValue("league_short_name")
getter, ok := validation.ParseFormOrNotify(s, w, r) getter, ok := validation.ParseFormOrNotify(s, w, r)
if !ok { if !ok {
@@ -36,7 +36,7 @@ func SeasonLeagueAddTeam(
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error var err error
team, season, league, err = db.NewTeamParticipation(ctx, tx, seasonStr, leagueStr, teamID, db.NewAudit(r, nil)) team, season, league, err = db.NewTeamParticipation(ctx, tx, seasonShortName, leagueShortName, teamID, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)

View File

@@ -92,7 +92,7 @@ func SeasonLeagueDeleteFixtures(
var league *db.League var league *db.League
var fixtures []*db.Fixture var fixtures []*db.Fixture
if !conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { if !conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
err := db.DeleteAllFixtures(ctx, tx, seasonShortName, leagueShortName, db.NewAudit(r, nil)) err := db.DeleteAllFixtures(ctx, tx, seasonShortName, leagueShortName, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.BadRequest(w, errors.Wrap(err, "db.DeleteAllFixtures")) respond.BadRequest(w, errors.Wrap(err, "db.DeleteAllFixtures"))

View File

@@ -19,13 +19,13 @@ func SeasonAddLeague(
conn *db.DB, conn *db.DB,
) http.Handler { ) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seasonStr := r.PathValue("season_short_name") seasonShortName := r.PathValue("season_short_name")
leagueStr := r.PathValue("league_short_name") leagueShortName := r.PathValue("league_short_name")
var season *db.Season var season *db.Season
var allLeagues []*db.League var allLeagues []*db.League
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
err := db.NewSeasonLeague(ctx, tx, seasonStr, leagueStr, db.NewAudit(r, nil)) err := db.NewSeasonLeague(ctx, tx, seasonShortName, leagueShortName, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.BadRequest(w, err) respond.BadRequest(w, err)
@@ -35,7 +35,7 @@ func SeasonAddLeague(
} }
// Reload season with updated leagues // Reload season with updated leagues
season, err = db.GetSeason(ctx, tx, seasonStr) season, err = db.GetSeason(ctx, tx, seasonShortName)
if err != nil { if err != nil {
return false, errors.Wrap(err, "db.GetSeason") return false, errors.Wrap(err, "db.GetSeason")
} }
@@ -75,7 +75,7 @@ func SeasonRemoveLeague(
} }
return false, errors.Wrap(err, "db.GetSeason") return false, errors.Wrap(err, "db.GetSeason")
} }
err = season.RemoveLeague(ctx, tx, leagueStr, db.NewAudit(r, nil)) err = season.RemoveLeague(ctx, tx, leagueStr, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
if db.IsBadRequest(err) { if db.IsBadRequest(err) {
respond.BadRequest(w, err) respond.BadRequest(w, err)

View File

@@ -66,7 +66,7 @@ func NewSeasonSubmit(
if !nameUnique || !shortNameUnique { if !nameUnique || !shortNameUnique {
return true, nil return true, nil
} }
season, err = db.NewSeason(ctx, tx, name, version, shortname, start, db.NewAudit(r, nil)) season, err = db.NewSeason(ctx, tx, name, version, shortname, start, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "db.NewSeason") return false, errors.Wrap(err, "db.NewSeason")
} }

View File

@@ -71,7 +71,7 @@ func NewTeamSubmit(
if !nameUnique || !shortNameComboUnique { if !nameUnique || !shortNameComboUnique {
return true, nil return true, nil
} }
_, err = db.NewTeam(ctx, tx, name, shortName, altShortName, color, db.NewAudit(r, nil)) _, err = db.NewTeam(ctx, tx, name, shortName, altShortName, color, db.NewAuditFromRequest(r))
if err != nil { if err != nil {
return false, errors.Wrap(err, "db.NewTeam") return false, errors.Wrap(err, "db.NewTeam")
} }