backend
Technical notes on web development, DevOps, and AI integration.
67 articles
- 06:27backend
Setting Another User's Bidang: A Privileged Action
Only a superadmin may set a user bidang; the ALL default keeps 19 legacy users through the migration unharmed.
TL;DR: The schema change was simple, but deciding who can assign another user's scope was the real challenge. A central resolveBidang function lets only superadmins set it, returning 403 if others try while still allowing edits to other fields. Defaulting to ALL keeps existing users unlocked, with strict validation and eight tests locking the behavior.
#go#mysql#security - 06:27backend
Announcement Scope: Writing a Value vs Mutating a Row
Two authorization functions for per-division announcements: resolveBidang for new values, canManage for existing rows, NULL as global.
TL;DR: Portal now scopes announcements by division with two separate checks. resolveBidang governs creation, letting ALL admins write global NULL or any bidang while scoped writers are locked to their own. canManage governs existing rows, so only ALL can touch global content and labeled rows are owner-only, enforced in the service layer before any mutation.
#go#mysql#security - 06:27backend
Extensible Entity Attributes: a TEXT Column Holding JSON
Adding a TEXT attributes column for per-module dynamic attributes, with validation moved into the application.
TL;DR: Instead of adding nullable columns for each module, we added a single TEXT attributes field to handle flexible data. Since MySQL TEXT doesn't validate JSON, the app enforces rules like paired labels and no duplicates before saving. It's a lightweight escape hatch that avoids schema churn and only gets promoted to a real column when filtering is needed.
#go#mysql#json - 05:37backend
COALESCE at the API Door: Keep the ALL Sentinel in the Query
A super-admin without a bidang returns NULL from the database. One COALESCE in the login query keeps the API contract total and the admin badge correct.
TL;DR: The badge broke for super-admins because their bidang is NULL and old tokens don't even have the field. Rather than patching each Go handler, the team used COALESCE(bidang, 'ALL') in SQL to guarantee a valid string everywhere. TypeScript keeps bidang optional for legacy sessions, and React only renders the badge when the value isn't ALL.
#mysql#golang#typescript - 05:05backend
Bidang RBAC Enforcement: The Middleware Gate and IN Filters
Enforcing the bidang scope in Go: middleware at the gate, IN filters in repositories, and subtests that read like the security policy.
TL;DR: Middleware at the gate verifies the JWT and injects the bidang scope so handlers stay clean. Repositories then filter by allowed modules using sqlx In, while announcements handle NULL as global, and global routes require ALL access. Leaving the repository fail-open keeps public calls simple, and subtests ensure scoped admins only see their own data.
#go#mysql#security - 04:57backend
Two-Axis RBAC: Roles for Actions, Bidang for Data
Roles decide actions, bidang decides data. Notes from building a fail-closed scope resolver in Go: ModulesFor, ResolveBidang, and the NULL trap.
TL;DR: Roles handle actions while a bidang column scopes data by agency, avoiding a huge permission matrix. A helper maps bidang to modules, returns nil for unknowns to fail closed, and copies the ALL slice to prevent Go append bugs. Announcements use NULL for global posts with an IS NULL filter, and empty bidang maps to ALL for legacy JWT compatibility.
#go#mysql#security - 04:23backend
XLSX Export from Go, and the Binary Guess That Missed
openapi-fetch does have parseAs blob. The friction is binary-format typing and the DOM download flow.
TL;DR: I assumed openapi-fetch couldn't handle binary downloads, but checking the source showed it supports blobs just fine. The real hurdles were typing that masks blobs as strings and the manual DOM work to trigger downloads. On the backend, the Go export validates module filters and always returns a valid file via excelize, even when empty.
#go#excelize#openapi-fetch - 03:34backend
Move One Category, Watch the Tree Loop
A validation that only rejects self-parenting looks sufficient until a re-parent to your own descendant quietly forms a cycle in the category tree.
TL;DR: Moving A under B seemed fine but created a loop because B was already A's child. The old validation only blocked self-parenting, so the fix now walks up ancestors until it hits a root, a missing parent, or the original category. It uses the standard wrapped-error check and limits the walk to ten levels to handle legacy cycles safely.
#golang#mysql#backend - 02:23testing
Parity tests without a shared validator
Two validation gates stay separate, but a parametrized test table binds both to the same accept/reject contract and the same error precedence.
TL;DR: Two standalone modules rejected bad configs differently, and sharing validation code at runtime wasn’t allowed due to isolation rules. Instead of merging code, a parametrized pytest table now feeds hostile inputs to both, ensuring they agree on accept versus reject. It also enforces resolve-first ordering where path traversal errors beat file shape errors, keeping docs updated and modules isolated.
#python#testing#plane