93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import { parseWithZod } from "@conform-to/zod";
|
|
import { type ActionFunction, json } from "@remix-run/node";
|
|
import { z } from "zod";
|
|
import { $replica, prisma } from "~/db.server";
|
|
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
|
import { resolveSessionByIdOrExternalId } from "~/services/realtime/sessions.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { requireUserId } from "~/services/session.server";
|
|
|
|
export const closeSessionSchema = z.object({
|
|
redirectUrl: z.string(),
|
|
environmentId: z.string(),
|
|
reason: z.string().optional(),
|
|
});
|
|
|
|
const ParamSchema = z.object({
|
|
sessionParam: z.string(),
|
|
});
|
|
|
|
export const action: ActionFunction = async ({ request, params }) => {
|
|
const userId = await requireUserId(request);
|
|
const { sessionParam } = ParamSchema.parse(params);
|
|
|
|
const formData = await request.formData();
|
|
const submission = parseWithZod(formData, { schema: closeSessionSchema });
|
|
|
|
if (submission.status !== "success") {
|
|
return json(submission.reply());
|
|
}
|
|
|
|
const { redirectUrl, environmentId, reason } = submission.value;
|
|
const trimmedReason = reason?.trim();
|
|
const closedReason =
|
|
trimmedReason && trimmedReason.length > 0 ? trimmedReason : "closed-from-dashboard";
|
|
|
|
try {
|
|
// Confirm the user belongs to the org that owns this environment, then
|
|
// resolve the session by friendlyId or externalId scoped to that env.
|
|
const environment = await $replica.runtimeEnvironment.findFirst({
|
|
where: {
|
|
id: environmentId,
|
|
organization: { members: { some: { userId } } },
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!environment) {
|
|
return json(submission.reply({ fieldErrors: { environmentId: ["Environment not found"] } }));
|
|
}
|
|
|
|
const session = await resolveSessionByIdOrExternalId($replica, environment.id, sessionParam);
|
|
|
|
if (!session) {
|
|
return json(submission.reply({ fieldErrors: { sessionParam: ["Session not found"] } }));
|
|
}
|
|
|
|
if (session.closedAt) {
|
|
// Already closed — no-op, but redirect with a friendly message so the
|
|
// UI doesn't look like it did nothing.
|
|
return redirectWithSuccessMessage(redirectUrl, request, `Session already closed`);
|
|
}
|
|
|
|
// Conditional update mirrors the public API: two concurrent closes race
|
|
// through the read but only one wins this update.
|
|
await prisma.session.updateMany({
|
|
where: { id: session.id, closedAt: null },
|
|
data: {
|
|
closedAt: new Date(),
|
|
closedReason,
|
|
},
|
|
});
|
|
|
|
return redirectWithSuccessMessage(redirectUrl, request, `Closed session`);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
logger.error("Failed to close session", {
|
|
error: { name: error.name, message: error.message, stack: error.stack },
|
|
});
|
|
return redirectWithErrorMessage(
|
|
redirectUrl,
|
|
request,
|
|
`Failed to close session, ${error.message}`
|
|
);
|
|
}
|
|
logger.error("Failed to close session", { error });
|
|
return redirectWithErrorMessage(
|
|
redirectUrl,
|
|
request,
|
|
`Failed to close session, ${JSON.stringify(error)}`
|
|
);
|
|
}
|
|
};
|