80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
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 });
|
|
}
|