chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
const body = parseResult.data;
|
||||
|
||||
const organization = await prisma.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: { maxSessionDuration: body.maxSessionDuration },
|
||||
select: { id: true, slug: true, maxSessionDuration: true },
|
||||
});
|
||||
|
||||
// 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})
|
||||
`;
|
||||
}
|
||||
|
||||
return json({ success: true, organization });
|
||||
}
|
||||
Reference in New Issue
Block a user