191 lines
5.0 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
});
|