chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { useSubmit } from "@remix-run/react";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import type { SessionDurationOption } from "~/services/sessionDuration.server";
|
||||
|
||||
interface SessionDurationSettingProps {
|
||||
currentValue: number;
|
||||
options: SessionDurationOption[];
|
||||
orgCapSeconds: number | null;
|
||||
}
|
||||
|
||||
export function SessionDurationSetting({
|
||||
currentValue,
|
||||
options,
|
||||
orgCapSeconds,
|
||||
}: SessionDurationSettingProps) {
|
||||
const submit = useSubmit();
|
||||
|
||||
const orgCapOption =
|
||||
orgCapSeconds === null ? null : options.find((o) => o.value === orgCapSeconds);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
<InputGroup className="flex-1">
|
||||
<Label>Automatic logout</Label>
|
||||
<Paragraph variant="small">
|
||||
Automatically log out after a period of time.
|
||||
{orgCapSeconds !== null ? (
|
||||
<>
|
||||
{" "}
|
||||
Your organization caps this at {orgCapOption?.label ?? `${orgCapSeconds} seconds`}.
|
||||
</>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</InputGroup>
|
||||
<div className="flex flex-none items-center">
|
||||
<Select
|
||||
name="sessionDuration"
|
||||
variant="secondary/small"
|
||||
defaultValue={String(currentValue)}
|
||||
setValue={(value) => {
|
||||
const next = Array.isArray(value) ? value[0] : value;
|
||||
if (typeof next !== "string" || next === String(currentValue)) return;
|
||||
submit(
|
||||
{ sessionDuration: next },
|
||||
{ method: "post", action: "/resources/account/session-duration" }
|
||||
);
|
||||
}}
|
||||
text={(value: string) =>
|
||||
options.find((o) => String(o.value) === value)?.label ?? "Select a duration"
|
||||
}
|
||||
dropdownIcon
|
||||
>
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={String(option.value)}
|
||||
className="text-text-bright"
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { redirect, type ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
commitSession as commitMessageSession,
|
||||
getSession as getMessageSession,
|
||||
setErrorMessage,
|
||||
setSuccessMessage,
|
||||
} from "~/models/message.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import {
|
||||
commitAuthenticatedSession,
|
||||
getAllowedSessionOptions,
|
||||
getEffectiveSessionDuration,
|
||||
isAllowedSessionDuration,
|
||||
} from "~/services/sessionDuration.server";
|
||||
import { getUserSession } from "~/services/sessionStorage.server";
|
||||
|
||||
const FormSchema = z.object({
|
||||
sessionDuration: z.coerce.number().int().positive(),
|
||||
});
|
||||
|
||||
const REDIRECT_PATH = "/account/security";
|
||||
|
||||
async function redirectWithError(request: Request, message: string) {
|
||||
const messageSession = await getMessageSession(request.headers.get("cookie"));
|
||||
setErrorMessage(messageSession, message);
|
||||
return redirect(REDIRECT_PATH, {
|
||||
headers: { "Set-Cookie": await commitMessageSession(messageSession) },
|
||||
});
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const parsed = FormSchema.safeParse(Object.fromEntries(formData));
|
||||
|
||||
if (!parsed.success) {
|
||||
return redirectWithError(request, "Invalid session duration value.");
|
||||
}
|
||||
|
||||
const { sessionDuration } = parsed.data;
|
||||
|
||||
if (!isAllowedSessionDuration(sessionDuration)) {
|
||||
return redirectWithError(request, "Invalid session duration value.");
|
||||
}
|
||||
|
||||
const { orgCapSeconds, durationSeconds } = await getEffectiveSessionDuration(userId);
|
||||
const allowed = getAllowedSessionOptions(orgCapSeconds, durationSeconds);
|
||||
if (!allowed.some((o) => o.value === sessionDuration)) {
|
||||
return redirectWithError(
|
||||
request,
|
||||
"Your organization's policy does not allow that session duration."
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { sessionDuration },
|
||||
});
|
||||
|
||||
// Re-issue the cookie with the new maxAge and reset issuedAt so the user
|
||||
// gets a fresh window matching their new selection right away. This also
|
||||
// restamps `User.nextSessionEnd` against the new effective duration.
|
||||
const authSession = await getUserSession(request);
|
||||
const authCookie = await commitAuthenticatedSession(authSession, userId);
|
||||
|
||||
const messageSession = await getMessageSession(request.headers.get("cookie"));
|
||||
setSuccessMessage(messageSession, "Session duration updated.");
|
||||
const messageCookie = await commitMessageSession(messageSession);
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append("Set-Cookie", authCookie);
|
||||
headers.append("Set-Cookie", messageCookie);
|
||||
|
||||
return redirect(REDIRECT_PATH, { headers });
|
||||
}
|
||||
Reference in New Issue
Block a user