Repository navigation
Enforce future-only purchase start_date server-side (defense in depth for #1249) #50
Description
Problem
PR LeanerCloud/cloud-commitments-cli#1258 (closes LeanerCloud/cloud-commitments-cli#1249) added a min attribute to the Add Purchases start-date <input type="date"> so the browser date-picker blocks past dates. The PR title and commit message say "enforce future-only planned start date", but the constraint is only a UA-side hint — there is no JS validation in handleAddPurchases and no server-side validation in handler_plans.go::parseCreatePurchasesRequest.
Today's behaviour
- Frontend (
frontend/src/plans.ts,handleAddPurchasesat ~line 2110): readsadd-purchases-start-date.value, passes it straight toapi.createPlannedPurchases(planId, count, startDate)without checking thatstartDate >= today. - Backend (
internal/api/handler_plans.go::parseCreatePurchasesRequest, lines 327-343): only validates the date format (YYYY-MM-DD). A past date is silently accepted and the executions are inserted withScheduledDatein the past. - Scheduler (
internal/purchase/manager.go:529):A pastif exec.ScheduledDate.After(now) { logging.Debugf("...not yet due...") continue }
ScheduledDatefalls through theAfter(now)guard and is treated as due-now, so all past-dated executions fire on the next tick.
How to reproduce a bypass
Any of:
- Firefox / Safari accept the form submission even when the value is < min (
minis advisory in these browsers; Chrome does block). - Devtools: remove
minattribute, pick a past date, submit. - Direct API call:
POST /plans/{id}/purchaseswith{"count":1,"start_date":"2020-01-01"}— the backend accepts it.
Why this matters
A user (or a misbehaving client) scheduling a past date inserts count purchase rows that immediately become "due" and queue for execution. With Approval pending → notification fires for all of them at once; with auto-approval thresholds, they execute immediately. Either way the user-visible Plan state diverges from what the picker visually suggested.
Suggested fix
Add startDate < today rejection at both layers:
- Server (
parseCreatePurchasesRequest):Note: the comparison should account for the user's timezone — a Pacific-time user at 23:00 local Dec 31 sees "today = Dec 31" but UTC truncated-day = Jan 1, so a strict UTC compare risks rejecting their valid same-day submission. Options: accept any date >= (UTC today - 1 day) as today (cheap, tolerates worst-case offset), or take the user's timezone from a profile field. Discuss in the PR review.if startDate.Before(time.Now().UTC().Truncate(24 * time.Hour)) { return nil, time.Time{}, NewClientError(400, "start_date must be today or later") }
- Client (
handleAddPurchases):Keeps the Firefox/devtools path closed and gives a clear inline error before round-tripping.const todayLocal = toLocalDateInputValue(new Date()); // helper added in PR LeanerCloud/cloud-commitments-cli#1258 if (startDate < todayLocal) { errorDiv.textContent = 'Start date must be today or later'; errorDiv.classList.remove('hidden'); return; }
Acceptance criteria
-
POST /plans/{id}/purchaseswithstart_date< today returns 400 with a clear message. - Backend test that confirms a past date is rejected.
- Frontend
handleAddPurchasesblocks the submit and surfaces the error inline (regression test inplans-range-validation.test.tsmirroring the count-range pattern). - No regression for
todayandtoday+N. - Decision recorded for timezone handling (UTC-truncate vs per-user TZ).
Related
- Surfaced during adversarial review of PR fix(ui): enforce future-only planned start date + widen Configure Purchase Term field cloud-commitments-cli#1258 (UI hint only, no server enforcement).
- Closes the title's "enforce future-only" promise that fix(ui): enforce future-only planned start date + widen Configure Purchase Term field cloud-commitments-cli#1258 only partially delivered.
- Touches the same code as fix(plans): Add Purchases start-date picker allows past dates (QA 5.6) cloud-commitments-cli#1249, but server-side, so kept separate to keep fix(ui): enforce future-only planned start date + widen Configure Purchase Term field cloud-commitments-cli#1258's effort/xs scope honest.
Activity
IAmJSD commentedon Oct 16, 2026
Claiming: working on this
test(api): pin the future-only start_date guard on Add Purchases (#1028)
fix(frontend): thirteen small UI, validation and a11y fixes (#1132)