449edd6c68
Jenkins Production Deployment
Reviewed-on: #9 Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech> Co-committed-by: Patrick Müller <patrick@mueller-patrick.tech>
67 lines
2.9 KiB
TypeScript
67 lines
2.9 KiB
TypeScript
import axios from 'axios';
|
|
|
|
// Shared server-to-server access to the one Nachklang Salesforce org. Both the
|
|
// newsletter-signup sync (feedback module) and the transactional-email relay
|
|
// (common.mail) authenticate the same way - OAuth 2.0 client credentials
|
|
// against the nk_Nachklang_API_Integration external client app - so the token
|
|
// cache and the retry-once-on-401 live here rather than being duplicated.
|
|
//
|
|
// Salesforce's client-credentials token response does not reliably include
|
|
// expires_in, so the cache lifetime is a conservative guess rather than a
|
|
// value read from the response - a 401 on the next call just triggers a fresh
|
|
// fetch (see salesforceApexRestPost).
|
|
|
|
const TOKEN_CACHE_MS = 15 * 60 * 1000;
|
|
let cachedToken: {accessToken: string; fetchedAt: number} | null = null;
|
|
|
|
export const salesforceEnabled = (): boolean => process.env.SALESFORCE_ENABLED === 'true';
|
|
|
|
const readConfig = (): {instanceUrl: string; clientId: string; clientSecret: string} => {
|
|
const instanceUrl = process.env.SALESFORCE_API_URL;
|
|
const clientId = process.env.SALESFORCE_CLIENT_ID;
|
|
const clientSecret = process.env.SALESFORCE_CLIENT_SECRET;
|
|
if (!instanceUrl || !clientId || !clientSecret) {
|
|
throw new Error('SALESFORCE_ENABLED is true but SALESFORCE_API_URL/SALESFORCE_CLIENT_ID/SALESFORCE_CLIENT_SECRET are not fully configured.');
|
|
}
|
|
return {instanceUrl, clientId, clientSecret};
|
|
};
|
|
|
|
const getAccessToken = async (forceRefresh: boolean): Promise<string> => {
|
|
if (!forceRefresh && cachedToken && Date.now() - cachedToken.fetchedAt < TOKEN_CACHE_MS) {
|
|
return cachedToken.accessToken;
|
|
}
|
|
|
|
const {instanceUrl, clientId, clientSecret} = readConfig();
|
|
const res = await axios.post(
|
|
`${instanceUrl}/services/oauth2/token`,
|
|
new URLSearchParams({grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret}).toString(),
|
|
{headers: {'Content-Type': 'application/x-www-form-urlencoded'}, timeout: 10000}
|
|
);
|
|
cachedToken = {accessToken: res.data.access_token, fetchedAt: Date.now()};
|
|
return cachedToken.accessToken;
|
|
};
|
|
|
|
/**
|
|
* POSTs a JSON body to an Apex REST path (e.g. '/services/apexrest/newsletter/signup')
|
|
* and returns the parsed response body. Retries once with a forced token
|
|
* refresh on a 401 - the server-side token may have expired even though our
|
|
* conservative local TTL has not. All other errors propagate to the caller.
|
|
*/
|
|
export const salesforceApexRestPost = async <T>(path: string, body: unknown): Promise<T> => {
|
|
const {instanceUrl} = readConfig();
|
|
const url = `${instanceUrl}${path}`;
|
|
|
|
try {
|
|
const token = await getAccessToken(false);
|
|
const res = await axios.post<T>(url, body, {headers: {Authorization: `Bearer ${token}`}, timeout: 10000});
|
|
return res.data;
|
|
} catch (err: any) {
|
|
if (err?.response?.status === 401) {
|
|
const token = await getAccessToken(true);
|
|
const res = await axios.post<T>(url, body, {headers: {Authorization: `Bearer ${token}`}, timeout: 10000});
|
|
return res.data;
|
|
}
|
|
throw err;
|
|
}
|
|
};
|