Checkout: only customer fields on order updates, verified PayPal capture, safe logging #6

Merged
libretech merged 16 commits from fix/paypal-request-logging into main 2026-10-15 10:23:04 +02:00
2 changed files with 27 additions and 21 deletions
Showing only changes of commit b67150376d - Show all commits

No files matched your search

+1 -1
View File
@@ -358,7 +358,7 @@ export default factories.createCoreController("api::order.order", ({ strapi }) =
try {
return await paypalApi.handleWebhook(ctx.request.body);
} catch (error) {
strapi.log.error(`paypal-api: ${JSON.stringify({ error })}`);
strapi.log.error("app:e:order-controller: Handling the PayPal webhook failed", { error: describeError(error) });
return ctx.internalServerError();
}
}
+26 -20
View File
@@ -16,10 +16,12 @@ class PayPalApi {
},
timeout: 0,
environment,
// The SDK logs method, URL and status of each request to the console. Not the bodies: a request body holds the buyer's name
// and address. Not the headers: they hold the token.
logging: {
logLevel: LogLevel.Info,
logRequest: { logBody: true },
logResponse: { logHeaders: true }
logRequest: { logBody: false, logHeaders: false },
logResponse: { logBody: false, logHeaders: false }
}
});
this.ordersController = new OrdersController(this.client);
@@ -29,28 +31,25 @@ class PayPalApi {
* Creates the PayPal order for the order and returns PayPal's id for it, which the capture accepts, and its links. Throws
* CheckoutNotReadyError or ProductUnavailableError before PayPal is asked, for an order PayPal cannot take.
*/
async createSessionOrThrow(returnUrl: string, order: PayPalOrderSource): Promise<{ id: string; links: unknown }> {
async createSessionOrThrow(returnUrl: string, order: PayPalOrderSource & { id?: number | string }): Promise<{ id: string; links: unknown }> {
// Outside the try block: an order that is not ready is the customer's to fix, not a PayPal failure.
const request = buildPayPalOrderRequest(returnUrl, order) as OrderRequest;
try {
const collect = { body: request, prefer: "return=minimal" };
const { body } = await this.ordersController.ordersCreate({ body: request, prefer: "return=minimal" });
const session = JSON.parse(body as string);
if (typeof session?.id !== "string" || session.id === "") throw new Error("PayPal answered without an order id");
strapi.log.silly("app:d:paypal-api: Creating PayPal order " + JSON.stringify({ collect }));
const { body, ...httpResponse } = await this.ordersController.ordersCreate(collect);
const sessionData = JSON.parse(body as string);
strapi.log.info("app:i:paypal-api: " + JSON.stringify({ session: sessionData }));
if (typeof sessionData?.id !== "string" || sessionData.id === "") throw new Error("PayPal answered without an order id");
return {
id: sessionData.id,
links: sessionData.links
};
// The PayPal order's id and status only: the request and PayPal's order hold the buyer's name and address.
strapi.log.info("app:i:paypal-api: Created the PayPal order", {
order: order.id,
paypalOrderId: session.id,
status: typeof session.status === "string" ? session.status : undefined
});
return { id: session.id, links: session.links };
} catch (error) {
// Never the error object: an SDK or axios error carries the request with its Authorization header.
strapi.log.error("app:e:paypal-api: Creating the PayPal order failed", { error: describeError(error) });
strapi.log.error("app:e:paypal-api: Creating the PayPal order failed", { order: order.id, error: describeError(error) });
throw error;
}
}
@@ -71,9 +70,16 @@ class PayPalApi {
return JSON.parse(body as string);
}
async handleWebhook(req: any) {
// Implement PayPal webhook validation and processing
strapi.log.verbose("app:i:paypal-api: " + JSON.stringify({ req }));
async handleWebhook(req: unknown) {
// Implement PayPal webhook validation and processing. The event's resource holds the buyer's name and address, so only the
// event type and the resource's id and status are logged.
const event = (typeof req === "object" && req !== null ? req : {}) as { event_type?: unknown; resource?: { id?: unknown; status?: unknown } };
const textOf = (value: unknown) => (typeof value === "string" ? value : undefined);
strapi.log.verbose("app:v:paypal-api: Webhook received", {
eventType: textOf(event.event_type),
resource: textOf(event.resource?.id),
status: textOf(event.resource?.status)
});
// Note: PayPal webhook handling requires specific implementation
// This is a placeholder and should be replaced with actual PayPal webhook validation