diff --git a/functions/.env.example b/functions/.env.example index c19a79d..670bb4c 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -1,6 +1,3 @@ -TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}# -TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}# -TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}# SERVICES_RGN=#{SERVICES_RGN}# CASHFREE_CLIENT_ID=#{CASHFREE_CLIENT_ID}# CASHFREE_CLIENT_SECRET=#{CASHFREE_CLIENT_SECRET}# diff --git a/functions/src/email/index.ts b/functions/src/email/index.ts deleted file mode 100644 index 53ac110..0000000 --- a/functions/src/email/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { sendEmailSES } from './sendEmailSES'; diff --git a/functions/src/email/sendEmailSES.ts b/functions/src/email/sendEmailSES.ts deleted file mode 100644 index ff486be..0000000 --- a/functions/src/email/sendEmailSES.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { getLogger } from "../shared/config"; -import { getCorsHandler } from "../shared/middleware"; -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import { Response } from "express"; -import { SESClient } from "@aws-sdk/client-ses"; -import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; -import { HttpsError } from "firebase-functions/v2/https"; -import * as mime from 'mime-types'; -import axios from 'axios'; - -const logger = getLogger(); -const corsHandler = getCorsHandler(); - -interface EmailRequest { - to: string | string[]; - subject: string; - html: string; - text?: string; - from: string; - replyTo?: string; - attachments?: Attachment[]; - fileUrl?: string; - fileName?: string; -} - -interface Attachment { - filename: string; - content: string | Buffer; - contentType?: string; -} - -const stripHtml = (html: string): string => { - if (!html) return ''; - return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); -} - -async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { - const ses = new SESClient({ - region: process.env.AWS_REGION, - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' - } - }); - - const command = new SendEmailCommand({ - Source: data.from, - Destination: { ToAddresses: recipients }, - Message: { - Subject: { Data: data.subject }, - Body: { - Html: { Data: data.html }, - Text: { Data: data.text || stripHtml(data.html) } - } - }, - ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined, - }); - - const result = await ses.send(command); - return { messageId: result.MessageId }; -} - -async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { - const ses = new SESClient({ - region: process.env.AWS_REGION, - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' - } - }); - - const boundary = `boundary_${Math.random().toString(16).substr(2)}`; - let rawMessage = `From: ${data.from}\n`; - rawMessage += `To: ${recipients.join(', ')}\n`; - rawMessage += `Subject: ${data.subject}\n`; - rawMessage += `MIME-Version: 1.0\n`; - rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; - - // Add email body (multipart/alternative) - rawMessage += `--${boundary}\n`; - rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`; - - // Text part - if (data.text) { - rawMessage += `--alt_${boundary}\n`; - rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`; - rawMessage += `${data.text}\n\n`; - } - - // HTML part - rawMessage += `--alt_${boundary}\n`; - rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`; - rawMessage += `${data.html}\n\n`; - - // Close alternative part - rawMessage += `--alt_${boundary}--\n\n`; - - // Add attachments - for (const attachment of data.attachments || []) { - const contentType = attachment.contentType || - mime.lookup(attachment.filename) || - 'application/octet-stream'; - - rawMessage += `--${boundary}\n`; - rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`; - rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; - rawMessage += `Content-Transfer-Encoding: base64\n\n`; - - const contentBuffer = typeof attachment.content === 'string' - ? Buffer.from(attachment.content, 'base64') - : attachment.content; - - rawMessage += contentBuffer.toString('base64') + '\n\n'; - } - - // Close message - rawMessage += `--${boundary}--`; - - const command = new SendRawEmailCommand({ - RawMessage: { Data: Buffer.from(rawMessage) } - }); - - const result = await ses.send(command); - return { messageId: result.MessageId }; -} - -async function downloadFileFromUrl(url: string): Promise { - try { - const response = await axios.get(url, { responseType: 'arraybuffer' }); - return Buffer.from(response.data); - } catch (error) { - logger.error(`Error downloading file from URL: ${error}`); - throw new Error(`Failed to download file: ${error}`); - } -} - -export const sendEmailSES = onRequest({ - region: '#{SERVICES_RGN}#' -}, (request: Request, response: Response) => { - return corsHandler(request, response, async () => { - try { - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - - // Initialize data with basic fields - const data: EmailRequest = { - to: toAddress, - html: message, - subject: subject, - text: stripHtml(message), - from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', - replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', - attachments: request.body.attachments as Attachment[] || [] - }; - - // Handle file URL if provided - if (request.body.fileUrl && request.body.fileName) { - logger.info(`Downloading attachment from URL: ${request.body.fileUrl}`); - try { - const fileContent = await downloadFileFromUrl(request.body.fileUrl); - - // If attachments array doesn't exist, create it - if (!data.attachments) { - data.attachments = []; - } - - // Add the downloaded file as an attachment - data.attachments.push({ - filename: request.body.fileName, - content: fileContent, - contentType: mime.lookup(request.body.fileName) || 'application/octet-stream' - }); - - logger.info(`Successfully downloaded attachment: ${request.body.fileName}`); - } catch (downloadError) { - logger.error(`Failed to download attachment: ${downloadError}`); - throw new Error(`Failed to process attachment: ${downloadError}`); - } - } - - if (!data.to || !data.subject || !data.html || !data.from) { - throw new HttpsError( - 'invalid-argument', - 'Missing required email fields' - ); - } - - logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); - const recipients = Array.isArray(data.to) ? data.to : [data.to]; - - if (data.attachments && data.attachments.length > 0) { - const messageResult = await sendEmailWithAttachments(data, recipients); - response.status(200).json(messageResult); - } else { - const messageResult = await sendSimpleEmail(data, recipients); - response.status(200).json(messageResult); - } - } catch (e) { - logger.error(`Error while sending E-mail. Error: ${e}`); - console.error(`Error while sending E-mail. Error: ${e}`); - response.status(500).json({ - success: false, - error: 'Error while sending E-mail' - }); - } - }); -}); \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index a5d136c..a7f3b5a 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -10,16 +10,12 @@ setGlobalOptions({ }); export * from "./shared/config"; -export { sendEmailSES } from "./email"; -export { sendSMSMessage } from "./sms"; export { accessFile } from "./storage"; export { processNotificationOnCreate, checkExpiredMemberships, } from "./notifications"; export * from "./payments"; -export { getPlaceDetails, getPlacesAutocomplete } from "./places"; -export { registerClient } from "./users"; export { esslGetUserDetails, esslUpdateUser, diff --git a/functions/src/places/autocomplete.ts b/functions/src/places/autocomplete.ts deleted file mode 100644 index c846d3e..0000000 --- a/functions/src/places/autocomplete.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import * as express from "express"; -import { getLogger } from "../shared/config"; -import { getCorsHandler } from "../shared/middleware"; -import axios from "axios"; - -const logger = getLogger(); -const corsHandler = getCorsHandler(); - -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) - }); - } - }); -}); diff --git a/functions/src/places/details.ts b/functions/src/places/details.ts deleted file mode 100644 index 7ad9f43..0000000 --- a/functions/src/places/details.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import * as express from "express"; -import axios from "axios"; - -const { getCorsHandler } = require('../shared/middleware'); -const corsHandler = getCorsHandler(); -const { getLogger } = require('../shared/config'); -const logger = getLogger(); - - -export const getPlaceDetails = onRequest({ - region: '#{SERVICES_RGN}#' -}, async (request: Request, response: express.Response) => { - return corsHandler(request, response, async () => { - try { - const { place_id, fields } = request.query; - - if (!place_id) { - response.status(400).json({ - error: 'place_id parameter is required' - }); - 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/details/json'; - const params: any = { - key: apiKey, - place_id: place_id, - fields: fields || 'geometry' - }; - - const result = await axios.get(url, { params }); - logger.info('Google Places Details API request completed successfully'); - response.json(result.data); - } catch (error) { - logger.error('Error fetching place details:', error); - response.status(500).json({ - success: false, - error: error instanceof Error ? error.message : String(error) - }); - } - }); -}); \ No newline at end of file diff --git a/functions/src/places/index.ts b/functions/src/places/index.ts deleted file mode 100644 index a280b3c..0000000 --- a/functions/src/places/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { getPlaceDetails } from './details'; -export { getPlacesAutocomplete } from './autocomplete'; diff --git a/functions/src/sms/index.ts b/functions/src/sms/index.ts deleted file mode 100644 index f1afb2c..0000000 --- a/functions/src/sms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { sendSMSMessage } from './sendSMS'; \ No newline at end of file diff --git a/functions/src/sms/sendSMS.ts b/functions/src/sms/sendSMS.ts deleted file mode 100644 index 5887e9a..0000000 --- a/functions/src/sms/sendSMS.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { Request } from "firebase-functions/v2/https"; -import { getCorsHandler } from "../shared/middleware"; -import { getLogger } from "../shared/config"; -import twilio from 'twilio'; - -const corsHandler = getCorsHandler(); -const logger = getLogger(); - -// Initialize Twilio client -const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); - -interface SMSRequest { - to: string; - body: string; -} - -export const sendSMSMessage = onRequest({ - region: '#{SERVICES_RGN}#' -}, async (request: Request, response) => { - return corsHandler(request, response, async () => { - try { - const { to, body } = request.body as SMSRequest; - - // Input validation - if (!to || !body) { - logger.error('Missing required SMS parameters'); - response.status(400).json({ - success: false, - error: 'Both "to" and "body" parameters are required' - }); - return; - } - - // Validate phone number format (basic check) - if (!/^\+?[1-9]\d{1,14}$/.test(to)) { - logger.error('Invalid phone number format', { to }); - response.status(400).json({ - success: false, - error: 'Invalid phone number format' - }); - return; - } - - // Send SMS - const message = await twilioClient.messages.create({ - body: body, - from: process.env.TWILIO_PHONE_NUMBER, - to: to - }); - - logger.info('SMS sent successfully', { - messageId: message.sid, - to: to, - length: body.length - }); - - response.json({ - success: true, - messageId: message.sid, - timestamp: message.dateCreated - }); - - } catch (error: any) { - logger.error('Error sending SMS:', error); - - const statusCode = error.status === 401 ? 401 : 500; - - response.status(statusCode).json({ - success: false, - error: error.message, - code: error.code, - moreInfo: error.moreInfo - }); - } - }); -}); \ No newline at end of file diff --git a/functions/src/users/clientRegistration.ts b/functions/src/users/clientRegistration.ts deleted file mode 100644 index f39b827..0000000 --- a/functions/src/users/clientRegistration.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { onRequest } from "firebase-functions/v2/https"; -import { getCorsHandler } from "../shared/middleware"; -import { getAdmin, getLogger } from "../shared/config"; -import { Request } from "firebase-functions/v2/https"; -import { Response } from "express"; - - -const corsHandler = getCorsHandler(); -const admin = getAdmin(); -const logger = getLogger(); - -export const registerClient = onRequest({ - region: '#{SERVICES_RGN}#' -}, async (req: Request, res: Response) => { - return corsHandler(req, res, async () => { - try { - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); - } - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Unauthorized. Missing or invalid authorization header.' }); - } - const idToken = authHeader.split('Bearer ')[1]; - try { - const decodedToken = await admin.auth().verifyIdToken(idToken); - const uid = decodedToken.uid; - const userDoc = await admin.firestore().collection('users').doc(uid).get(); - - if (!userDoc.exists) { - return res.status(403).json({ error: 'Forbidden. User not found.' }); - } - - const userData = userDoc.data(); - if (!userData || !userData.roles || !userData.roles.includes('gym_owner')) { - return res.status(403).json({ error: 'Forbidden. Only gym owners can register clients.' }); - } - const gymUser = req.body; - if (!gymUser.fields["phone-number"]) { - return res.status(400).json({ error: 'Phone number is required' }); - } - - let clientUid; - try { - const userRecord = await admin.auth().getUserByPhoneNumber(gymUser.fields["phone-number"]) - .catch(() => null); - - if (userRecord) { - clientUid = userRecord.uid; - } else { - const newUser = await admin.auth().createUser({ - phoneNumber: gymUser.fields["phone-number"], - displayName: gymUser.fields["first-name"] || '', - }); - clientUid = newUser.uid; - } - } catch (error) { - logger.error('Error creating authentication user:', error); - return res.status(500).json({ - error: 'Failed to create authentication user', - details: error - }); - } - - try { - gymUser.uid = clientUid; - gymUser.registeredBy = uid; - - if (gymUser.name) { - gymUser.normalizedName = gymUser.name.toLowerCase(); - } - - if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { - gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); - } - - const clientData = { - ...gymUser, - }; - - await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData); - - return res.status(201).json({ - success: true, - message: 'Client registered successfully', - clientId: clientUid - }); - } catch (error) { - logger.error('Error creating client profile:', error); - - try { - if (!gymUser.uid) { - await admin.auth().deleteUser(clientUid); - } - } catch (deleteError) { - logger.error('Error deleting auth user after failed profile creation:', deleteError); - } - - return res.status(500).json({ - error: 'Failed to create client profile', - details: error - }); - } - - } catch (authError) { - logger.error('Authentication error:', authError); - return res.status(401).json({ error: 'Unauthorized. Invalid token.' }); - } - } catch (error) { - logger.error('Unexpected error in client registration:', error); - return res.status(500).json({ - error: 'Internal server error', - details: error - }); - } - }); -}); diff --git a/functions/src/users/index.ts b/functions/src/users/index.ts deleted file mode 100644 index 1f2445e..0000000 --- a/functions/src/users/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { registerClient } from './clientRegistration'; \ No newline at end of file