Cinder pass across /admin, the login gate, and the library's a11y floor
Uncommitted work from three design runs on this branch, against one design system: docs/design-system.md is updated to match the CSS, not the reverse. Library (Reader-facing): - .chrome sticks at top: 0. Search and the tab row were unreachable three screens into a 300-item library, which is exactly where they earn their keep; everything above them still scrolls away on purpose. - One :focus-visible ring (2px --paper) on the nine controls that defined none and fell back to the UA blue. .searchbar keeps its border recolour as a resting cue but no longer stands in for a ring. - Mono labels lift 10px -> 11px everywhere. The brief names night reading and glare as the usage scene; 10px small-caps was where taste overrode it. - A card in flight past 2s says "Saving..." and carries aria-busy. htmx sets neither, so the wait up to its 15s timeout was silent in both channels. - Titles clamp at 3 lines; .is-new .title takes width: fit-content, or -webkit-box stretches the ember underline past the text it sizes to. /admin: - Overview routes into Lanes when a lane is unhealthy, prefixes each figure with its column word on the phone layout that drops the thead, labels state cells for a screen reader, and has an empty state where the sites table assumed rows. - The admin shell picks up the library's chrome: htmx 15s timeout, the shared #notice slot, #sr-announce, filter.js. admin.css follows the same pass. - admin_render_test.go and card_render_test.go render the templates directly, so markup regressions in either surface fail without a browser. Login: - DISCORD_GUILD_NAME (optional) names the community on the login screen and in the refusal message, so a stranger knows which Discord to ask for an invite. Unset degrades to a generic label; neither form names the guild id. Handlers: - maxChapterNum (9999) bounds both typed-chapter paths. uiChapter and adminSeriesCorrectLatest each parsed a float64 with no ceiling, so a hand-rolled POST stored 1e308 and every later reader of that row inherited it. Matches the max on the card's chapter input. go test ./... green.
This commit is contained in:
@@ -42,6 +42,11 @@ type DiscordConfig struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
GuildID string
|
||||
// GuildName is a human-readable name for the guild that gates access.
|
||||
// It is never fetched from Discord; it is an optional display string the
|
||||
// operator sets (DISCORD_GUILD_NAME) so the login screen can name the
|
||||
// community without inventing one.
|
||||
GuildName string
|
||||
// RequiredRole, when non-empty, is a role ID a member must hold on top of
|
||||
// guild membership. Empty by default: membership alone suffices.
|
||||
RequiredRole string
|
||||
@@ -173,16 +178,21 @@ func (h *Handler) discordCallback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// The refusal is the same for a non-member and a member without the
|
||||
// required role, and it names neither the guild nor its id: an outsider
|
||||
// cannot tell whether the guild exists, let alone which one gates.
|
||||
// required role. When DISCORD_GUILD_NAME is set the message names the
|
||||
// community so a stranger knows which Discord to ask about; otherwise it
|
||||
// degrades to a generic community label and still gives the next step. It
|
||||
// never names the numeric guild id, which would not be actionable.
|
||||
//
|
||||
// It also returns before EnsureReader, so a refused sign-in leaves no
|
||||
// Reader row behind — the gate is the only thing standing between guild
|
||||
// membership and a library.
|
||||
if !isMember || (h.discord.RequiredRole != "" && !slices.Contains(member.Roles, h.discord.RequiredRole)) {
|
||||
h.limiter.Fail(ip, time.Now())
|
||||
h.renderLogin(w, http.StatusForbidden,
|
||||
"This Discord account is not a member of this community.")
|
||||
msg := "This Discord account is not a member of this community. Ask a member for an invite and try again."
|
||||
if h.discord.GuildName != "" {
|
||||
msg = fmt.Sprintf("This Discord account is not a member of the %s Discord. Ask a member for an invite and try again.", h.discord.GuildName)
|
||||
}
|
||||
h.renderLogin(w, http.StatusForbidden, msg)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user