Skip to content

Make invite token expiration configurable#3094

Merged
gantoine merged 4 commits intomasterfrom
copilot/feature-configurable-invite-token-expiration
Mar 9, 2026
Merged

Make invite token expiration configurable#3094
gantoine merged 4 commits intomasterfrom
copilot/feature-configurable-invite-token-expiration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

Invite link tokens had a hardcoded 10-minute expiration, making them impractical to share asynchronously (SMS, Signal, etc.).

Changes

Backend

  • New env var INVITE_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)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Make invite token expiration configurable Make invite token expiration configurable Mar 9, 2026
@gantoine gantoine marked this pull request as ready for review March 9, 2026 02:53
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
13812 9263 67% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
backend/config/_init_.py 99% 🟢
backend/endpoints/user.py 58% 🟢
backend/handler/auth/base_handler.py 66% 🟢
TOTAL 74% 🟢

updated for commit: e2ece6b by action🐍

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

Test Results (postgresql)

945 tests  ±0   944 ✅ ±0   2m 16s ⏱️ -3s
  1 suites ±0     1 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit e2ece6b. ± Comparison against base commit 4d54673.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

Test Results (mariadb)

945 tests  ±0   944 ✅ ±0   2m 16s ⏱️ -12s
  1 suites ±0     1 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit e2ece6b. ± Comparison against base commit 4d54673.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 9, 2026

Greptile Summary

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.pygenerate_invite_link_token() now accepts expiration (seconds); JWT exp and Redis TTL are both derived from the same value
  • backend/endpoints/user.pycreate_invite_link accepts expiration: int | None; validates it is positive when provided
  • frontend/src/services/api/user.tscreateInviteLink() conditionally appends expiration to query params
  • frontend/.../InviteLink.vue — expiration v-select dropdown added alongside the role toggle

Issues found:

  1. 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.
  2. 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.
  3. 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.
frontend/src/components/Settings/Administration/Users/Dialog/InviteLink.vue 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)

  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).

    Consider resetting state on close:

Last reviewed commit: e2ece6b

@gantoine gantoine merged commit b7d6edc into master Mar 9, 2026
12 checks passed
@gantoine gantoine deleted the copilot/feature-configurable-invite-token-expiration branch March 9, 2026 03:01
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.

[Feature] Make invite token expiration configurable

2 participants