-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(webapp): app auto session logout #3473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
2ee9047
Implements new session logout
samejr 0783ff7
Submit the form without a save button
samejr 62fecbc
Better settings layout
samejr 6082525
Show friendly labels for capped duration values
samejr 4237c5d
Bug fix: When getting logged out, logging back in redirected you to /…
samejr fa06144
Fixes bug where session logout returned error.
samejr 31595ee
Remove login page toast message logic
samejr d7d6b2b
Cloudwatch picks up session logout events
samejr dffeec0
small classname tweak
samejr c294c50
Aggregate the session length values
samejr 05095c5
Merge the 2 db migrations
samejr abd7562
userId param is no longer used
samejr 9234800
Fix for ensuring a DB change updates the availble duration options
samejr bad8895
Code review fixes and improvements
samejr 309742e
Code review improvements
samejr 7ca42aa
Merge branch 'main' into feat(webapp)-auto-app-logout
samejr 44d55cd
Code review fix
samejr 2e0d248
code comment update
samejr b2ca91e
Merge branch 'main' into feat(webapp)-auto-app-logout
samejr bca5ec3
Merge branch 'main' into feat(webapp)-auto-app-logout
samejr 4fa011d
perf(webapp): make auto-logout enforcement zero-query on the read path
matt-aitken 411ad27
fix(webapp): restore admin verification on impersonation in getUserId
matt-aitken 723b13f
fix(webapp): prevent /logout redirect loop on auto-logout
matt-aitken b41e964
fix(webapp): return 400 (not 500) on malformed JSON in admin session-…
matt-aitken cbe150e
Mock the db server in the new tests
matt-aitken 9190d1f
fix(webapp): align malformed-JSON error shape with Zod flatten() shape
matt-aitken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: feature | ||
| --- | ||
|
|
||
| App auto session logout. Users can configure their own session duration; org admins can set a `maxSessionDuration` cap that takes the tightest value across an account's orgs. Sessions exceeding their effective duration are redirected to `/logout` with a HIPAA audit trail emitted to CloudWatch (`event: session.auto_logout`). Enforcement reads `User.nextSessionEnd` — written at login and bulk-updated when admins change the cap — so the auth path adds no per-request DB queries. |
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
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
79 changes: 79 additions & 0 deletions
79
apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.session-duration.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; | ||
| import { z } from "zod"; | ||
| import { prisma } from "~/db.server"; | ||
| import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; | ||
| import { | ||
| ALLOWED_SESSION_DURATION_VALUES, | ||
| isAllowedSessionDuration, | ||
| } from "~/services/sessionDuration.server"; | ||
|
|
||
| const ParamsSchema = z.object({ | ||
| organizationId: z.string(), | ||
| }); | ||
|
|
||
| const RequestBodySchema = z.object({ | ||
| /** | ||
| * Maximum session lifetime (seconds) for members of this organization, or | ||
| * null to remove the cap. When set, this caps each member's | ||
| * `User.sessionDuration` and is enforced on the user's next request. | ||
| * | ||
| * Must be one of the values in `SESSION_DURATION_OPTIONS` so the cap always | ||
| * maps to a labeled dropdown option for users — otherwise users see fallback | ||
| * labels like "7200 seconds" in the UI. To allow a new value, add it to | ||
| * `SESSION_DURATION_OPTIONS`. | ||
| */ | ||
| maxSessionDuration: z | ||
| .number() | ||
| .int() | ||
| .positive() | ||
| .nullable() | ||
| .refine((v) => v === null || isAllowedSessionDuration(v), { | ||
| message: `maxSessionDuration must be one of: ${[...ALLOWED_SESSION_DURATION_VALUES] | ||
| .sort((a, b) => a - b) | ||
| .join(", ")}`, | ||
| }), | ||
| }); | ||
|
|
||
| export async function action({ request, params }: ActionFunctionArgs) { | ||
| await requireAdminApiRequest(request); | ||
|
|
||
| const { organizationId } = ParamsSchema.parse(params); | ||
| let rawBody: unknown; | ||
| try { | ||
| rawBody = await request.json(); | ||
| } catch { | ||
| return json( | ||
| { success: false, errors: { formErrors: ["Invalid JSON body"], fieldErrors: {} } }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
| const parseResult = RequestBodySchema.safeParse(rawBody); | ||
| if (!parseResult.success) { | ||
| return json({ success: false, errors: parseResult.error.flatten() }, { status: 400 }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| const body = parseResult.data; | ||
|
|
||
| const organization = await prisma.organization.update({ | ||
| where: { id: organizationId }, | ||
| data: { maxSessionDuration: body.maxSessionDuration }, | ||
| select: { id: true, slug: true, maxSessionDuration: true }, | ||
| }); | ||
|
samejr marked this conversation as resolved.
|
||
|
|
||
| // Propagate the new cap to currently-logged-in members by shortening their | ||
| // `nextSessionEnd`. We only ever shorten (`LEAST`): raising or removing the | ||
| // cap leaves existing sessions alone — the larger window applies on next | ||
| // login. If a member is in another org with a tighter cap that other cap | ||
| // remains in effect via their existing `nextSessionEnd` (LEAST keeps it). | ||
| if (body.maxSessionDuration !== null) { | ||
| await prisma.$executeRaw` | ||
| UPDATE "User" | ||
| SET "nextSessionEnd" = LEAST( | ||
| COALESCE("nextSessionEnd", 'infinity'::timestamp), | ||
| NOW() + (LEAST("sessionDuration", ${body.maxSessionDuration}) * INTERVAL '1 second') | ||
| ) | ||
| WHERE "id" IN (SELECT "userId" FROM "OrgMember" WHERE "organizationId" = ${organizationId}) | ||
| `; | ||
|
matt-aitken marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return json({ success: true, organization }); | ||
| } | ||
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
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
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.