supersam/supabase/functions/get-delivery-invitation/index.ts

191 lines
5.0 KiB
TypeScript

import {
buildPublicOrderGroupInvitationView,
buildPublicInvitationView,
getClientInvitationStateFromOrderGroupStatus,
getClientInvitationStateFromOrderStatus,
hashInvitationToken,
isActiveInvitationState,
isInvitationExpired,
} from "../_shared/delivery-invitations.ts";
import { createServiceClient } from "../_shared/chatbot.ts";
import {
getClientIp,
getCorsHeaders,
hashText,
jsonResponse,
preflightResponse,
readJsonBody,
requireRateLimit,
} from "../_shared/security.ts";
const MAX_BODY_BYTES = 8 * 1024;
type InvitationBody = {
token?: string;
};
const getTokenFromRequest = async (request: Request) => {
if (request.method === "GET") {
return new URL(request.url).searchParams.get("token") || "";
}
const { body } = await readJsonBody<InvitationBody>(request, {
maxBytes: MAX_BODY_BYTES,
});
return String(body.token || "").trim();
};
Deno.serve(async (request) => {
if (request.method === "OPTIONS") {
return preflightResponse(request, "public");
}
if (!["GET", "POST"].includes(request.method)) {
return jsonResponse({ ok: false, error: "Method not allowed" }, 405);
}
const corsHeaders = getCorsHeaders(request, "public");
if (!corsHeaders) {
return jsonResponse({ ok: false, error: "Origin not allowed" }, 403);
}
try {
const token = await getTokenFromRequest(request);
if (!token) {
return jsonResponse({ ok: false, error: "token is required" }, 400, corsHeaders);
}
const tokenHash = await hashInvitationToken(token);
const supabase = createServiceClient();
const ipHash = await hashText(getClientIp(request));
await requireRateLimit(supabase, {
scope: "invitation-get",
key: `${ipHash}:${tokenHash.slice(0, 16)}`,
maxCount: 30,
windowSeconds: 600,
});
const { data: invitation, error: invitationError } = await supabase
.from("delivery_invitations")
.select("*")
.eq("token_hash", tokenHash)
.single();
if (invitationError) {
if (invitationError.code === "PGRST116") {
return jsonResponse({ ok: false, error: "Invitation not found" }, 404, corsHeaders);
}
throw invitationError;
}
if (isInvitationExpired(invitation)) {
return jsonResponse({ ok: false, error: "Invitation expired" }, 410, corsHeaders);
}
if (invitation.order_group_id) {
const { data: group, error: groupError } = await supabase
.from("order_groups")
.select("*")
.eq("id", invitation.order_group_id)
.single();
if (groupError) {
throw groupError;
}
const publicState = getClientInvitationStateFromOrderGroupStatus(
group.delivery_status,
invitation.state,
);
await supabase
.from("delivery_invitations")
.update({
opened_at: isActiveInvitationState(publicState) && !invitation.opened_at
? new Date().toISOString()
: invitation.opened_at,
access_count: (invitation.access_count || 0) + 1,
last_accessed_at: new Date().toISOString(),
})
.eq("id", invitation.id);
const invitationView = buildPublicOrderGroupInvitationView(invitation, group);
return jsonResponse(
{
ok: true,
invitation: {
...invitationView,
token,
state: publicState,
},
},
200,
corsHeaders,
);
}
const { data: order, error: orderError } = await supabase
.from("orders")
.select("id, order_number, status, delivery_agreement_status, customer")
.eq("id", invitation.order_id)
.single();
if (orderError) {
throw orderError;
}
const publicState = getClientInvitationStateFromOrderStatus(order.status);
if (isActiveInvitationState(publicState) && !invitation.opened_at) {
await supabase
.from("delivery_invitations")
.update({
opened_at: new Date().toISOString(),
access_count: (invitation.access_count || 0) + 1,
last_accessed_at: new Date().toISOString(),
})
.eq("id", invitation.id);
} else {
await supabase
.from("delivery_invitations")
.update({
access_count: (invitation.access_count || 0) + 1,
last_accessed_at: new Date().toISOString(),
})
.eq("id", invitation.id);
}
const invitationView = buildPublicInvitationView(invitation, order);
return jsonResponse(
{
ok: true,
invitation: {
...invitationView,
token,
state: publicState,
},
},
200,
corsHeaders,
);
} catch (error) {
if (error instanceof Error && "status" in error) {
const httpError = error as { status: number; message: string };
return jsonResponse({ ok: false, error: httpError.message }, httpError.status, corsHeaders);
}
return jsonResponse(
{
ok: false,
error: error instanceof Error ? error.message : "Unexpected error",
},
500,
corsHeaders,
);
}
});