From 3fff09ba81d28ee670fdc15f047b2477c1f2659e Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Mon, 20 Jul 2026 15:23:16 +0200 Subject: [PATCH] feat: new secrets --- .gitignore | 2 + functions/config/keys.js | 10 ----- functions/config/secrets.js | 15 ++++++++ functions/helpers/gemini.js | 8 ++-- functions/helpers/stripe.js | 26 ++++--------- functions/serviceAccountKey.json | 13 ------- functions/src/cover.js | 2 + functions/src/lyrics.js | 8 ++-- functions/src/music.js | 10 ++--- functions/src/notifications.js | 49 ++++++++++++++---------- functions/src/project.js | 8 +++- functions/src/stripe.js | 12 +++--- functions/src/subscription/catalog.js | 6 ++- functions/src/subscription/checkout.js | 10 +++-- functions/src/subscription/management.js | 6 ++- functions/src/subscription/shared.js | 10 +---- functions/src/subscription/webhooks.js | 7 +++- functions/src/users.js | 28 +++++++++----- 18 files changed, 121 insertions(+), 109 deletions(-) delete mode 100644 functions/config/keys.js create mode 100644 functions/config/secrets.js delete mode 100644 functions/serviceAccountKey.json diff --git a/.gitignore b/.gitignore index 1586d67..f277a53 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ npm-debug.* *.key *.mobileprovision *.orig.* +functions/.secret.local +functions/serviceAccountKey.json web-build/ ios/ android/ diff --git a/functions/config/keys.js b/functions/config/keys.js deleted file mode 100644 index e54df06..0000000 --- a/functions/config/keys.js +++ /dev/null @@ -1,10 +0,0 @@ -exports.GEMINI_API_KEY = 'AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y' - -exports.SUNO_API_KEY = 'c1636e04f606811511e19ec6e1545aa6' // api key - -exports.RESEND_API_KEY = 're_NLcHmYiz_4LoFrzvPBbShNQBgNGTQmBbu' - -exports.STRIPE_SECRET_KEY = - 'sk_test_51SPfcjCzf2o5bDRdUFGNrQYIE271EDfS2Ucn31f98Ublttcl1EBNRoOoJX1RfXXzHp7mKRGrIlCG24biiqUZ2YMh00s9WODluu' -exports.STRIPE_WEBHOOK_SECRET = 'whsec_pDrvXVjjMuZjtsnRaFVJrDmKO5QEtNkW' -exports.STRIPE_RETURN_URL = '' diff --git a/functions/config/secrets.js b/functions/config/secrets.js new file mode 100644 index 0000000..764e6d1 --- /dev/null +++ b/functions/config/secrets.js @@ -0,0 +1,15 @@ +const { defineSecret } = require('firebase-functions/params') + +const GEMINI_API_KEY = defineSecret('GEMINI_API_KEY') +const SUNO_API_KEY = defineSecret('SUNO_API_KEY') +const RESEND_API_KEY = defineSecret('RESEND_API_KEY') +const STRIPE_SECRET_KEY = defineSecret('STRIPE_SECRET_KEY') +const STRIPE_WEBHOOK_SECRET = defineSecret('STRIPE_WEBHOOK_SECRET') + +module.exports = { + GEMINI_API_KEY, + SUNO_API_KEY, + RESEND_API_KEY, + STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET, +} diff --git a/functions/helpers/gemini.js b/functions/helpers/gemini.js index cb3cd2c..d00bb79 100644 --- a/functions/helpers/gemini.js +++ b/functions/helpers/gemini.js @@ -1,14 +1,14 @@ const { googleAI } = require('@genkit-ai/googleai') const { genkit, z } = require('genkit') -const { GEMINI_API_KEY } = require('../config/keys') +const { GEMINI_API_KEY } = require('../config/secrets') const admin = require('firebase-admin') const { Buffer } = require('buffer') const { setTimeout } = require('timers/promises') // --- CONFIGURATION --- // Plus intelligente que le Flash original, ultra rapide, et stable sur l'API. -const TEXT_MODEL_NAME = 'gemini-3-pro-preview' -const IMAGE_MODEL_NAME = 'gemini-3-pro-image-preview' +const TEXT_MODEL_NAME = 'gemini-3.5-flash' +const IMAGE_MODEL_NAME = 'gemini-3-pro-image' // --- SINGLETON PATTERN (WARM START) --- // On stocke l'instance en dehors de la fonction pour la réutiliser @@ -19,7 +19,7 @@ const getAiInstance = () => { if (!aiInstance) { console.log('⚡ [Gemini] Initialisation froide (Cold Start)') aiInstance = genkit({ - plugins: [googleAI({ apiKey: GEMINI_API_KEY })], + plugins: [googleAI({ apiKey: GEMINI_API_KEY.value() })], }) } return aiInstance diff --git a/functions/helpers/stripe.js b/functions/helpers/stripe.js index 85879dd..d1f2f62 100644 --- a/functions/helpers/stripe.js +++ b/functions/helpers/stripe.js @@ -3,17 +3,11 @@ const { HttpsError } = require('firebase-functions/https') const Stripe = require('stripe') const { URL } = require('url') -const { - STRIPE_SECRET_KEY = '', - STRIPE_RETURN_URL = '', - STRIPE_PORTAL_CONFIGURATION = '', - STRIPE_MODE: CONFIG_STRIPE_MODE, -} = require('../config/keys') +const { STRIPE_SECRET_KEY } = require('../config/secrets') -const STRIPE_MODE = - typeof CONFIG_STRIPE_MODE === 'string' && CONFIG_STRIPE_MODE.trim() - ? CONFIG_STRIPE_MODE.trim() - : 'test' +const STRIPE_RETURN_URL = process.env.STRIPE_RETURN_URL?.trim() || '' +const STRIPE_PORTAL_CONFIGURATION = process.env.STRIPE_PORTAL_CONFIGURATION?.trim() || '' +const STRIPE_MODE = process.env.STRIPE_MODE?.trim() || 'test' const requireEnv = (key) => { const value = process.env?.[key] @@ -31,15 +25,9 @@ let cachedStripeClient = null let cachedPortalConfigurationId = null const resolveStripeSecretKey = () => { - const inlineKey = typeof STRIPE_SECRET_KEY === 'string' ? STRIPE_SECRET_KEY.trim() : '' - - if (inlineKey) { - return inlineKey - } - - const required = requireEnv('STRIPE_SECRET_KEY') - if (typeof required === 'string' && required.trim()) { - return required.trim() + const secretKey = STRIPE_SECRET_KEY.value().trim() + if (secretKey) { + return secretKey } throw new Error('STRIPE_SECRET_KEY not configured') diff --git a/functions/serviceAccountKey.json b/functions/serviceAccountKey.json deleted file mode 100644 index d448922..0000000 --- a/functions/serviceAccountKey.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "musicland-d33f9", - "private_key_id": "c1110a2aa31eb3ac7cf42d931249a2572bf68e37", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCZKlW0nTIg4Il2\nPtr/BYi247mWp8pxpCwS2ggwGBU7iO44jLQAgLWtZdHrQ7kpRfjVuceJDrkLrS82\ntkt8wQpytCFKfZjil9fdyS1hrVIQdfaPppovbRtzDpe0varA8XxgWXG8L+CQtCQd\nNhcixKONFhgOWTKB/tvEtGEExtpRRTb7y21o/EFbfpZqSbGEPcota2DPPvNh1r1y\nnhF7AcboGFTouDaTVL/oFIYYm3eWQP6nJRiM15xPqEhar+2z3lG9Y6HoRkkynFFj\nwvhQ6AUidA4A1ZW90HC33jsY5u7MAbzXY/QZ2MC82gaf26rda/7hThmk0RUSEaOv\n/G2f2lQBAgMBAAECggEAKnssdEDZQ3cjiTFbyjm9toQo2FlHdAeF1eC1PqmfUfjj\nobmcvzrzqNlBxM/uzjzDDHxPlHsJ5ewt2S3sd/iOrku5pBB70/UrgS8oMBxDU590\nOFmMEDVh1KKqfw3QC12vEoYcXoak8V2VWqjC9/i0KBCFsY0ZziZizAb/lbSVns52\nuJQhjvZURHQQOQmrZDm5mhklKCjE6qDxAMjL0yIhDQLAATzqeVPPeP8Rvq6u7yiB\noiB40OGiGHLdt28Q7IAXSQ+/Vqig7aw/qwrfZrFqgbgCYJKo1GiYFaDgRbH6GiaE\nhBJg7LtM0EaDR5G0raBWv5szqpfOb2DC6K802dcQRQKBgQDQm7HoJjmvEu9a6hxF\nVwS3rdJO67dTtxFMZM0HespQu659NT8f21QsRf/2JIcwktTnGzSeVTca0GZ4z8de\nkP9Wid0VPcA6Y3U7yplBf7Xh/5KW7/O6iSCEN03Y3/lH6vK6tBe5xCT3kfYKkrAd\n/xePLLBx++dAL5YFixMzBFfI5wKBgQC79i2S6Bcu6WQc0yykVHkAMStPz4gsAYA5\nqqCuWEwFcgEEY5AiP/oH056Hu2Z2HJHPp4OKYWSbqdxkNM214vysO0yhrF59BjQX\nyo+hwQfBqS0JfH7bJgUjNuDjP947qGtRXVo2i3FRJuZ4NmS2cGC9gT00p6rZRrbL\nJkaKSuVW1wKBgH3xsgxVn7fqd6Xs99qJOejnQbSyLUGWohP8O6jLYoAxf5xKXG0Q\nTxZXwLxLu28BBxbsXu1Ex3sh3OwNjfGzGuWVYGvllzvpNis+Gx7nStlCIq+L+Rti\nlMYeWZqZRng8CgXcmwJlwGfTsLcbbsb9bqzbLz/x9ODUmf+IwrOnaJR7AoGBALA7\nPjm6YFo8vAAGw5mEFjtm3owjMjLIaXgoEiXFZenJHUkHLNHgGEcUVFRBEe12FgST\ntFKfxFvrJFfT5ng+TdgSdWQ0oBcrJNaVtpvbx7D9NNvrHWnOOQzuXGbVsEFRROXx\n2C7CCUokZfyaS01/CkVEEqX2+spjceoBhiwN0v/bAoGBAK4sYw+3o3yjH26/M/Ni\nn5hwxofTktvAkYi7GlSFaRf2QwwxMBeA6qM/AJn7rer6pRzWwApbae/kOyfZfivq\nLnfpwloJMtds/Mm2egz0zoX7WIZ15uEx4vha2IUmi4Qz92kUzgaD1jTN8ZLUCypn\npR/zlJVkLv5BFqOtfX8shz1T\n-----END PRIVATE KEY-----\n", - "client_email": "firebase-adminsdk-fbsvc@musicland-d33f9.iam.gserviceaccount.com", - "client_id": "114560176426659117925", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40musicland-d33f9.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} diff --git a/functions/src/cover.js b/functions/src/cover.js index dbc82eb..572b10f 100644 --- a/functions/src/cover.js +++ b/functions/src/cover.js @@ -7,6 +7,7 @@ const sharp = require('sharp') const crypto = require('crypto') // Imports internes +const { GEMINI_API_KEY } = require('../config/secrets') const { generateImageV2 } = require('../helpers/gemini') const { generatePicturePrompt } = require('../helpers/prompts') const { ALERT_TYPE, refList } = require('../index') @@ -284,6 +285,7 @@ exports.onTaskCreateGenerateCover = onDocumentCreated( timeoutSeconds: 540, // 9 minutes max (Imagen peut être lent) memory: '1GiB', document: 'tasks/{taskId}', + secrets: [GEMINI_API_KEY], }, async (event) => { const data = event.data?.data() || {} diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js index ae720d3..45320f5 100644 --- a/functions/src/lyrics.js +++ b/functions/src/lyrics.js @@ -2,7 +2,7 @@ const { onCall, HttpsError } = require('firebase-functions/v2/https') const { z } = require('genkit') const { generateAI, analyseLyrics } = require('../helpers/gemini') const { SUNO_API_BASE, SUNO_TIMESTAMPED_LYRICS_PATH } = require('../config/suno') -const { SUNO_API_KEY } = require('../config/keys') +const { GEMINI_API_KEY, SUNO_API_KEY } = require('../config/secrets') const axios = require('axios') const { FieldValue } = require('firebase-admin/firestore') const { refList } = require('../index') @@ -137,7 +137,7 @@ const buildModerationBrief = ({ // --- GENERATION DE PAROLES (MAIN) --- -exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => { +exports.generateLyrics = onCall({ secrets: [GEMINI_API_KEY] }, async ({ auth = {}, data = {} }) => { try { const { objective = '', @@ -344,7 +344,7 @@ const softenModerationDecision = (rawResult = {}) => { } } -exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => { +exports.analyseLyricsToxicity = onCall({ secrets: [GEMINI_API_KEY] }, async ({ data = {} }) => { try { const requestSchema = z.object({ title: z.string().optional(), @@ -420,7 +420,7 @@ async function getSunoTimestamps(projectId) { { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${SUNO_API_KEY}`, + Authorization: `Bearer ${SUNO_API_KEY.value()}`, }, timeout: 30000, } diff --git a/functions/src/music.js b/functions/src/music.js index f977663..530364d 100644 --- a/functions/src/music.js +++ b/functions/src/music.js @@ -8,7 +8,7 @@ const { randomUUID } = require('crypto') const { ALERT_TYPE, refList } = require('../index') const { sendNotification } = require('./notifications') const { createOrderDocument, ORDER_TYPES } = require('./helpers/orders') -const { SUNO_API_KEY } = require('../config/keys') +const { SUNO_API_KEY } = require('../config/secrets') const { SUNO_MODEL, SUNO_CALLBACK_URL, @@ -468,7 +468,7 @@ const mergeMusicUrls = async (projectRef, newUrls) => { // 5. FONCTIONS PRINCIPALES (CLOUD FUNCTIONS) // ========================================== -exports.generateMusic = onCall(async ({ data = {} }) => { +exports.generateMusic = onCall({ secrets: [SUNO_API_KEY] }, async ({ data = {} }) => { try { // Extraction des données du Frontend const { @@ -542,7 +542,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => { const response = await axios.post(`${SUNO_API_BASE}${SUNO_API_PATH}`, payload, { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${SUNO_API_KEY}`, + Authorization: `Bearer ${SUNO_API_KEY.value()}`, }, }) @@ -571,12 +571,12 @@ exports.generateMusic = onCall(async ({ data = {} }) => { }) // GET STATUS (inchangé mais inclus pour complétude) -exports.getSunoStatus = onCall(async ({ data = {} }) => { +exports.getSunoStatus = onCall({ secrets: [SUNO_API_KEY] }, async ({ data = {} }) => { const { taskId } = data if (!taskId) throw new Error('TaskId manquant') try { const response = await axios.get(`${SUNO_API_BASE}${SUNO_STATUS_PATH}?taskId=${taskId}`, { - headers: { Authorization: `Bearer ${SUNO_API_KEY}` }, + headers: { Authorization: `Bearer ${SUNO_API_KEY.value()}` }, timeout: 30000, }) return { diff --git a/functions/src/notifications.js b/functions/src/notifications.js index 8110d3c..21d5b16 100644 --- a/functions/src/notifications.js +++ b/functions/src/notifications.js @@ -4,9 +4,16 @@ const { refList, ALERT_TYPE } = require('../index') const { Expo } = require('expo-server-sdk') const { Resend } = require('resend') const { basicTemplate } = require('../helpers/email') -const { RESEND_API_KEY } = require('../config/keys') +const { RESEND_API_KEY } = require('../config/secrets') -const resendInstance = RESEND_API_KEY ? new Resend(RESEND_API_KEY) : null +let resendInstance = null + +const getResendInstance = () => { + if (!resendInstance) { + resendInstance = new Resend(RESEND_API_KEY.value()) + } + return resendInstance +} // Initialisation de Expo SDK let expo = new Expo() @@ -58,7 +65,11 @@ function buildNotificationEmailPayload({ title = '', message = '', template = {} } exports.sendNotificationWhenDocIsCreated = onDocumentCreated( - { region: 'europe-west1', document: 'notifications/{notificationId}' }, + { + region: 'europe-west1', + document: 'notifications/{notificationId}', + secrets: [RESEND_API_KEY], + }, async (event) => { try { const { @@ -112,24 +123,20 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated( } if ((emailNotifications || mailOnly) && !!receiverEmail) { - if (!resendInstance) { - console.warn('Resend client not configured; unable to send notification email.') - } else { - try { - const { subject, html } = buildNotificationEmailPayload({ - title, - message, - template: notifData?.email, - }) - await resendInstance.emails.send({ - from: EMAIL_FROM, - to: [receiverEmail], - subject, - html, - }) - } catch (e) { - console.log('Error sending email:', e) - } + try { + const { subject, html } = buildNotificationEmailPayload({ + title, + message, + template: notifData?.email, + }) + await getResendInstance().emails.send({ + from: EMAIL_FROM, + to: [receiverEmail], + subject, + html, + }) + } catch (e) { + console.log('Error sending email:', e) } } else { console.log( diff --git a/functions/src/project.js b/functions/src/project.js index 1a0e646..7f439a0 100644 --- a/functions/src/project.js +++ b/functions/src/project.js @@ -3,11 +3,17 @@ const { FieldValue } = require('firebase-admin/firestore') const { onDocumentWritten } = require('firebase-functions/firestore') const _ = require('lodash') const { refList, db } = require('../index') +const { SUNO_API_KEY } = require('../config/secrets') const { getSunoTimestamps } = require('./lyrics') const { deleteFolder } = require('../helpers/firebase') const { buildMonthKey } = require('../helpers/stats') -exports.onProjectWritten = onDocumentWritten('projects/{projectId}', async (projectSnap) => { +const PROJECT_FUNCTION_OPTIONS = { + document: 'projects/{projectId}', + secrets: [SUNO_API_KEY], +} + +exports.onProjectWritten = onDocumentWritten(PROJECT_FUNCTION_OPTIONS, async (projectSnap) => { try { const { projectId } = projectSnap.params const beforeData = projectSnap?.data?.before?.data() || null diff --git a/functions/src/stripe.js b/functions/src/stripe.js index edaceb9..201bae6 100644 --- a/functions/src/stripe.js +++ b/functions/src/stripe.js @@ -9,6 +9,7 @@ try { } const { REGION, refsList } = require('../index') +const { STRIPE_SECRET_KEY } = require('../config/secrets') const STRIPE_MODE = 'test' const { getStripeClient, @@ -22,6 +23,7 @@ const { } = require('../helpers/stripe') const paymentsCollection = admin.firestore().collection('payments') +const STRIPE_FUNCTION_OPTIONS = { region: REGION, secrets: [STRIPE_SECRET_KEY] } const getServerTimestamp = () => { if (FieldValue?.serverTimestamp) { @@ -47,7 +49,7 @@ const normalizePaymentIntent = (paymentIntent) => ({ metadata: paymentIntent.metadata, }) -const createCheckoutSession = onCall({ region: REGION }, async (request) => { +const createCheckoutSession = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -123,7 +125,7 @@ const createCheckoutSession = onCall({ region: REGION }, async (request) => { } }) -const getPremiumStatus = onCall({ region: REGION }, async (request) => { +const getPremiumStatus = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -203,7 +205,7 @@ const getPremiumStatus = onCall({ region: REGION }, async (request) => { } }) -const createStripeCustomerPortalSession = onCall({ region: REGION }, async (request) => { +const createStripeCustomerPortalSession = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -365,7 +367,7 @@ const ensureStripeConnectAccount = async ({ uid, stripe, userRef, userData }) => } } -const createStripeConnectLoginLink = onCall({ region: REGION }, async (request) => { +const createStripeConnectLoginLink = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -425,7 +427,7 @@ const createStripeConnectLoginLink = onCall({ region: REGION }, async (request) } }) -const verifyStripePayment = onCall({ region: REGION }, async (request) => { +const verifyStripePayment = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { diff --git a/functions/src/subscription/catalog.js b/functions/src/subscription/catalog.js index 91939d3..211160e 100644 --- a/functions/src/subscription/catalog.js +++ b/functions/src/subscription/catalog.js @@ -1,6 +1,7 @@ const { HttpsError, onCall } = require('firebase-functions/https') const { getStripeClient, mapStripeErrorToHttps } = require('../../helpers/stripe') +const { STRIPE_SECRET_KEY } = require('../../config/secrets') const { REGION } = require('./config') const { SUBSCRIPTION_PRICE_IDS, @@ -8,6 +9,7 @@ const { COIN_PACK_PRODUCTS, } = require('./constants') const { parseCoinsPerMonth, formatCoinPack } = require('./shared') +const STRIPE_FUNCTION_OPTIONS = { region: REGION, secrets: [STRIPE_SECRET_KEY] } const formatPlan = (price, priceId) => { if (!price || typeof price !== 'object') { @@ -40,7 +42,7 @@ const formatPlan = (price, priceId) => { } } -const listSubscriptionPlans = onCall({ region: REGION }, async () => { +const listSubscriptionPlans = onCall(STRIPE_FUNCTION_OPTIONS, async () => { try { const stripe = getStripeClient() @@ -76,7 +78,7 @@ const listSubscriptionPlans = onCall({ region: REGION }, async () => { } }) -const listCoinPacks = onCall({ region: REGION }, async () => { +const listCoinPacks = onCall(STRIPE_FUNCTION_OPTIONS, async () => { try { const stripe = getStripeClient() diff --git a/functions/src/subscription/checkout.js b/functions/src/subscription/checkout.js index b13e0f6..48d3174 100644 --- a/functions/src/subscription/checkout.js +++ b/functions/src/subscription/checkout.js @@ -8,6 +8,7 @@ const { formatCheckoutSessionResponse, mapStripeErrorToHttps, } = require('../../helpers/stripe') +const { STRIPE_SECRET_KEY } = require('../../config/secrets') const { REGION } = require('./config') const { ALL_SUBSCRIPTION_PRICE_IDS, @@ -17,6 +18,7 @@ const { const { refsList, formatCoinPack, getSubscriptionMetaFromPrice } = require('./shared') const CHECKOUT_UI_MODES = new Set(['hosted', 'embedded']) +const STRIPE_FUNCTION_OPTIONS = { region: REGION, secrets: [STRIPE_SECRET_KEY] } const sanitizePriceId = (value) => { if (typeof value !== 'string') { @@ -65,7 +67,7 @@ const withCheckoutNavigationParams = (baseParams, { uiMode, successUrl, cancelUr } } -const createSubscriptionCheckoutSession = onCall({ region: REGION }, async (request) => { +const createSubscriptionCheckoutSession = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -152,7 +154,7 @@ const createSubscriptionCheckoutSession = onCall({ region: REGION }, async (requ } }) -const createSongDownloadCheckoutSession = onCall({ region: REGION }, async (request) => { +const createSongDownloadCheckoutSession = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -231,7 +233,7 @@ const createSongDownloadCheckoutSession = onCall({ region: REGION }, async (requ } }) -const createPlaybackDownloadCheckoutSession = onCall({ region: REGION }, async (request) => { +const createPlaybackDownloadCheckoutSession = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -310,7 +312,7 @@ const createPlaybackDownloadCheckoutSession = onCall({ region: REGION }, async ( } }) -const createCoinPackCheckoutSession = onCall({ region: REGION }, async (request) => { +const createCoinPackCheckoutSession = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { diff --git a/functions/src/subscription/management.js b/functions/src/subscription/management.js index b51f749..8005cd4 100644 --- a/functions/src/subscription/management.js +++ b/functions/src/subscription/management.js @@ -1,11 +1,13 @@ const { HttpsError, onCall } = require('firebase-functions/https') const { getStripeClient, mapStripeErrorToHttps } = require('../../helpers/stripe') +const { STRIPE_SECRET_KEY } = require('../../config/secrets') const { REGION } = require('./config') const { CANCELABLE_SUBSCRIPTION_STATUSES, ACTIVE_SUBSCRIPTION_STATUSES } = require('./constants') const { refsList, formatSubscriptionForClient, resolveUserContext } = require('./shared') +const STRIPE_FUNCTION_OPTIONS = { region: REGION, secrets: [STRIPE_SECRET_KEY] } -const cancelActiveSubscription = onCall({ region: REGION }, async (request) => { +const cancelActiveSubscription = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { @@ -101,7 +103,7 @@ const cancelActiveSubscription = onCall({ region: REGION }, async (request) => { } }) -const getActiveSubscription = onCall({ region: REGION }, async (request) => { +const getActiveSubscription = onCall(STRIPE_FUNCTION_OPTIONS, async (request) => { try { const uid = request?.auth?.uid if (!uid) { diff --git a/functions/src/subscription/shared.js b/functions/src/subscription/shared.js index 8f7024f..897ff30 100644 --- a/functions/src/subscription/shared.js +++ b/functions/src/subscription/shared.js @@ -2,7 +2,7 @@ const admin = require('firebase-admin') const { FieldValue } = require('firebase-admin/firestore') const { refList } = require('../../index') -const { STRIPE_WEBHOOK_SECRET } = require('../../config/keys') +const { STRIPE_WEBHOOK_SECRET } = require('../../config/secrets') const { SUBSCRIPTION_LEVEL_ALLOWANCES, SUBSCRIPTION_PRICE_METADATA, @@ -120,13 +120,7 @@ const resolveStripeWebhookSecret = () => { return cachedStripeWebhookSecret } - const envSecret = - typeof process?.env?.STRIPE_WEBHOOK_SECRET === 'string' - ? process.env.STRIPE_WEBHOOK_SECRET.trim() - : '' - const inlineSecret = typeof STRIPE_WEBHOOK_SECRET === 'string' ? STRIPE_WEBHOOK_SECRET.trim() : '' - - const secret = envSecret || inlineSecret + const secret = STRIPE_WEBHOOK_SECRET.value().trim() if (!secret) { throw new Error('STRIPE_WEBHOOK_SECRET not configured') } diff --git a/functions/src/subscription/webhooks.js b/functions/src/subscription/webhooks.js index 53f2b71..dab08b0 100644 --- a/functions/src/subscription/webhooks.js +++ b/functions/src/subscription/webhooks.js @@ -3,6 +3,7 @@ const { HttpsError } = require('firebase-functions/https') const { ORDER_TYPES, createOrderDocument } = require('../helpers/orders') const { getStripeClient } = require('../../helpers/stripe') +const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } = require('../../config/secrets') const { REGION } = require('./config') const { paymentsCollection, @@ -20,6 +21,10 @@ const { upsertPaymentDocument, } = require('./shared') const { PREMIUM_SUBSCRIPTION_STATUSES } = require('./constants') +const STRIPE_WEBHOOK_OPTIONS = { + region: REGION, + secrets: [STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET], +} const isPaidCheckoutSession = (session) => { if (!session || typeof session !== 'object') { @@ -749,7 +754,7 @@ const handleStripeWebhookEvent = async ({ event, stripe }) => { } } -const handleStripeWebhook = onRequest({ region: REGION }, async (req, res) => { +const handleStripeWebhook = onRequest(STRIPE_WEBHOOK_OPTIONS, async (req, res) => { if (req.method !== 'POST') { res.status(405).send('Method Not Allowed') return diff --git a/functions/src/users.js b/functions/src/users.js index 4ba66ab..22d633a 100644 --- a/functions/src/users.js +++ b/functions/src/users.js @@ -6,14 +6,26 @@ const { ORDER_TYPES, createOrderDocument } = require('./helpers/orders') const { deleteFolder } = require('../helpers/firebase') const { Resend } = require('resend') const { welcomeTemplate } = require('../helpers/email') -const { RESEND_API_KEY } = require('../config/keys') +const { RESEND_API_KEY } = require('../config/secrets') const { onRequest } = require('firebase-functions/https') -const resendClient = new Resend(RESEND_API_KEY) +let resendClient = null + +const getResendClient = () => { + if (!resendClient) { + resendClient = new Resend(RESEND_API_KEY.value()) + } + return resendClient +} + const WELCOME_EMAIL_FROM = 'MusicLand ' const WELCOME_EMAIL_SUBJECT = 'Bienvenue sur MusicLand' +const USER_CREATED_OPTIONS = { + document: 'users/{userID}', + secrets: [RESEND_API_KEY], +} -exports.testWelcomMail = onRequest(async (req, res) => { +exports.testWelcomMail = onRequest({ secrets: [RESEND_API_KEY] }, async (req, res) => { if (req.method !== 'GET') { res.set('Allow', 'GET') return res.status(405).json({ success: false, error: 'Method not allowed' }) @@ -22,7 +34,7 @@ exports.testWelcomMail = onRequest(async (req, res) => { const targetEmail = req.query.email || 'tdtomthomas@gmail.com' const firstName = req.query.firstName || 'Toto' const lastName = req.query.lastName || 'Test' - const { data, error } = await resendClient.emails.send({ + const { data, error } = await getResendClient().emails.send({ from: WELCOME_EMAIL_FROM, to: [targetEmail], subject: WELCOME_EMAIL_SUBJECT, @@ -40,7 +52,7 @@ exports.testWelcomMail = onRequest(async (req, res) => { } }) -exports.onUserCreated = onDocumentCreated('users/{userID}', async (event) => { +exports.onUserCreated = onDocumentCreated(USER_CREATED_OPTIONS, async (event) => { try { const { email = '', firstName = '', lastName = '' } = event?.data?.data() || {} @@ -62,13 +74,9 @@ exports.onUserCreated = onDocumentCreated('users/{userID}', async (event) => { ) } if (email) { - if (!resendClient) { - console.warn('Resend API key not configured; skipping welcome email.') - return - } try { console.log(`Sending welcome email to ${email}`) - const { data, error } = await resendClient.emails.send({ + const { data, error } = await getResendClient().emails.send({ from: WELCOME_EMAIL_FROM, to: [email], subject: WELCOME_EMAIL_SUBJECT,