import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; import * as admin from 'firebase-admin'; import * as express from "express"; import * as logger from "firebase-functions/logger"; import { onDocumentCreated } from "firebase-functions/firestore"; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; import cors from 'cors'; import axios from "axios"; import { getStorage } from 'firebase-admin/storage'; const formData = require('form-data'); const Mailgun = require('mailgun.js'); const { convert } = require('html-to-text'); const twilio = require('twilio'); if (!admin.apps.length) { admin.initializeApp(); } const corsHandler = cors({ origin: true }); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { try { const { toAddress, subject, message, fileUrl, fileName } = request.body; if (!toAddress || !subject || !message || !fileUrl) { response.status(400).json({ error: 'Missing required fields (toAddress, subject, message, fileUrl)' }); return; } const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); await new Promise((resolve, reject) => { const file = fs.createWriteStream(tempFilePath); https.get(fileUrl, (res) => { res.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', (err) => { fs.unlink(tempFilePath, () => { }); reject(err); }); }); const mailgun = new Mailgun(formData); const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); const options = { wordwrap: 130, }; const textMessage = convert(message, options); const fileBuffer = fs.readFileSync(tempFilePath); const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); const data = { from: process.env.MAILGUN_FROM_ADDRESS, to: toAddress, subject: subject, text: textMessage, html: message, attachment: { data: fileBuffer, filename: attachmentFilename, contentType: 'application/pdf', } }; const result = await client.messages.create(process.env.MAILGUN_SERVER, data); fs.unlinkSync(tempFilePath); logger.info('Email with attachment from URL sent successfully'); response.json({ success: true, result }); } catch (error) { logger.error('Error sending email with attachment from URL:', error); response.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) }); } }); }); export const accessFile = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { try { const filePath = request.query.path as string; if (!filePath) { response.status(400).send('File path is required'); return; } const expirationMs = 60 * 60 * 1000; const bucket = getStorage().bucket(); const file = bucket.file(filePath); const [exists] = await file.exists(); if (!exists) { response.status(404).send('File not found'); return; } const [signedUrl] = await file.getSignedUrl({ action: 'read', expires: Date.now() + expirationMs, responseDisposition: `attachment; filename="${path.basename(filePath)}"`, }); response.redirect(signedUrl); logger.info(`File access redirect for ${filePath}`); } catch (error) { logger.error('Error accessing file:', error); response.status(500).send('Error accessing file'); } }); }); export const sendEmailMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { const mailgun = new Mailgun(formData); const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); const toAddress = request.body.toAddress; const subject = request.body.subject; const message = request.body.message; const options = { wordwrap: 130, }; const textMessage = convert(message, options); mailGunClient.messages.create(process.env.MAILGUN_SERVER, { from: process.env.MAILGUN_FROM_ADDRESS, to: toAddress, subject: subject, text: textMessage, html: message }).then((res: any) => { logger.info(res); response.send(res); }).catch((err: any) => { logger.error(err); response.send(err); }); }); }); export const sendSMSMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); const { to, body } = request.body; client.messages .create({ body: body, from: process.env.TWILIO_PHONE_NUMBER, to: to }) .then((message: any) => { logger.info('SMS sent successfully:', message.sid); response.json({ success: true, messageId: message.sid }); }) .catch((error: any) => { logger.error('Error sending SMS:', error); response.status(500).json({ success: false, error: error.message }); }); }); }); interface Invitation { email: string; phoneNumber: string; gymName: string; invitedByName: string; } export const notifyInvitation = onDocumentCreated({ document: 'notifications/{notificationId}', region: '#{SERVICES_RGN}#' }, async (event: any) => { const invitation = event.data?.data() as Invitation; const invitationId = event.params.invitationId; if (!invitation) { console.error('Invitation data is missing.'); return null; } try { const userQuery = await admin .firestore() .collection('users') .where('email', '==', invitation.email) .where('phoneNumber', '==', invitation.phoneNumber) .limit(1) .get(); if (userQuery.empty) { console.log( `User not found for email: ${invitation.email} and phone: ${invitation.phoneNumber}.` ); return null; } const user = userQuery.docs[0].data(); const fcmToken = user.fcmToken; if (!fcmToken) { console.log(`FCM token not found for user: ${invitation.email}.`); return null; } const message: admin.messaging.Message = { notification: { title: 'New Gym Invitation', body: `${invitation.invitedByName} has invited you to join ${invitation.gymName}`, }, data: { type: 'invitation', invitationId: invitationId, gymName: invitation.gymName, senderName: invitation.invitedByName, }, android: { priority: 'high', notification: { channelId: 'invitations_channel', priority: 'high', defaultSound: true, defaultVibrateTimings: true, icon: '@mipmap/ic_launcher', clickAction: 'FLUTTER_NOTIFICATION_CLICK', }, }, token: fcmToken, }; await admin.messaging().send(message); console.log(`Invitation notification sent to ${invitation.email}.`); return null; } catch (error) { console.error('Error sending invitation notification:', error); return null; } }); export const createCashfreeOrder = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { try { const authHeader = request.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { response.status(401).json({ error: 'Unauthorized' }); return; } const idToken = authHeader.split('Bearer ')[1]; const decodedToken = await admin.auth().verifyIdToken(idToken); const uid = decodedToken.uid; const { amount, customerName, customerEmail, customerPhone, productInfo } = request.body; if (!amount || !customerEmail || !customerPhone) { response.status(400).json({ error: 'Missing required fields' }); return; } const clientId = process.env.CASHFREE_CLIENT_ID; const clientSecret = process.env.CASHFREE_CLIENT_SECRET; const isTest = true; const apiUrl = isTest ? 'https://sandbox.cashfree.com/pg/orders' : 'https://api.cashfree.com/pg/orders'; const orderId = `order_${Date.now()}_${uid.substring(0, 6)}`; const cashfreeResponse = await axios.post( apiUrl, { order_id: orderId, order_amount: amount, order_currency: 'INR', customer_details: { customer_id: uid, customer_name: customerName || 'Fitlien User', customer_email: customerEmail, customer_phone: customerPhone }, order_meta: { return_url: `https://fitlien.com/payment/status?order_id={order_id}`, // notify_url: `https://$filien.web.app/verifyCashfreePayment` }, order_note: productInfo || 'Fitlien Membership' }, { headers: { 'x-api-version': '2022-09-01', 'x-client-id': clientId, 'x-client-secret': clientSecret, 'Content-Type': 'application/json' } } ); await admin.firestore().collection('payment_orders').doc(orderId).set({ userId: uid, amount: amount, customerEmail: customerEmail, customerPhone: customerPhone, orderStatus: 'CREATED', paymentGateway: 'Cashfree', createdAt: new Date(), ...cashfreeResponse.data }); response.json({ order_id: cashfreeResponse.data.order_id, payment_session_id: cashfreeResponse.data.payment_session_id }); logger.info(`Cashfree order created: ${orderId}`); } catch (error: any) { logger.error('Cashfree order creation error:', error); response.status(500).json({ error: 'Failed to create payment order', details: error.response?.data || error.message }); } }); }); export const verifyCashfreePayment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { try { const orderId = request.body.order_id || request.query.order_id; if (!orderId) { response.status(400).json({ error: 'Order ID is required' }); return; } const clientId = process.env.CASHFREE_CLIENT_ID; const clientSecret = process.env.CASHFREE_CLIENT_SECRET; const isTest = process.env.CASHFREE_ENVIRONMENT !== 'production'; const apiUrl = isTest ? `https://sandbox.cashfree.com/pg/orders/${orderId}` : `https://api.cashfree.com/pg/orders/${orderId}`; const cashfreeResponse = await axios.get( apiUrl, { headers: { 'x-api-version': '2022-09-01', 'x-client-id': clientId, 'x-client-secret': clientSecret } } ); await admin.firestore().collection('payment_orders').doc(orderId).update({ orderStatus: cashfreeResponse.data.order_status, paymentDetails: cashfreeResponse.data, updatedAt: new Date() }); if (request.headers['x-webhook-source'] === 'cashfree') { response.status(200).send('OK'); return; } response.json({ status: cashfreeResponse.data.order_status, paymentDetails: cashfreeResponse.data }); logger.info(`Cashfree payment verified: ${orderId}`); } catch (error: any) { logger.error('Cashfree payment verification error:', error); response.status(500).json({ error: 'Failed to verify payment status', details: error.response?.data || error.message }); } }); }); export const getPlacesAutocomplete = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { return corsHandler(request, response, async () => { try { const { input, location, radius, types, components, sessiontoken } = request.query; if (!input) { response.status(400).json({ error: 'Input parameter is required for autocomplete' }); return; } const apiKey = process.env.GOOGLE_MAPS_API_KEY; if (!apiKey) { logger.error('Google Places API key is not configured'); response.status(500).json({ error: 'Server configuration error' }); return; } const url = 'https://maps.googleapis.com/maps/api/place/autocomplete/json'; const params: any = { key: apiKey, input: input }; if (location && radius) { params.location = location; params.radius = radius; } if (types) { params.types = types; } if (components) { params.components = components; } if (sessiontoken) { params.sessiontoken = sessiontoken; } const result = await axios.get(url, { params }); logger.info('Google Places Autocomplete API request completed successfully'); response.json(result.data); } catch (error) { logger.error('Error fetching place autocomplete suggestions:', error); response.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) }); } }); });