CDX-12: console: support expirations on app passwords - #38444
Conversation
Frontegg's user and tenant API token endpoints accept `expiresInMinutes` on create and return `expires` on list, but the console never used either, so every app password was immortal and the list gave no signal about staleness. Add an Expiration select to the new app password modal (30/60/90 days or no expiration, defaulting to 90 days) and an Expires column to the list that renders "Never" for passwords without an expiration and flags expired and soon to expire ones with a status pill. CDX-12
Drop the redundant aria-label on the expiration select so the label/id wiring is what tests exercise, let StatusPill derive its own text, and cover the service password path in the test. CDX-12
The Expires column left the verbose date format wrapping to three lines in both date columns. Day granularity is enough for a credential's lifecycle, so drop the time and timezone from Created at and Expires, and let the expiry pill wrap below its date rather than squeeze it. CDX-12
The button opened the connect drawer scoped to one password's user, which the sidebar's Connect entry already covers. Removing it leaves the row actions to Delete alone and gives the new Expires column room. CDX-12
QA LLM Review1. MEDIUM -- Removing Connect drops the service-account context
The row action now only renders Delete, but the removed Connect button was the only path that passed a service app password's SQL user into the connection drawer. The remaining sidebar drawer is always scoped to the signed-in Frontegg user, so its generated SQL, external-tool, and MCP token instructions use the personal email instead of the service user and do not work with that service password. Details
|
leedqin
left a comment
There was a problem hiding this comment.
Looks good! some minor comments and nits
|
|
||
| type ExpiresInOption = keyof typeof EXPIRES_IN_OPTIONS; | ||
|
|
||
| const DEFAULT_EXPIRES_IN: ExpiresInOption = "90d"; |
There was a problem hiding this comment.
Will this also apply to Service accounts as well?
There was a problem hiding this comment.
Yes, but it's really just the default for the dropdown
| ); | ||
| }; | ||
|
|
||
| const ConnectAppPasswordButton = ({ userStr }: { userStr: string }) => { |
There was a problem hiding this comment.
Since we are removing this, can we move the service account password creation to the new connect modal:
https://linear.app/materializeinc/issue/CNS-146/add-service-account-password-creation-in-connect-modal
There was a problem hiding this comment.
I think that makes sense, but let's not do that in this PR.
| const { colors } = useTheme<MaterializeTheme>(); | ||
|
|
||
| if (!expires) { | ||
| return <Text color={colors.gray["500"]}>Never</Text>; |
There was a problem hiding this comment.
nit: Prefer colors.foreground.secondary over colors.gray["500"]
There was a problem hiding this comment.
Done in ecdf5cf. Also switched the two pre-existing gray["500"] usages in this table (the muted user and roles cells) for consistency: in the light theme foreground.secondary is gray[500], but in dark it resolves to gray[400], so leaving them mixed would have rendered the muted text in a single row as two different shades in dark mode.
| <FormLabel htmlFor="expiresIn" fontSize="sm"> | ||
| Expiration | ||
| </FormLabel> | ||
| <Select {...register("expiresIn")} id="expiresIn" size="sm"> |
There was a problem hiding this comment.
Nit: Prefer to use <SimpleSelect /> console's component
There was a problem hiding this comment.
Done in ecdf5cf. Passed width="100%" to match the in-modal precedent in AlterClusterModal/NewClusterForm, since SimpleSelect defaults to width="auto" and the field sits under a full-width Name input.
Use the console's SimpleSelect for the expiration control, and the semantic foreground.secondary token for muted table text. The latter resolves to gray[400] in the dark theme, so the raw gray[500] it replaces was wrong there. CDX-12


CDX-12
Problem
Console app passwords (Frontegg user/tenant API tokens) were immortal: the create flow only took a description, and the list gave no signal about which credentials were stale. That blocks customers with credential-rotation requirements and makes cleanup guesswork.
Solution
Frontegg already supports this end to end, so no backend work was needed. Confirmed against
frontegg/openapi-public/identity.json:POST .../{users,tenants}/api-tokens/v1acceptexpiresInMinutes(omitted = never expires).GETlist responses returnexpires(date-time). Note the field isexpires, notexpiresAt.lastUsed/lastSeenfield anywhere in the identity spec, so "show last use" is not buildable from Frontegg. Follow-up write-up is on the Linear issue.Changes:
expiresInMinutesthroughcreateUserApiToken/createTenantApiTokenanduseCreateApiToken, and typeexpireson the token interfaces.DATE_FORMAT). The seventh column leftMMM. dd, yyyy HH:mm zwrapping to three lines in both Created at and Expires; time-of-day is not useful for a credential's lifecycle.Testing
New
src/access/AppPasswordsPage.test.tsxcovers a legacy token with noexpiresrendering "Never", the expired and expiring-soon pills, the default create postingexpiresInMinutes: 129600, and "No expiration" posting noexpiresInMinutes.yarn lint,yarn typecheck, and the console suite pass locally;console-e2e-test/console-e2e-test-prodexercise the real-region path.Reviewer notes
connectComponents,ConnectMcpPanel,MzCliAppPasswordPage) are deliberately unchanged. The param is optional, so those keep creating non-expiring passwords. Adding an expiration control there is a follow-up.src/frontegg-auth'sactive_sessionscache serves repeat authentications without calling Frontegg and refreshes at ~0.8x JWT lifetime, so an already-authenticated app password can keep working for up to one refresh period past its expiry. This is the same bounded window that already exists when a password is deleted, not a new regression.src/frontegg-mockmodels no expiry (it accepts onlydescription). Console e2e hits real staging Frontegg so the mock is not on this path; teaching itexpiresInMinutes/expiresis a cheap follow-up if a Rust integration test ever needs it.🤖 Generated with Claude Code