You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Invite link tokens had a hardcoded 10-minute expiration, making them impractical to share asynchronously (SMS, Signal, etc.).
Changes
Backend
New env varINVITE_TOKEN_EXPIRY_SECONDS (default: 10 * 60 = 600 seconds) — sets the server-wide default expiration for invite tokens
generate_invite_link_token() — accepts optional expiration_seconds param; falls back to env var if not provided
POST /users/invite-link — accepts optional expiration_seconds query param with validation (must be positive integer)
Frontend
createInviteLink() — passes optional expirationSeconds to the API
Invite Link dialog — adds an "Expires in" dropdown with preset options (1h, 6h, 12h, 1 day, 3 days, 7 days, 30 days); defaults to 1 day
![Invite dialog with expiration selector showing dropdown options]
POST /users/invite-link?role=viewer&expiration_seconds=604800
Original prompt
This section details on the original issue you should resolve
<issue_title>[Feature] Make invite token expiration configurable</issue_title>
<issue_description>Is your feature request related to a problem? Please describe.
It appears the invite expiration time is hard coded to 10 minutes. self.invite_link_token_expires_in_minutes = 10 in backend/handler/auth/base_handler.py, which means invites essentially need to be handled in roughly real time (can't send to someone in sms/signal/etc. and then let them get to it at their leisure).
Describe the solution you'd like
Best case would be a configuration value in the invite UI dialog, but having something like a configurable environment variable would be a great option as well.
Describe alternatives you've considered
I've been reaching out to people and making sure they're active as I message them
Additional context
This product is awesome, and I appreciate everything you've done!
</issue_description>
Comments on the Issue (you are @copilot in this section)
This PR makes invite token expiration configurable, replacing a hardcoded 10-minute timeout with an INVITE_TOKEN_EXPIRY_SECONDS environment variable (default 600 s) and an optional expiration query parameter on POST /users/invite-link. The frontend dialog gains an "Expires in" dropdown with seven presets (1 h → 30 days), defaulting to 1 day.
Key changes:
backend/config/__init__.py — new INVITE_TOKEN_EXPIRY_SECONDS config constant via safe_int
backend/handler/auth/base_handler.py — generate_invite_link_token() now accepts expiration (seconds); JWT exp and Redis TTL are both derived from the same value
backend/endpoints/user.py — create_invite_link accepts expiration: int | None; validates it is positive when provided
frontend/src/services/api/user.ts — createInviteLink() conditionally appends expiration to query params
frontend/.../InviteLink.vue — expiration v-select dropdown added alongside the role toggle
Issues found:
The expiration query-param name is inconsistent with the INVITE_TOKEN_EXPIRY_SECONDS env-var name (and with the expiration_seconds example shown in the PR description). Renaming the param to expiration_seconds would improve discoverability for API consumers.
Dialog state (selectedRole, selectedExpiration, fullInviteLink) is not reset when the dialog is closed, so a stale link from a previous session remains visible when the dialog is reopened.
INVITE_TOKEN_EXPIRY_SECONDS is read with safe_int but never validated to be positive. Setting it to 0 or a negative value would cause Redis SETEX to raise a ResponseError and break all invite-link creation. The same > 0 guard applied to the query param should be applied to the env var at startup.
Confidence Score: 2/5
Not safe to merge: missing critical positivity validation on env var can cause production crashes if misconfigured.
The core feature logic is sound (base_handler.py correctly manages JWT expiry and Redis TTL), but the env var validation is incomplete. A zero or negative INVITE_TOKEN_EXPIRY_SECONDS will silently accept the invalid value and crash all invite-link creation at runtime when Redis SETEX rejects the non-positive TTL. This is a critical production issue. Additionally, there are two secondary concerns: API parameter naming is inconsistent with the env var (confusing for API consumers), and the invite dialog state is not reset on close (UX confusion). These should all be fixed before merge.
backend/config/init.py (critical: add positivity validation for INVITE_TOKEN_EXPIRY_SECONDS), backend/endpoints/user.py (rename expiration param to expiration_seconds for consistency), frontend/src/components/Settings/Administration/Users/Dialog/InviteLink.vue (reset dialog state on close)
Important Files Changed
Filename
Overview
backend/config/init.py
Adds INVITE_TOKEN_EXPIRY_SECONDS env var with a default of 600s. Missing validation that the value is positive — a zero or negative value would propagate to Redis setex and cause a runtime error that crashes invite-link creation.
backend/endpoints/user.py
Adds optional expiration query param to the invite-link endpoint with correct ≤0 validation. Naming is inconsistent with the env var (expiration vs. expiration_seconds), which could confuse API consumers.
backend/handler/auth/base_handler.py
Migrates expiry from a hard-coded minutes field to a seconds-based parameter; JWT payload and Redis TTL are both updated correctly.
Adds an "Expires in" v-select with seven preset durations; state (selectedRole, selectedExpiration, fullInviteLink) is not reset when the dialog is closed, leaving stale data visible on reopen.
frontend/src/services/api/user.ts
createInviteLink now accepts an optional expiration param and conditionally includes it in query params; clean and correct.
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Admin opens Invite dialog] --> B[Select role and expiration]
B --> C[Click Generate]
C --> D{expiration provided?}
D -- Yes --> E[Use provided expiration seconds]
D -- No --> F[Use INVITE_TOKEN_EXPIRY_SECONDS env var]
E --> G{expiration > 0?}
F --> H[Build JWT payload with exp = now + expires_in]
G -- No --> I[HTTP 400 Bad Request]
G -- Yes --> H
H --> J[Sign JWT]
J --> K[Store in Redis with matching TTL]
K --> L[Return InviteLinkSchema to frontend]
L --> M[Display full invite URL]
Loading
Comments Outside Diff (1)
frontend/src/components/Settings/Administration/Users/Dialog/InviteLink.vue, line 57-59 (link)
Dialog state not reset on close.
closeDialog only hides the dialog but leaves selectedRole, selectedExpiration, and fullInviteLink at their last-used values. When the dialog is reopened, the user sees the previously generated link alongside the previous role/expiration selections, which could be confusing (they might not notice the stale link and copy it instead of generating a new one).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Invite link tokens had a hardcoded 10-minute expiration, making them impractical to share asynchronously (SMS, Signal, etc.).
Changes
Backend
INVITE_TOKEN_EXPIRY_SECONDS(default:10 * 60= 600 seconds) — sets the server-wide default expiration for invite tokensgenerate_invite_link_token()— accepts optionalexpiration_secondsparam; falls back to env var if not providedPOST /users/invite-link— accepts optionalexpiration_secondsquery param with validation (must be positive integer)Frontend
createInviteLink()— passes optionalexpirationSecondsto the API![Invite dialog with expiration selector showing dropdown options]
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.