73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
/**
|
|
* SMS notification service using Twilio.
|
|
*
|
|
* Reads TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER from env.
|
|
* If any of those are missing, sendSms is a no-op that returns
|
|
* { success: false } and logs a warning. This keeps the app running in
|
|
* environments where Twilio has not been provisioned yet.
|
|
*/
|
|
|
|
import twilio, { type Twilio } from "twilio";
|
|
import { childLogger } from "../_core/logger.js";
|
|
|
|
const log = childLogger("sms");
|
|
|
|
let cachedClient: Twilio | null = null;
|
|
|
|
export function isSmsConfigured(): boolean {
|
|
return Boolean(
|
|
process.env.TWILIO_ACCOUNT_SID &&
|
|
process.env.TWILIO_AUTH_TOKEN &&
|
|
process.env.TWILIO_PHONE_NUMBER
|
|
);
|
|
}
|
|
|
|
function getClient(): Twilio | null {
|
|
if (cachedClient) return cachedClient;
|
|
if (!isSmsConfigured()) return null;
|
|
try {
|
|
cachedClient = twilio(
|
|
process.env.TWILIO_ACCOUNT_SID!,
|
|
process.env.TWILIO_AUTH_TOKEN!
|
|
);
|
|
return cachedClient;
|
|
} catch (err) {
|
|
log.error({ err }, "failed to initialise Twilio client");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function normalizePhone(input: string): string {
|
|
const trimmed = input.trim();
|
|
if (trimmed.startsWith("+")) return trimmed;
|
|
const digits = trimmed.replace(/[^0-9]/g, "");
|
|
return `+${digits}`;
|
|
}
|
|
|
|
export async function sendSms(
|
|
to: string,
|
|
body: string
|
|
): Promise<{ success: boolean; messageId?: string; error?: string }> {
|
|
const client = getClient();
|
|
if (!client) {
|
|
log.warn({ to: to.slice(0, 4) + "***" }, "Twilio not configured — SMS skipped");
|
|
return { success: false, error: "Twilio non configuré" };
|
|
}
|
|
|
|
const from = process.env.TWILIO_PHONE_NUMBER!;
|
|
const normalized = normalizePhone(to);
|
|
|
|
try {
|
|
const msg = await client.messages.create({
|
|
to: normalized,
|
|
from,
|
|
body,
|
|
});
|
|
log.info({ messageId: msg.sid, to: normalized.slice(0, 4) + "***" }, "SMS sent");
|
|
return { success: true, messageId: msg.sid };
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
log.error({ err, to: normalized.slice(0, 4) + "***" }, "SMS send failed");
|
|
return { success: false, error: errorMessage };
|
|
}
|
|
}
|