backend
Technical notes on web development, DevOps, and AI integration.
128 articles
- 12:58backend
A Deleted User Is an Authentication Failure: 401, Not 500
A cryptographically valid JWT can still name a user who no longer exists. Mapping sql.ErrNoRows to 401 lets the frontend purge the stale session.
TL;DR: A deleted user's JWT kept hitting the API, and since the middleware treated missing rows as a generic DB error, users got stuck in a 500 banner loop instead of being logged out. The fix uses errors.Is(err, sql.ErrNoRows) to return 401, letting the frontend purge the stale session. Tests now pin both paths.
#go#golang#jwt - 05:37backend
Out-of-System Payments: a Marker, Not a Module
An externally verified payment modeled as two nullable columns: required *bool contract, a dedicated 409, and a two-arm union SQL.
TL;DR: The system never handles payments, so the fix adds just two nullable columns, payment_marked and payment_marked_at, to track when treasury verifies fees externally. A guarded endpoint uses *bool decoding so missing fields fail loudly instead of silently clearing the marker. Public API hides the columns, admin API exposes them, and submission status stays untouched.
#go#mysql#api - 05:23backend
Schedule Conflict Checks as a Warning, Not a Wall
A booking form that warns about schedule clashes without blocking: read-only endpoint, inclusive overlap query, debounce and abort.
TL;DR: A hall got double-booked because two officers approved overlapping applications without checking each other, and the old form couldn't flag taken dates. The shipped fix pairs a validated, read-only endpoint that flags overlapping schedules with a debounced, race-safe warning box on the form. The warning never blocks submission; the officer still decides.
#go#mysql#react - 04:42backend
Four Contracts Behind a Public Upload Form
One multipart endpoint enforces four contracts: closed enums, conditional file slots, bounded streaming, and safe schema migration.
TL;DR: The upload form enforces four contracts: hand-maintained Go enums for validation, files that become required only when a flag says so, streaming writes capped at 10 MiB with magic-byte checks into private storage, and schema migrations that clean up enum values before narrowing on rollback. All four must stay synchronized server-side. Any drift breaks the endpoint predictably.
#go#multipart#file-upload - 04:42backend
Calendar-Day Flooring Is Not 24-Hour Truncation in Go
time.Truncate cuts by duration since the zero time, not to local midnight. Here is the safe pattern for DATE columns.
TL;DR: Go's time.Truncate(24 * time.Hour) counts multiples from Go's zero time, not local midnight, so early-morning timestamps on a UTC+7 server slip to the previous day. The MySQL driver's default UTC location conversion then locks the wrong day in. Fix: extract date components and rebuild with time.Date at UTC midnight.
#go#mysql#timezone - 01:34security
Guard the Contents, Not the Container: Two-Sided Rebind Media
The endpoint guarded the target entity but never the source media ids. After a rebind, managing someone else's media became legitimate.
TL;DR: An editor rebinds another bidang's media into their own gallery because the endpoint guarded only the target, not the source. The fix resolves media modules in one parameterized batched query and rejects out-of-scope ids with a static 403 before any write. A similar unguarded path in announcements shows source objects need the same object-level checks on every write path.
#security#rbac#idor - 01:34database
Flipping a Column Default: A Behavior Switch for New Rows
Changing one column DEFAULT is instant and metadata-only. The real work is auditing the INSERT paths whose meaning silently changed.
TL;DR: Changing a column default in MySQL is instant metadata-only, no table lock or rebuild. Existing rows keep their values; only INSERTs that omit the column flip behavior. Audit those callers first, then ship the migration.
#mysql#database-migration#schema - 00:24security
Enum and Slug: The Two Identifiers That Keep a Gate Shut
The moderation gate never rejected anyone: an unmapped enum simply compared false forever, and read-only won by default.
TL;DR: A read-only badge appeared on all reviews because the frontend passed raw module enums like PARIWISATA into a gate expecting slugs like wisata, so the check silently failed and defaulted to read-only. The fix centralized enum-to-slug mapping in one TypeScript Record. Server-side, guardModule now enforces deny-by-default checks with tests locking in 403s and 404s.
#rbac#typescript#authorization - 23:29backend
Two Sets: What You May Read, What You May Manage
Opening read access across departments without loosening writes: readable and manageable module sets as two separate fail-closed sets.
TL;DR: KotaPortal treated the module query parameter as a write gate, hiding all content from other departments. The fix splits access into two sets: ModulesFor for writable modules and ReadableModules for readable ones, with fail-closed checks moved earlier in the chain. The frontend keeps full navigation while write affordances hide per permission, marked by a read-only banner.
#rbac#golang#http