From ef8c022e60d0029be0a82a5e8ebea6720a04e978 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Sun, 15 Feb 2026 12:27:36 +1100 Subject: [PATCH] everybody loves a refactor --- internal/db/auditlog.go | 2 + internal/db/delete.go | 17 +++---- internal/db/discordtokens.go | 6 +-- internal/db/errors.go | 31 +++++++++++++ internal/db/getbyfield.go | 3 +- internal/db/getlist.go | 2 + internal/db/permission.go | 4 +- internal/db/role.go | 7 +-- internal/db/seasonleague.go | 15 ++----- internal/db/setup.go | 1 + internal/db/teamparticipation.go | 13 +----- internal/db/update.go | 10 ++++- internal/db/user.go | 6 +-- internal/db/userrole.go | 3 -- internal/handlers/admin_audit.go | 8 ++-- internal/handlers/admin_preview_role.go | 8 ++-- internal/handlers/admin_roles.go | 49 ++++++++++++--------- internal/handlers/auth_helpers.go | 3 -- internal/handlers/callback.go | 2 +- internal/handlers/isunique.go | 11 +++-- internal/handlers/leagues_new.go | 4 +- internal/handlers/login.go | 5 ++- internal/handlers/logout.go | 8 +--- internal/handlers/register.go | 5 ++- internal/handlers/season_detail.go | 11 ++--- internal/handlers/season_edit.go | 24 +++++----- internal/handlers/season_league_add_team.go | 4 ++ internal/handlers/season_league_detail.go | 12 +++-- internal/handlers/season_league_finals.go | 11 +++-- internal/handlers/season_league_fixtures.go | 9 ++-- internal/handlers/season_league_stats.go | 9 ++-- internal/handlers/season_league_table.go | 10 ++--- internal/handlers/season_league_teams.go | 9 ++-- internal/handlers/season_leagues.go | 15 +++++-- internal/handlers/seasons_new.go | 4 +- internal/handlers/static.go | 38 +--------------- internal/handlers/team_shortnames_unique.go | 11 ++--- internal/handlers/teams_list.go | 31 +++---------- internal/handlers/teams_new.go | 4 +- internal/respond/error.go | 36 +++++++++++++++ internal/respond/headers.go | 39 ++++++++++++++++ internal/server/routes.go | 7 +-- internal/validation/forms.go | 3 +- justfile | 2 +- 44 files changed, 278 insertions(+), 234 deletions(-) create mode 100644 internal/db/errors.go create mode 100644 internal/respond/error.go create mode 100644 internal/respond/headers.go diff --git a/internal/db/auditlog.go b/internal/db/auditlog.go index f615e93..8727c1c 100644 --- a/internal/db/auditlog.go +++ b/internal/db/auditlog.go @@ -3,6 +3,7 @@ package db import ( "context" "encoding/json" + "fmt" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -77,6 +78,7 @@ 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/delete.go b/internal/db/delete.go index 07753b5..501285c 100644 --- a/internal/db/delete.go +++ b/internal/db/delete.go @@ -2,7 +2,6 @@ package db import ( "context" - "database/sql" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -46,13 +45,18 @@ func (d *deleter[T]) WithAudit(meta *AuditMeta, info *AuditInfo) *deleter[T] { } func (d *deleter[T]) Delete(ctx context.Context) error { - _, err := d.q.Exec(ctx) + result, err := d.q.Exec(ctx) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil - } return errors.Wrap(err, "bun.DeleteQuery.Exec") } + rows, err := result.RowsAffected() + if err != nil { + return errors.Wrap(err, "result.RowsAffected") + } + if rows == 0 { + resource := extractResourceType(extractTableName[T]()) + return BadRequestNotFound(resource, "id", d.resourceID) + } // Handle audit logging if enabled if d.audit != nil { @@ -88,9 +92,6 @@ func DeleteWithProtection[T systemType](ctx context.Context, tx bun.Tx, id int, if err != nil { return errors.Wrap(err, "GetByID") } - if item == nil { - return errors.New("record not found") - } if (*item).isSystem() { return errors.New("record is system protected") } diff --git a/internal/db/discordtokens.go b/internal/db/discordtokens.go index 568fdd8..229075c 100644 --- a/internal/db/discordtokens.go +++ b/internal/db/discordtokens.go @@ -51,11 +51,11 @@ func (u *User) UpdateDiscordToken(ctx context.Context, tx bun.Tx, token *discord func (u *User) DeleteDiscordTokens(ctx context.Context, tx bun.Tx) (*DiscordToken, error) { token, err := u.GetDiscordToken(ctx, tx) if err != nil { + if IsBadRequest(err) { + return nil, nil // Token doesn't exist - not an error + } return nil, errors.Wrap(err, "user.GetDiscordToken") } - if token == nil { - return nil, nil - } _, err = tx.NewDelete(). Model((*DiscordToken)(nil)). Where("discord_id = ?", u.DiscordID). diff --git a/internal/db/errors.go b/internal/db/errors.go new file mode 100644 index 0000000..d0b7db5 --- /dev/null +++ b/internal/db/errors.go @@ -0,0 +1,31 @@ +package db + +import ( + "fmt" + "strings" +) + +func IsBadRequest(err error) bool { + return strings.HasPrefix(err.Error(), "bad request:") +} + +func BadRequest(err string) error { + return fmt.Errorf("bad request: %s", err) +} + +func BadRequestNotFound(resource, field string, value any) error { + errStr := fmt.Sprintf("%s with %s=%v not found", resource, field, value) + return BadRequest(errStr) +} + +func BadRequestNotAssociated(parent, child string, parentID, childID any) error { + errStr := fmt.Sprintf("%s (ID: %v) not associated with %s (ID: %v)", + child, childID, parent, parentID) + return BadRequest(errStr) +} + +func BadRequestAssociated(parent, child string, parentID, childID any) error { + errStr := fmt.Sprintf("%s (ID: %v) already associated with %s (ID: %v)", + child, childID, parent, parentID) + return BadRequest(errStr) +} diff --git a/internal/db/getbyfield.go b/internal/db/getbyfield.go index 4cb8f48..7a86944 100644 --- a/internal/db/getbyfield.go +++ b/internal/db/getbyfield.go @@ -24,7 +24,8 @@ func (g *fieldgetter[T]) get(ctx context.Context) (*T, error) { Scan(ctx) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, nil + resource := extractResourceType(extractTableName[T]()) + return nil, BadRequestNotFound(resource, g.field, g.value) } return nil, errors.Wrap(err, "bun.SelectQuery.Scan") } diff --git a/internal/db/getlist.go b/internal/db/getlist.go index d94007d..d1f887c 100644 --- a/internal/db/getlist.go +++ b/internal/db/getlist.go @@ -3,6 +3,7 @@ package db import ( "context" "database/sql" + "fmt" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -104,6 +105,7 @@ 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/permission.go b/internal/db/permission.go index 6321e0a..ec80cf3 100644 --- a/internal/db/permission.go +++ b/internal/db/permission.go @@ -28,7 +28,7 @@ func (p Permission) isSystem() bool { } // GetPermissionByName queries the database for a permission matching the given name -// Returns nil, nil if no permission is found +// Returns a BadRequestNotFound error if no permission is found func GetPermissionByName(ctx context.Context, tx bun.Tx, name permissions.Permission) (*Permission, error) { if name == "" { return nil, errors.New("name cannot be empty") @@ -37,7 +37,7 @@ func GetPermissionByName(ctx context.Context, tx bun.Tx, name permissions.Permis } // GetPermissionByID queries the database for a permission matching the given ID -// Returns nil, nil if no permission is found +// Returns a BadRequestNotFound error if no permission is found func GetPermissionByID(ctx context.Context, tx bun.Tx, id int) (*Permission, error) { if id <= 0 { return nil, errors.New("id must be positive") diff --git a/internal/db/role.go b/internal/db/role.go index 0962c8d..b73a8fb 100644 --- a/internal/db/role.go +++ b/internal/db/role.go @@ -30,7 +30,7 @@ func (r Role) isSystem() bool { } // GetRoleByName queries the database for a role matching the given name -// Returns nil, nil if no role is found +// Returns a BadRequestNotFound error if no role is found func GetRoleByName(ctx context.Context, tx bun.Tx, name roles.Role) (*Role, error) { if name == "" { return nil, errors.New("name cannot be empty") @@ -39,7 +39,7 @@ func GetRoleByName(ctx context.Context, tx bun.Tx, name roles.Role) (*Role, erro } // GetRoleByID queries the database for a role matching the given ID -// Returns nil, nil if no role is found +// Returns a BadRequestNotFound error if no role is found func GetRoleByID(ctx context.Context, tx bun.Tx, id int) (*Role, error) { return GetByID[Role](tx, id).Relation("Permissions").Get(ctx) } @@ -110,9 +110,6 @@ func DeleteRole(ctx context.Context, tx bun.Tx, id int, audit *AuditMeta) error if err != nil { return errors.Wrap(err, "GetRoleByID") } - if role == nil { - return errors.New("role not found") - } if role.IsSystem { return errors.New("cannot delete system roles") } diff --git a/internal/db/seasonleague.go b/internal/db/seasonleague.go index df9cdea..d0cfcba 100644 --- a/internal/db/seasonleague.go +++ b/internal/db/seasonleague.go @@ -35,8 +35,8 @@ func GetSeasonLeague(ctx context.Context, tx bun.Tx, seasonShortName, leagueShor if err != nil { return nil, nil, nil, errors.Wrap(err, "GetLeague") } - if season == nil || league == nil || !season.HasLeague(league.ID) { - return nil, nil, nil, nil + if !season.HasLeague(league.ID) { + return nil, nil, nil, BadRequestNotAssociated("season", "league", seasonShortName, leagueShortName) } // Get all teams participating in this season+league @@ -59,18 +59,12 @@ func NewSeasonLeague(ctx context.Context, tx bun.Tx, seasonShortName, leagueShor if err != nil { return errors.Wrap(err, "GetSeason") } - if season == nil { - return errors.New("season not found") - } league, err := GetLeague(ctx, tx, leagueShortName) if err != nil { return errors.Wrap(err, "GetLeague") } - if league == nil { - return errors.New("league not found") - } if season.HasLeague(league.ID) { - return errors.New("league already added to season") + return BadRequestAssociated("season", "league", seasonShortName, leagueShortName) } seasonLeague := &SeasonLeague{ SeasonID: season.ID, @@ -94,9 +88,6 @@ func (s *Season) RemoveLeague(ctx context.Context, tx bun.Tx, leagueShortName st if err != nil { return errors.Wrap(err, "GetLeague") } - if league == nil { - return errors.New("league not found") - } if !s.HasLeague(league.ID) { return errors.New("league not in season") } diff --git a/internal/db/setup.go b/internal/db/setup.go index dd2b4b4..e5b39e4 100644 --- a/internal/db/setup.go +++ b/internal/db/setup.go @@ -32,6 +32,7 @@ func (db *DB) RegisterModels() []any { (*Role)(nil), (*Permission)(nil), (*AuditLog)(nil), + (*Fixture)(nil), } db.RegisterModel(models...) return models diff --git a/internal/db/teamparticipation.go b/internal/db/teamparticipation.go index 51eb6dc..d1cdd47 100644 --- a/internal/db/teamparticipation.go +++ b/internal/db/teamparticipation.go @@ -23,28 +23,19 @@ func NewTeamParticipation(ctx context.Context, tx bun.Tx, if err != nil { return nil, nil, nil, errors.Wrap(err, "GetSeason") } - if season == nil { - return nil, nil, nil, errors.New("season not found") - } league, err := GetLeague(ctx, tx, leagueShortName) if err != nil { return nil, nil, nil, errors.Wrap(err, "GetLeague") } - if league == nil { - return nil, nil, nil, errors.New("league not found") - } if !season.HasLeague(league.ID) { - return nil, nil, nil, errors.New("league is not assigned to the season") + return nil, nil, nil, BadRequestNotAssociated("season", "league", seasonShortName, leagueShortName) } team, err := GetTeam(ctx, tx, teamID) if err != nil { return nil, nil, nil, errors.Wrap(err, "GetTeam") } - if team == nil { - return nil, nil, nil, errors.New("team not found") - } if team.InSeason(season.ID) { - return nil, nil, nil, errors.New("team already in season") + return nil, nil, nil, BadRequestAssociated("season", "team", seasonShortName, teamID) } participation := &TeamParticipation{ SeasonID: season.ID, diff --git a/internal/db/update.go b/internal/db/update.go index 900ef06..de41b83 100644 --- a/internal/db/update.go +++ b/internal/db/update.go @@ -85,10 +85,18 @@ func (u *updater[T]) Exec(ctx context.Context) error { } // Execute update - _, err := u.q.Exec(ctx) + result, err := u.q.Exec(ctx) if err != nil { return errors.Wrap(err, "bun.UpdateQuery.Exec") } + rows, err := result.RowsAffected() + if err != nil { + return errors.Wrap(err, "result.RowsAffected") + } + if rows == 0 { + resource := extractResourceType(extractTableName[T]()) + return BadRequestNotFound(resource, "id", extractPrimaryKey(u.model)) + } // Handle audit logging if enabled if u.audit != nil { diff --git a/internal/db/user.go b/internal/db/user.go index 4826f31..fb276d7 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -53,13 +53,13 @@ func CreateUser(ctx context.Context, tx bun.Tx, username string, discorduser *di } // GetUserByID queries the database for a user matching the given ID -// Returns nil, nil if no user is found +// 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) } // GetUserByUsername queries the database for a user matching the given username -// Returns nil, nil if no user is found +// Returns a BadRequestNotFound error if no user is found func GetUserByUsername(ctx context.Context, tx bun.Tx, username string) (*User, error) { if username == "" { return nil, errors.New("username not provided") @@ -68,7 +68,7 @@ func GetUserByUsername(ctx context.Context, tx bun.Tx, username string) (*User, } // GetUserByDiscordID queries the database for a user matching the given discord id -// Returns nil, nil if no user is found +// Returns a BadRequestNotFound error if no user is found func GetUserByDiscordID(ctx context.Context, tx bun.Tx, discordID string) (*User, error) { if discordID == "" { return nil, errors.New("discord_id not provided") diff --git a/internal/db/userrole.go b/internal/db/userrole.go index 20c1a47..38c1bd1 100644 --- a/internal/db/userrole.go +++ b/internal/db/userrole.go @@ -94,9 +94,6 @@ func HasRole(ctx context.Context, tx bun.Tx, userID int, roleName roles.Role) (b if err != nil { return false, errors.Wrap(err, "GetByID") } - if user == nil { - return false, nil - } for _, role := range user.Roles { if role.Name == roleName { return true, nil diff --git a/internal/handlers/admin_audit.go b/internal/handlers/admin_audit.go index c5a2c88..1787add 100644 --- a/internal/handlers/admin_audit.go +++ b/internal/handlers/admin_audit.go @@ -185,12 +185,12 @@ func AdminAuditLogDetail(s *hws.Server, conn *db.DB) http.Handler { var err error log, err = db.GetAuditLogByID(ctx, tx, id) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetAuditLogByID") } - if log == nil { - throw.NotFound(s, w, r, r.URL.Path) - return false, nil - } return true, nil }); !ok { return diff --git a/internal/handlers/admin_preview_role.go b/internal/handlers/admin_preview_role.go index 3723972..d4db973 100644 --- a/internal/handlers/admin_preview_role.go +++ b/internal/handlers/admin_preview_role.go @@ -31,12 +31,12 @@ func AdminPreviewRoleStart(s *hws.Server, conn *db.DB, ssl bool) http.Handler { var err error role, err = db.GetRoleByID(ctx, tx, roleID) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, "Role not found") + return false, nil + } return false, errors.Wrap(err, "db.GetRoleByID") } - if role == nil { - throw.NotFound(s, w, r, "Role not found") - return false, nil - } // Cannot preview admin role if role.Name == roles.Admin { throw.BadRequest(s, w, r, "Cannot preview admin role", nil) diff --git a/internal/handlers/admin_roles.go b/internal/handlers/admin_roles.go index 53e6cbd..3394253 100644 --- a/internal/handlers/admin_roles.go +++ b/internal/handlers/admin_roles.go @@ -9,6 +9,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/roles" "git.haelnorr.com/h/oslstats/internal/validation" adminview "git.haelnorr.com/h/oslstats/internal/view/adminview" @@ -108,7 +109,7 @@ func AdminRoleManage(s *hws.Server, conn *db.DB) http.Handler { roleIDStr := r.PathValue("id") roleID, err := strconv.Atoi(roleIDStr) if err != nil { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } @@ -117,11 +118,12 @@ func AdminRoleManage(s *hws.Server, conn *db.DB) http.Handler { var err error role, err = db.GetRoleByID(ctx, tx, roleID) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetRoleByID") } - if role == nil { - return false, errors.New("role not found") - } return true, nil }); !ok { return @@ -146,11 +148,12 @@ func AdminRoleDeleteConfirm(s *hws.Server, conn *db.DB) http.Handler { var err error role, err = db.GetRoleByID(ctx, tx, roleID) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetRoleByID") } - if role == nil { - return false, errors.New("role not found") - } return true, nil }); !ok { return @@ -166,7 +169,7 @@ func AdminRoleDelete(s *hws.Server, conn *db.DB) http.Handler { roleIDStr := r.PathValue("id") roleID, err := strconv.Atoi(roleIDStr) if err != nil { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } @@ -180,11 +183,12 @@ func AdminRoleDelete(s *hws.Server, conn *db.DB) http.Handler { // First check if role exists and get its details role, err := db.GetRoleByID(ctx, tx, roleID) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetRoleByID") } - if role == nil { - return false, errors.New("role not found") - } // Check if it's a system role if role.IsSystem { @@ -194,6 +198,10 @@ func AdminRoleDelete(s *hws.Server, conn *db.DB) http.Handler { // Delete the role with audit logging err = db.DeleteRole(ctx, tx, roleID, db.NewAudit(r, nil)) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.DeleteRole") } @@ -218,7 +226,7 @@ func AdminRolePermissionsModal(s *hws.Server, conn *db.DB) http.Handler { roleIDStr := r.PathValue("id") roleID, err := strconv.Atoi(roleIDStr) if err != nil { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } @@ -232,11 +240,12 @@ func AdminRolePermissionsModal(s *hws.Server, conn *db.DB) http.Handler { var err error role, err = db.GetRoleByID(ctx, tx, roleID) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetRoleByID") } - if role == nil { - return false, errors.New("role not found") - } // Load all permissions allPermissions, err = db.ListAllPermissions(ctx, tx) @@ -281,7 +290,7 @@ func AdminRolePermissionsUpdate(s *hws.Server, conn *db.DB) http.Handler { roleIDStr := r.PathValue("id") roleID, err := strconv.Atoi(roleIDStr) if err != nil { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } @@ -305,12 +314,12 @@ func AdminRolePermissionsUpdate(s *hws.Server, conn *db.DB) http.Handler { if ok := conn.WithWriteTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { role, err := db.GetRoleByID(ctx, tx, roleID) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetRoleByID") } - if role == nil { - w.WriteHeader(http.StatusBadRequest) - return false, nil - } err = role.UpdatePermissions(ctx, tx, permissionIDs, db.NewAudit(r, nil)) if err != nil { return false, errors.Wrap(err, "role.UpdatePermissions") diff --git a/internal/handlers/auth_helpers.go b/internal/handlers/auth_helpers.go index 5aac97b..6cde8a6 100644 --- a/internal/handlers/auth_helpers.go +++ b/internal/handlers/auth_helpers.go @@ -42,9 +42,6 @@ func ensureUserHasAdminRole(ctx context.Context, tx bun.Tx, user *db.User) error if err != nil { return errors.Wrap(err, "db.GetRoleByName") } - if adminRole == nil { - return errors.New("admin role not found in database") - } // Grant admin role err = db.AssignRole(ctx, tx, user.ID, adminRole.ID, nil) diff --git a/internal/handlers/callback.go b/internal/handlers/callback.go index 99e016b..c713fd0 100644 --- a/internal/handlers/callback.go +++ b/internal/handlers/callback.go @@ -158,7 +158,7 @@ func login( } user, err := db.GetUserByDiscordID(ctx, tx, discorduser.ID) - if err != nil { + if err != nil && !db.IsBadRequest(err) { return nil, errors.Wrap(err, "db.GetUserByDiscordID") } var redirect string diff --git a/internal/handlers/isunique.go b/internal/handlers/isunique.go index 4f4d4d7..3190f2b 100644 --- a/internal/handlers/isunique.go +++ b/internal/handlers/isunique.go @@ -2,10 +2,12 @@ package handlers import ( "context" + "fmt" "net/http" "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/validation" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -22,12 +24,12 @@ func IsUnique( return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { getter, err := validation.ParseForm(r) if err != nil { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } value := getter.String(field).TrimSpace().Required().Value if !getter.Validate() { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } unique := false @@ -41,9 +43,10 @@ func IsUnique( return } if unique { - w.WriteHeader(http.StatusOK) + respond.OK(w) } else { - w.WriteHeader(http.StatusConflict) + err := fmt.Errorf("'%s' is not unique for field '%s'", value, field) + respond.Conflict(w, err) } }) } diff --git a/internal/handlers/leagues_new.go b/internal/handlers/leagues_new.go index 07fe0fa..f033286 100644 --- a/internal/handlers/leagues_new.go +++ b/internal/handlers/leagues_new.go @@ -11,6 +11,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/notify" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/validation" leaguesview "git.haelnorr.com/h/oslstats/internal/view/leaguesview" ) @@ -78,8 +79,7 @@ func NewLeagueSubmit( notify.Warn(s, w, r, "Duplicate Short Name", "This short name is already taken.", nil) return } - w.Header().Set("HX-Redirect", fmt.Sprintf("/leagues/%s", league.ShortName)) - w.WriteHeader(http.StatusOK) + respond.HXRedirect(w, "/leagues/%s", league.ShortName) notify.SuccessWithDelay(s, w, r, "League Created", fmt.Sprintf("Successfully created league: %s", name), nil) }) } diff --git a/internal/handlers/login.go b/internal/handlers/login.go index 5097651..6720e16 100644 --- a/internal/handlers/login.go +++ b/internal/handlers/login.go @@ -12,6 +12,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/discord" "git.haelnorr.com/h/oslstats/internal/notify" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/store" "git.haelnorr.com/h/oslstats/internal/throw" "git.haelnorr.com/h/oslstats/pkg/oauth" @@ -34,10 +35,10 @@ func Login( if r.Method == "POST" { if err != nil { notify.ServiceUnavailable(s, w, r, "Login currently unavailable", err) - w.WriteHeader(http.StatusOK) + respond.OK(w) return } - w.Header().Set("HX-Redirect", "/login") + respond.HXRedirect(w, "/login") return } diff --git a/internal/handlers/logout.go b/internal/handlers/logout.go index 40f2cc9..2720943 100644 --- a/internal/handlers/logout.go +++ b/internal/handlers/logout.go @@ -8,6 +8,7 @@ import ( "git.haelnorr.com/h/golib/hwsauth" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/discord" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/throw" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -22,11 +23,6 @@ func Logout( return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { user := db.CurrentUser(r.Context()) - if user == nil { - // JIC - should be impossible to get here if route is protected by LoginReq - w.Header().Set("HX-Redirect", "/") - return - } if ok := conn.WithWriteTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { token, err := user.DeleteDiscordTokens(ctx, tx) if err != nil { @@ -48,7 +44,7 @@ func Logout( }); !ok { return } - w.Header().Set("HX-Redirect", "/") + respond.HXRedirect(w, "/") }, ) } diff --git a/internal/handlers/register.go b/internal/handlers/register.go index de76f67..b4f7a28 100644 --- a/internal/handlers/register.go +++ b/internal/handlers/register.go @@ -12,6 +12,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/config" "git.haelnorr.com/h/oslstats/internal/db" + "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" @@ -82,7 +83,7 @@ func Register( return } if !unique { - w.WriteHeader(http.StatusConflict) + respond.Conflict(w, errors.New("username is taken")) } else { err = auth.Login(w, r, user, true) if err != nil { @@ -90,7 +91,7 @@ func Register( return } pageFrom := cookies.CheckPageFrom(w, r) - w.Header().Set("HX-Redirect", pageFrom) + respond.HXRedirect(w, "%s", pageFrom) } }, ) diff --git a/internal/handlers/season_detail.go b/internal/handlers/season_detail.go index 6be206b..541754f 100644 --- a/internal/handlers/season_detail.go +++ b/internal/handlers/season_detail.go @@ -25,11 +25,12 @@ func SeasonPage( var err error season, err = db.GetSeason(ctx, tx, seasonStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeason") } - if season == nil { - return true, nil - } leaguesWithTeams, err = season.MapTeamsToLeagues(ctx, tx) if err != nil { @@ -40,10 +41,6 @@ func SeasonPage( }); !ok { return } - if season == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } renderSafely(seasonsview.DetailPage(season, leaguesWithTeams), s, r, w) }) } diff --git a/internal/handlers/season_edit.go b/internal/handlers/season_edit.go index 3a0a898..42f3e92 100644 --- a/internal/handlers/season_edit.go +++ b/internal/handlers/season_edit.go @@ -8,6 +8,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/notify" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/throw" "git.haelnorr.com/h/oslstats/internal/validation" "git.haelnorr.com/h/oslstats/internal/view/seasonsview" @@ -28,6 +29,10 @@ func SeasonEditPage( var err error season, err = db.GetSeason(ctx, tx, seasonStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeason") } allLeagues, err = db.GetLeagues(ctx, tx) @@ -38,10 +43,6 @@ func SeasonEditPage( }); !ok { return } - if season == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } renderSafely(seasonsview.EditPage(season, allLeagues), s, r, w) }) } @@ -79,11 +80,12 @@ func SeasonEditSubmit( var err error season, err = db.GetSeason(ctx, tx, seasonStr) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetSeason") } - if season == nil { - return false, errors.New("season does not exist") - } err = season.Update(ctx, tx, version, start, end, finalsStart, finalsEnd, db.NewAudit(r, nil)) if err != nil { return false, errors.Wrap(err, "season.Update") @@ -93,13 +95,7 @@ func SeasonEditSubmit( return } - if season == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - - w.Header().Set("HX-Redirect", fmt.Sprintf("/seasons/%s", season.ShortName)) - w.WriteHeader(http.StatusOK) + respond.HXRedirect(w, "/seasons/%s", season.ShortName) notify.SuccessWithDelay(s, w, r, "Season Updated", fmt.Sprintf("Successfully updated season: %s", season.Name), nil) }) } diff --git a/internal/handlers/season_league_add_team.go b/internal/handlers/season_league_add_team.go index f3f516a..9c4abf7 100644 --- a/internal/handlers/season_league_add_team.go +++ b/internal/handlers/season_league_add_team.go @@ -38,6 +38,10 @@ func SeasonLeagueAddTeam( var err error team, season, league, err = db.NewTeamParticipation(ctx, tx, seasonStr, leagueStr, teamID, db.NewAudit(r, nil)) if err != nil { + if db.IsBadRequest(err) { + w.WriteHeader(http.StatusBadRequest) + return false, nil + } return false, errors.Wrap(err, "db.NewTeamParticipation") } return true, nil diff --git a/internal/handlers/season_league_detail.go b/internal/handlers/season_league_detail.go index 6e1f737..6e93f6b 100644 --- a/internal/handlers/season_league_detail.go +++ b/internal/handlers/season_league_detail.go @@ -22,12 +22,15 @@ func SeasonLeaguePage( leagueStr := r.PathValue("league_short_name") var season *db.Season - var league *db.League if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { var err error - season, league, _, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) + season, _, _, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeasonLeague") } return true, nil @@ -35,11 +38,6 @@ func SeasonLeaguePage( return } - if season == nil || league == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - defaultTab := season.GetDefaultTab() redirectURL := fmt.Sprintf( "/seasons/%s/leagues/%s/%s", diff --git a/internal/handlers/season_league_finals.go b/internal/handlers/season_league_finals.go index 2cd264c..b8f0187 100644 --- a/internal/handlers/season_league_finals.go +++ b/internal/handlers/season_league_finals.go @@ -7,7 +7,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/throw" - seasonsview "git.haelnorr.com/h/oslstats/internal/view/seasonsview" + "git.haelnorr.com/h/oslstats/internal/view/seasonsview" "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -28,6 +28,10 @@ func SeasonLeagueFinalsPage( var err error season, league, _, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeasonLeague") } return true, nil @@ -35,11 +39,6 @@ func SeasonLeagueFinalsPage( return } - if season == nil || league == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - if r.Method == "GET" { renderSafely(seasonsview.SeasonLeagueFinalsPage(season, league), s, r, w) } else { diff --git a/internal/handlers/season_league_fixtures.go b/internal/handlers/season_league_fixtures.go index bcd782c..24c0f0f 100644 --- a/internal/handlers/season_league_fixtures.go +++ b/internal/handlers/season_league_fixtures.go @@ -28,6 +28,10 @@ func SeasonLeagueFixturesPage( var err error season, league, _, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeasonLeague") } return true, nil @@ -35,11 +39,6 @@ func SeasonLeagueFixturesPage( return } - if season == nil || league == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - if r.Method == "GET" { renderSafely(seasonsview.SeasonLeagueFixturesPage(season, league), s, r, w) } else { diff --git a/internal/handlers/season_league_stats.go b/internal/handlers/season_league_stats.go index d3736a6..80b1c2b 100644 --- a/internal/handlers/season_league_stats.go +++ b/internal/handlers/season_league_stats.go @@ -28,6 +28,10 @@ func SeasonLeagueStatsPage( var err error season, league, _, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeasonLeague") } return true, nil @@ -35,11 +39,6 @@ func SeasonLeagueStatsPage( return } - if season == nil || league == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - if r.Method == "GET" { renderSafely(seasonsview.SeasonLeagueStatsPage(season, league), s, r, w) } else { diff --git a/internal/handlers/season_league_table.go b/internal/handlers/season_league_table.go index 89aa803..a061032 100644 --- a/internal/handlers/season_league_table.go +++ b/internal/handlers/season_league_table.go @@ -28,18 +28,16 @@ func SeasonLeagueTablePage( var err error season, league, _, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeasonLeague") } return true, nil }); !ok { return } - - if season == nil || league == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - if r.Method == "GET" { renderSafely(seasonsview.SeasonLeagueTablePage(season, league), s, r, w) } else { diff --git a/internal/handlers/season_league_teams.go b/internal/handlers/season_league_teams.go index e2c75b7..bd93bc3 100644 --- a/internal/handlers/season_league_teams.go +++ b/internal/handlers/season_league_teams.go @@ -30,6 +30,10 @@ func SeasonLeagueTeamsPage( var err error season, league, teams, err = db.GetSeasonLeague(ctx, tx, seasonStr, leagueStr) if err != nil { + if db.IsBadRequest(err) { + throw.NotFound(s, w, r, r.URL.Path) + return false, nil + } return false, errors.Wrap(err, "db.GetSeasonLeague") } @@ -46,11 +50,6 @@ func SeasonLeagueTeamsPage( return } - if season == nil || league == nil { - throw.NotFound(s, w, r, r.URL.Path) - return - } - if r.Method == "GET" { renderSafely(seasonsview.SeasonLeagueTeamsPage(season, league, teams, available), s, r, w) } else { diff --git a/internal/handlers/season_leagues.go b/internal/handlers/season_leagues.go index 5411e3f..963730d 100644 --- a/internal/handlers/season_leagues.go +++ b/internal/handlers/season_leagues.go @@ -10,6 +10,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/notify" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/view/seasonsview" ) @@ -26,6 +27,10 @@ func SeasonAddLeague( 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)) if err != nil { + if db.IsBadRequest(err) { + respond.BadRequest(w, err) + return false, nil + } return false, errors.Wrap(err, "db.NewSeasonLeague") } @@ -64,13 +69,17 @@ func SeasonRemoveLeague( var err error season, err = db.GetSeason(ctx, tx, seasonStr) if err != nil { + if db.IsBadRequest(err) { + respond.NotFound(w, err) + return false, nil + } return false, errors.Wrap(err, "db.GetSeason") } - if season == nil { - return false, errors.New("season not found") - } err = season.RemoveLeague(ctx, tx, leagueStr, db.NewAudit(r, nil)) if err != nil { + if db.IsBadRequest(err) { + respond.BadRequest(w, err) + } return false, errors.Wrap(err, "season.RemoveLeague") } diff --git a/internal/handlers/seasons_new.go b/internal/handlers/seasons_new.go index b2fd21b..547e11a 100644 --- a/internal/handlers/seasons_new.go +++ b/internal/handlers/seasons_new.go @@ -8,6 +8,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/notify" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/validation" seasonsview "git.haelnorr.com/h/oslstats/internal/view/seasonsview" "git.haelnorr.com/h/timefmt" @@ -83,8 +84,7 @@ func NewSeasonSubmit( notify.Warn(s, w, r, "Duplicate Short Name", "This short name is already taken.", nil) return } - w.Header().Set("HX-Redirect", fmt.Sprintf("/seasons/%s", season.ShortName)) - w.WriteHeader(http.StatusOK) + respond.HXRedirect(w, "/seasons/%s", season.ShortName) notify.SuccessWithDelay(s, w, r, "Season Created", fmt.Sprintf("Successfully created season: %s", name), nil) }) } diff --git a/internal/handlers/static.go b/internal/handlers/static.go index 8c35ece..8113033 100644 --- a/internal/handlers/static.go +++ b/internal/handlers/static.go @@ -2,10 +2,9 @@ package handlers import ( "net/http" - "path/filepath" - "strings" "git.haelnorr.com/h/golib/hws" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/throw" ) @@ -23,41 +22,8 @@ func StaticFS(staticFS *http.FileSystem, server *hws.Server) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - // Explicitly set Content-Type for CSS files - if strings.HasSuffix(r.URL.Path, ".css") { - w.Header().Set("Content-Type", "text/css; charset=utf-8") - } else if strings.HasSuffix(r.URL.Path, ".js") { - w.Header().Set("Content-Type", "application/javascript; charset=utf-8") - } else if strings.HasSuffix(r.URL.Path, ".ico") { - w.Header().Set("Content-Type", "image/x-icon") - } else { - // Let Go detect the content type for other files - ext := filepath.Ext(r.URL.Path) - if contentType := mimeTypes[ext]; contentType != "" { - w.Header().Set("Content-Type", contentType) - } - } + respond.ContentType(w, r) fs.ServeHTTP(w, r) }, ) } - -// Common MIME types for static files -var mimeTypes = map[string]string{ - ".html": "text/html; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".js": "application/javascript; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".xml": "application/xml; charset=utf-8", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".eot": "application/vnd.ms-fontobject", -} diff --git a/internal/handlers/team_shortnames_unique.go b/internal/handlers/team_shortnames_unique.go index 7ee2b57..625ec2d 100644 --- a/internal/handlers/team_shortnames_unique.go +++ b/internal/handlers/team_shortnames_unique.go @@ -6,6 +6,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/validation" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -20,7 +21,7 @@ func IsTeamShortNamesUnique( return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { getter, err := validation.ParseForm(r) if err != nil { - w.WriteHeader(http.StatusBadRequest) + respond.BadRequest(w, err) return } @@ -28,12 +29,12 @@ func IsTeamShortNamesUnique( altShortName := getter.String("alt_short_name").TrimSpace().ToUpper().MaxLength(3).Value if shortName == "" || altShortName == "" { - w.WriteHeader(http.StatusOK) + respond.OK(w) return } if shortName == altShortName { - w.WriteHeader(http.StatusConflict) + respond.Conflict(w, errors.New("short names cannot be the same")) return } @@ -49,9 +50,9 @@ func IsTeamShortNamesUnique( } if isUnique { - w.WriteHeader(http.StatusOK) + respond.OK(w) } else { - w.WriteHeader(http.StatusConflict) + respond.Conflict(w, errors.New("short name combination is taken")) } }) } diff --git a/internal/handlers/teams_list.go b/internal/handlers/teams_list.go index 5eaec71..2eff9b5 100644 --- a/internal/handlers/teams_list.go +++ b/internal/handlers/teams_list.go @@ -32,31 +32,10 @@ func TeamsPage( }); !ok { return } - renderSafely(teamsview.ListPage(teams), s, r, w) - }) -} - -// TeamsList renders just the teams list, for use with POST requests and HTMX -func TeamsList( - s *hws.Server, - conn *db.DB, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - pageOpts, ok := db.GetPageOpts(s, w, r) - if !ok { - return - } - var teams *db.List[db.Team] - if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { - var err error - teams, err = db.ListTeams(ctx, tx, pageOpts) - if err != nil { - return false, errors.Wrap(err, "db.ListTeams") - } - return true, nil - }); !ok { - return - } - renderSafely(teamsview.TeamsList(teams), s, r, w) + if r.Method == "GET" { + renderSafely(teamsview.ListPage(teams), s, r, w) + } else { + renderSafely(teamsview.TeamsList(teams), s, r, w) + } }) } diff --git a/internal/handlers/teams_new.go b/internal/handlers/teams_new.go index 0423454..9bf8fd4 100644 --- a/internal/handlers/teams_new.go +++ b/internal/handlers/teams_new.go @@ -11,6 +11,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/notify" + "git.haelnorr.com/h/oslstats/internal/respond" "git.haelnorr.com/h/oslstats/internal/validation" teamsview "git.haelnorr.com/h/oslstats/internal/view/teamsview" ) @@ -88,8 +89,7 @@ func NewTeamSubmit( notify.Warn(s, w, r, "Duplicate Short Names", "This combination of short names is already taken.", nil) return } - w.Header().Set("HX-Redirect", "/teams") - w.WriteHeader(http.StatusOK) + respond.HXRedirect(w, "teams") notify.SuccessWithDelay(s, w, r, "Team Created", fmt.Sprintf("Successfully created team: %s", name), nil) }) } diff --git a/internal/respond/error.go b/internal/respond/error.go new file mode 100644 index 0000000..be33827 --- /dev/null +++ b/internal/respond/error.go @@ -0,0 +1,36 @@ +// Package respond provides utilities for raw HTTP responses that don't fit into the throw or notify categories +package respond + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func OK(w http.ResponseWriter) { + w.WriteHeader(http.StatusOK) +} + +func BadRequest(w http.ResponseWriter, err error) { + respondError(w, http.StatusBadRequest, err) +} + +func NotFound(w http.ResponseWriter, err error) { + respondError(w, http.StatusNotFound, err) +} + +func Conflict(w http.ResponseWriter, err error) { + respondError(w, http.StatusConflict, err) +} + +func respondError(w http.ResponseWriter, statusCode int, err error) { + details := map[string]any{ + "error": statusCode, + "details": fmt.Sprintf("%s", err), + } + resp, err := json.Marshal(details) + w.WriteHeader(statusCode) + if err == nil { + _, _ = w.Write(resp) + } +} diff --git a/internal/respond/headers.go b/internal/respond/headers.go new file mode 100644 index 0000000..cffa46c --- /dev/null +++ b/internal/respond/headers.go @@ -0,0 +1,39 @@ +package respond + +import ( + "fmt" + "net/http" + "path/filepath" +) + +func HXRedirect(w http.ResponseWriter, format string, a ...any) { + w.Header().Set("HX-Redirect", fmt.Sprintf(format, a...)) + w.WriteHeader(http.StatusOK) +} + +func ContentType(w http.ResponseWriter, r *http.Request) { + ext := filepath.Ext(r.URL.Path) + if contentType := mimeTypes[ext]; contentType != "" { + w.Header().Set("Content-Type", contentType) + } +} + +// Common MIME types for static files +var mimeTypes = map[string]string{ + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".eot": "application/vnd.ms-fontobject", +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 9948375..a1f5440 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -163,14 +163,9 @@ func addRoutes( teamRoutes := []hws.Route{ { Path: "/teams", - Method: hws.MethodGET, + Methods: []hws.Method{hws.MethodGET, hws.MethodPOST}, Handler: handlers.TeamsPage(s, conn), }, - { - Path: "/teams", - Method: hws.MethodPOST, - Handler: handlers.TeamsList(s, conn), - }, { Path: "/teams/new", Method: hws.MethodGET, diff --git a/internal/validation/forms.go b/internal/validation/forms.go index 26120d8..7f59de6 100644 --- a/internal/validation/forms.go +++ b/internal/validation/forms.go @@ -2,6 +2,7 @@ package validation import ( "net/http" + "strings" "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/notify" @@ -25,7 +26,7 @@ func (f *FormGetter) Get(key string) string { } func (f *FormGetter) GetList(key string) []string { - return f.r.Form[key] + return strings.Split(f.Get(key), ",") } func (f *FormGetter) getChecks() []*ValidationRule { diff --git a/justfile b/justfile index 015d1e5..b2b5952 100644 --- a/justfile +++ b/justfile @@ -114,7 +114,7 @@ _migrate-status: {{bin}}/{{entrypoint}} --migrate-status --envfile $ENVFILE [private] -_migrate-new name: && _migrate-status +_migrate-new name: && _build _migrate-status {{bin}}/{{entrypoint}} --migrate-create {{name}} # Hard reset the database