Skip to content

fix: API key expiration dropdown never persists — date transform nulls relative expiration strings - #43

Merged
roncodes merged 1 commit into
dev-v0.2.15from
fix/api-key-expiration-serialization
Aug 29, 2026
Merged

fix: API key expiration dropdown never persists — date transform nulls relative expiration strings#43
roncodes merged 1 commit into
dev-v0.2.15from
fix/api-key-expiration-serialization

Conversation

@roncodes

Copy link
Copy Markdown
Member

Problem

Setting an expiry on an API credential in the developers console appears to succeed, but api_credentials.expires_at stays NULL. Every dropdown option behaves this way, including immediately, and the row saves without error — so revoking a key via expiry silently does nothing.

Root cause: the edit/create modal assigns the dropdown's relative strings ('never', 'immediately', 'in 1 hour', …) straight onto expires_at (controllers/api-keys/index.js:320-322), but the model declared @attr('date') expires_at. Ember Data's date transform serializes anything that isn't a valid Date instance to null, so every option reached the API as expires_at: null. 'never' mapping to NULL is correct by coincidence, which is probably why this survived so long.

The roll path was never affected: it reads the raw attribute and sends it via fetch.patch(..., { expiration: apiKey.get('expires_at') }), bypassing the serializer. That asymmetry confirmed the serializer as the culprit.

The server is the right owner of relative-time parsing — core-api's ApiCredential::setExpiresAtAttribute() is written to accept exactly these strings — so the client must deliver them untouched.

Fix

New expiration transform, and expires_at becomes @attr('expiration'):

  • serialize: strings pass through untouched (server resolves them); valid Dates → ISO string; anything else → null.
  • deserialize: identical to the stock date transform, so the display path (expiresAt computed with isValidDate/formatDate, the list's Expiry column) keeps receiving Date instances for API-returned datetimes.

Alternatives rejected

  • @attr('string') — breaks the display path: expiresAt calls isValidDate()/formatDate() on the value, and API responses would surface as raw strings ("Never" shown for keys that do expire). Every read site would need re-working.
  • Converting the relative option to a real Date client-side — moves expiry math into the console where it can drift from the server's strtotime interpretation, and changes immediately's meaning from save-time to select-time.
  • A serializer serializeAttribute override — equivalent behavior, but the transform is declarative, scoped to the attribute, and reusable.

Verification

  • Reproduced against a running console (dev server + local Fleetbase API): with pristine dev-engine@0.2.14, serializing an api-credential through the real store produced expires_at: null for all six dropdown options. With this fix, all six strings pass through verbatim and a Date serializes to ISO.
  • Server contract confirmed in the running API container: setExpiresAtAttribute maps neverNULL, immediately→now, in 1 hour/in 24 hours/in 3 days/in 7 days→correct future datetimes, ISO datetime→parsed. So the full path is: console sends 'in 24 hours' → DB gets the correct datetime.
  • Not verified end-to-end through an authenticated browser save (no console credentials available to the automation); the two halves above were verified independently against the same running stack.
  • immediately had a second, stacked defect server-side (strict < in Expirable::hasExpired()); fix(auth): api credentials survived revocation and creator removal core-api#246 makes it inclusive (<=), agreeing with ExpiryScope. Confirmed merged in core-api main. Both this PR and #246 must ship for immediately to reliably kill a key at the boundary instant.

Regression coverage (and making the suite runnable at all)

Nothing tested this path — and the engine's suite couldn't run: ember test died before executing a single test (ember-engines' LinkToExternal extends a non-extensible LinkTo under ember-source 5.4, and the lazy engine's modules aren't resolvable from the dummy app). CI never runs ember test either. This PR makes the suite runnable:

  • eager-load the engine for its own ember test runs only (same pattern as the fleetops engine's index.js); hosts always get the lazy engine
  • add @ember/legacy-built-in-components (devDependency) so LinkToExternal has an extensible base — the console already depends on it, which is why the console builds
  • add prismjs (devDependency) and skip the ember-prism component config for self-test builds, where ember-cli-node-assets registers the imports without funneling the files in and the vendor concat fails with ENOENT

New tests (all fail on the old @attr('date') code — verified by reverting the attr as a control):

  • tests/unit/transforms/expiration-test.js — string passthrough, Date→ISO, null/invalid handling, deserialization (incl. +0000 offsets)
  • tests/unit/models/api-credential-test.jsmodel.serialize().expires_at preserves each relative option end-to-end through the real serializer chain; Date→ISO; datetime deserializes to a Date and formats via the expiresAt computed

Note: with the suite now actually executing, ~44 pre-existing tests fail on missing dummy-app service stubs (hostRouter, intl, universe, …) — test debt that predates this PR, tracked separately.

Downstream

No api-credential model override exists in this workspace's host apps (checked the console and the fliit package). If FliitAU/fliit-extension shipped one downstream, it should be removable once this lands and the package is released.

Downstream reports: FliitAU/fliit-extension#2217, FliitAU/fliit-extension#2212. Related: fleetbase/core-api#246

The api-keys console assigns the expiration dropdown's relative strings
('never', 'immediately', 'in 1 hour', ...) directly to expires_at, but the
attribute was declared @attr('date') and Ember Data's date transform
serializes any non-Date value to null — so every option reached the API as
expires_at: null and no expiry was ever persisted. The roll path was
unaffected because it reads the raw attribute and PATCHes it outside the
serializer.

Add an `expiration` transform that passes strings through for the server
to resolve (core-api's ApiCredential::setExpiresAtAttribute owns the
relative-time parsing), serializes real Dates to ISO strings, and
deserializes datetimes like the date transform so the display path
(expiresAt computed, list column) keeps receiving Date instances.

Also make the engine's own test suite runnable, since nothing covered this
path (or ran at all):
- eager-load the engine for its own `ember test` runs (fleetops pattern)
  so the dummy app can resolve engine modules
- add @ember/legacy-built-in-components so ember-engines' LinkToExternal
  has an extensible base class under ember-source 5.4
- add prismjs and skip the ember-prism component config for self-test
  builds, where ember-cli-node-assets registers imports without funneling
  the files and the vendor concat fails
- regression tests: relative strings survive model.serialize(), Dates
  serialize to ISO, datetimes deserialize to Dates for display
@roncodes roncodes mentioned this pull request Aug 28, 2026
3 tasks
@roncodes
roncodes changed the base branch from main to dev-v0.2.15 August 28, 2026 10:31
@roncodes
roncodes merged commit 4749646 into dev-v0.2.15 Aug 29, 2026
6 checks passed
@roncodes
roncodes deleted the fix/api-key-expiration-serialization branch August 29, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant