From c93a4f47185be5ef8639663a0f5f5a2c48bc6abb Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 6 Apr 2025 19:48:37 +0530 Subject: [PATCH 01/10] Update .firebaserc --- .firebaserc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.firebaserc b/.firebaserc index 008864b..74dd134 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,6 +1,7 @@ { "projects": { "debug": "fitlien-dev", + "qa": "fitlien-qa", "release": "fitlien" } -} +} \ No newline at end of file From 0f11768b65a186084c2262bf6d7c1f0d8d9b8c5f Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 6 Apr 2025 19:57:50 +0530 Subject: [PATCH 02/10] Adding QA pipeline --- fitlien-services-qa-pipeline.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 fitlien-services-qa-pipeline.yaml diff --git a/fitlien-services-qa-pipeline.yaml b/fitlien-services-qa-pipeline.yaml new file mode 100644 index 0000000..c9fb9ed --- /dev/null +++ b/fitlien-services-qa-pipeline.yaml @@ -0,0 +1,30 @@ +trigger: + - dev + +pool: + vmImage: "ubuntu-latest" + +variables: + major: $(VERSION_MAJOR) + minor: $(VERSION_MINOR) + prefix: $[format('{0}.{1}', variables['major'], variables['minor'])] + patch: $[counter(variables['prefix'], 100)] + buildNumber: $(major).$(minor).$(patch) + +resources: + repositories: + - repository: templateRepo + endpoint: cosq-network + type: github + name: cosq-network/azure-build-pipeline-templates + +extends: + template: firebase-functions-deploy.yaml@templateRepo + parameters: + nodeVersion: "20" + firebaseTokenSecret: $(FIREBASE_TOKEN) + functionsWorkingDirectory: "$(Build.SourcesDirectory)/functions" + envExamplePath: "$(Build.SourcesDirectory)/functions/.env.example" + envPath: "$(Build.SourcesDirectory)/functions/.env" + buildNumber: $(buildNumber) + buildType: qa From d4090faa82e528f8d11aa3561ce4c45b26b51ccf Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Sun, 6 Apr 2025 20:38:32 +0530 Subject: [PATCH 03/10] Update fitlien-services-qa-pipeline.yaml --- fitlien-services-qa-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fitlien-services-qa-pipeline.yaml b/fitlien-services-qa-pipeline.yaml index c9fb9ed..75d9767 100644 --- a/fitlien-services-qa-pipeline.yaml +++ b/fitlien-services-qa-pipeline.yaml @@ -1,5 +1,5 @@ trigger: - - dev + - main pool: vmImage: "ubuntu-latest" From 1991da23e628c83d0fe3cf30d42c7650865cf64b Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 7 Apr 2025 14:36:46 +0000 Subject: [PATCH 04/10] Added cors createCashfreeOrder (#7) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/7 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/package-lock.json | 1 + functions/package.json | 1 + functions/src/index.ts | 169 +++++++++++++++++++----------------- 3 files changed, 90 insertions(+), 81 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index ac67f4d..8a295b3 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "axios": "^1.8.4", + "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/package.json b/functions/package.json index 5c20815..939628c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -17,6 +17,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "axios": "^1.8.4", + "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/src/index.ts b/functions/src/index.ts index a26adc6..b250098 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,6 +8,7 @@ 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'); @@ -19,6 +20,9 @@ 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) => { @@ -245,92 +249,95 @@ export const notifyInvitation = onDocumentCreated({ export const createCashfreeOrder = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - 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' - } + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; } - ); - 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 - }); + const idToken = authHeader.split('Bearer ')[1]; + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; - response.json({ - order_id: cashfreeResponse.data.order_id, - payment_session_id: cashfreeResponse.data.payment_session_id - }); + const { + amount, + customerName, + customerEmail, + customerPhone, + productInfo + } = request.body; - 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 - }); - } + 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({ From a333b9520d954411d04a2be46794a62aefc6ed02 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 7 Apr 2025 15:08:21 +0000 Subject: [PATCH 05/10] feature/fitlien-add-cors (#8) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/8 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/src/index.ts | 339 +++++++++++++++++++++-------------------- 1 file changed, 177 insertions(+), 162 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index b250098..4c98cab 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -26,147 +26,158 @@ const corsHandler = cors({ origin: true }); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const { toAddress, subject, message, fileUrl, fileName } = request.body; + 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(); + if (!toAddress || !subject || !message || !fileUrl) { + response.status(400).json({ + error: 'Missing required fields (toAddress, subject, message, fileUrl)' }); - }).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', + 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 result = await client.messages.create(process.env.MAILGUN_SERVER, data); - fs.unlinkSync(tempFilePath); + const mailgun = new Mailgun(formData); + const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - 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) }); - } + 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) => { - try { - const filePath = request.query.path as string; - if (!filePath) { - response.status(400).send('File path is required'); - return; + 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'); } - - 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) => { - const mailgun = new Mailgun(formData); - const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); + return corsHandler(request, response, async () => { - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - const options = { - wordwrap: 130, - }; + const mailgun = new Mailgun(formData); + const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - 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); + 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) => { - 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 }); - }); + 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 { @@ -180,6 +191,7 @@ 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; @@ -343,55 +355,58 @@ export const createCashfreeOrder = onRequest({ export const verifyCashfreePayment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const orderId = request.body.order_id || request.query.order_id; + return corsHandler(request, response, async () => { - if (!orderId) { - response.status(400).json({ error: 'Order ID is required' }); - return; - } + try { + const orderId = request.body.order_id || request.query.order_id; - 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 - } + if (!orderId) { + response.status(400).json({ error: 'Order ID is required' }); + return; } - ); - await admin.firestore().collection('payment_orders').doc(orderId).update({ - orderStatus: cashfreeResponse.data.order_status, - paymentDetails: cashfreeResponse.data, - updatedAt: new Date() - }); + const clientId = process.env.CASHFREE_CLIENT_ID; + const clientSecret = process.env.CASHFREE_CLIENT_SECRET; + const isTest = process.env.CASHFREE_ENVIRONMENT !== 'production'; - if (request.headers['x-webhook-source'] === 'cashfree') { - response.status(200).send('OK'); - return; + 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 + }); } - - 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 - }); - } + }); }); From 7d37e295fe7c1b8353e9314192f2aa407b8279fd Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 7 Apr 2025 14:36:46 +0000 Subject: [PATCH 06/10] Added cors createCashfreeOrder (#7) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/7 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/package-lock.json | 1 + functions/package.json | 1 + functions/src/index.ts | 169 +++++++++++++++++++----------------- 3 files changed, 90 insertions(+), 81 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index ac67f4d..8a295b3 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "axios": "^1.8.4", + "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/package.json b/functions/package.json index 5c20815..939628c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -17,6 +17,7 @@ "dependencies": { "@types/node-fetch": "^2.6.12", "axios": "^1.8.4", + "cors": "^2.8.5", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", diff --git a/functions/src/index.ts b/functions/src/index.ts index a26adc6..b250098 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,6 +8,7 @@ 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'); @@ -19,6 +20,9 @@ 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) => { @@ -245,92 +249,95 @@ export const notifyInvitation = onDocumentCreated({ export const createCashfreeOrder = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - 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' - } + return corsHandler(request, response, async () => { + try { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; } - ); - 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 - }); + const idToken = authHeader.split('Bearer ')[1]; + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; - response.json({ - order_id: cashfreeResponse.data.order_id, - payment_session_id: cashfreeResponse.data.payment_session_id - }); + const { + amount, + customerName, + customerEmail, + customerPhone, + productInfo + } = request.body; - 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 - }); - } + 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({ From 0be5cfe30cd631535dc1354346f74db2560f92e3 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 7 Apr 2025 15:08:21 +0000 Subject: [PATCH 07/10] feature/fitlien-add-cors (#8) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/8 Co-authored-by: DhanshCOSQ Co-committed-by: DhanshCOSQ --- functions/src/index.ts | 339 +++++++++++++++++++++-------------------- 1 file changed, 177 insertions(+), 162 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index b250098..4c98cab 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -26,147 +26,158 @@ const corsHandler = cors({ origin: true }); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const { toAddress, subject, message, fileUrl, fileName } = request.body; + 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(); + if (!toAddress || !subject || !message || !fileUrl) { + response.status(400).json({ + error: 'Missing required fields (toAddress, subject, message, fileUrl)' }); - }).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', + 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 result = await client.messages.create(process.env.MAILGUN_SERVER, data); - fs.unlinkSync(tempFilePath); + const mailgun = new Mailgun(formData); + const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - 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) }); - } + 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) => { - try { - const filePath = request.query.path as string; - if (!filePath) { - response.status(400).send('File path is required'); - return; + 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'); } - - 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) => { - const mailgun = new Mailgun(formData); - const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); + return corsHandler(request, response, async () => { - const toAddress = request.body.toAddress; - const subject = request.body.subject; - const message = request.body.message; - const options = { - wordwrap: 130, - }; + const mailgun = new Mailgun(formData); + const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); - 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); + 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) => { - 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 }); - }); + 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 { @@ -180,6 +191,7 @@ 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; @@ -343,55 +355,58 @@ export const createCashfreeOrder = onRequest({ export const verifyCashfreePayment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response: express.Response) => { - try { - const orderId = request.body.order_id || request.query.order_id; + return corsHandler(request, response, async () => { - if (!orderId) { - response.status(400).json({ error: 'Order ID is required' }); - return; - } + try { + const orderId = request.body.order_id || request.query.order_id; - 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 - } + if (!orderId) { + response.status(400).json({ error: 'Order ID is required' }); + return; } - ); - await admin.firestore().collection('payment_orders').doc(orderId).update({ - orderStatus: cashfreeResponse.data.order_status, - paymentDetails: cashfreeResponse.data, - updatedAt: new Date() - }); + const clientId = process.env.CASHFREE_CLIENT_ID; + const clientSecret = process.env.CASHFREE_CLIENT_SECRET; + const isTest = process.env.CASHFREE_ENVIRONMENT !== 'production'; - if (request.headers['x-webhook-source'] === 'cashfree') { - response.status(200).send('OK'); - return; + 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 + }); } - - 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 - }); - } + }); }); From ba77cccd7651552f216c41449ac8988fd5733f5e Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 14:16:58 +0530 Subject: [PATCH 08/10] Fixed some errors and added the function to register client when a owner added a memmb --- .../clientRegistration/clientRegistration.ts | 96 +++++++++++++++++++ functions/src/clientRegistration/index.ts | 1 + functions/src/email/sendEmail.ts | 7 +- .../src/email/sendEmailWithAttachment.ts | 7 +- functions/src/index.ts | 1 + .../src/payments/cashfree/createOrder.ts | 8 +- .../src/payments/cashfree/verifyPayment.ts | 8 +- 7 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 functions/src/clientRegistration/clientRegistration.ts create mode 100644 functions/src/clientRegistration/index.ts diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts new file mode 100644 index 0000000..08dbe19 --- /dev/null +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -0,0 +1,96 @@ +import { onRequest } from "firebase-functions/v2/https"; +import { getAdmin } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; + + +const corsHandler = getCorsHandler(); +const admin = getAdmin(); +export const registerClient = onRequest({ + region: '#{SERVICES_RGN}#' +}, async (req, res) => { + return corsHandler(req, res, async () => { + try { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); + } + + const gymUser = req.body; + if (!gymUser.phoneNumber) { + return res.status(400).json({ error: 'Phone number is required' }); + } + + const isdCode = gymUser.isdCode || '+1'; + const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') + ? gymUser.phoneNumber + : `${isdCode}${gymUser.phoneNumber}`; + + let uid; + try { + const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) + .catch(() => null); + + if (userRecord) { + uid = userRecord.uid; + } else { + const newUser = await admin.auth().createUser({ + phoneNumber: formattedPhoneNumber, + displayName: gymUser.name || '', + email: gymUser.email || null, + }); + uid = newUser.uid; + } + } catch (error) { + console.error('Error creating authentication user:', error); + return res.status(500).json({ + error: 'Failed to create authentication user', + details: error + }); + } + + try { + gymUser.uid = 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_profile').doc(uid).set(clientData); + + return res.status(201).json({ + success: true, + message: 'Client registered successfully', + clientId: uid + }); + } catch (error) { + console.error('Error creating client profile:', error); + + try { + if (!gymUser.uid) { + await admin.auth().deleteUser(uid); + } + } catch (deleteError) { + console.error('Error deleting auth user after failed profile creation:', deleteError); + } + + return res.status(500).json({ + error: 'Failed to create client profile', + details: error + }); + } + } catch (error) { + console.error('Unexpected error in client registration:', error); + return res.status(500).json({ + error: 'Internal server error', + details: error + }); + } + }); +}); diff --git a/functions/src/clientRegistration/index.ts b/functions/src/clientRegistration/index.ts new file mode 100644 index 0000000..1f2445e --- /dev/null +++ b/functions/src/clientRegistration/index.ts @@ -0,0 +1 @@ +export { registerClient } from './clientRegistration'; \ No newline at end of file diff --git a/functions/src/email/sendEmail.ts b/functions/src/email/sendEmail.ts index d7b366f..4200b76 100644 --- a/functions/src/email/sendEmail.ts +++ b/functions/src/email/sendEmail.ts @@ -1,13 +1,14 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../shared/middleware"; -import { logger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { getLogger } from "../shared/config"; import formData from 'form-data'; import Mailgun from 'mailgun.js'; const { convert } = require('html-to-text'); const mailgun = new Mailgun(formData); - +const logger = getLogger(); +const corsHandler = getCorsHandler(); export const sendEmailMessage = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response) => { diff --git a/functions/src/email/sendEmailWithAttachment.ts b/functions/src/email/sendEmailWithAttachment.ts index 545112a..7c1bc74 100644 --- a/functions/src/email/sendEmailWithAttachment.ts +++ b/functions/src/email/sendEmailWithAttachment.ts @@ -4,14 +4,15 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; -import { corsHandler } from "../shared/middleware"; -import { logger } from "../shared/config"; +import { getCorsHandler } from "../shared/middleware"; +import { getLogger } from "../shared/config"; import formData from 'form-data'; import Mailgun from 'mailgun.js'; const { convert } = require('html-to-text'); const mailgun = new Mailgun(formData); - +const logger = getLogger(); +const corsHandler = getCorsHandler(); export const sendEmailWithAttachment = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { diff --git a/functions/src/index.ts b/functions/src/index.ts index cf82272..692183f 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,3 +5,4 @@ export { sendSMSMessage } from './sms'; export { processNotificationOnCreate } from './notifications'; export { createCashfreeLink, createCashfreeOrder, verifyCashfreePayment } from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; +export { registerClient } from './clientRegistration'; \ No newline at end of file diff --git a/functions/src/payments/cashfree/createOrder.ts b/functions/src/payments/cashfree/createOrder.ts index ef29814..6f57b8f 100644 --- a/functions/src/payments/cashfree/createOrder.ts +++ b/functions/src/payments/cashfree/createOrder.ts @@ -1,9 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../../shared/middleware"; -import { admin, logger } from "../../shared/config"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; import axios from "axios"; - +const admin = getAdmin(); +const logger = getLogger(); +const corsHandler = getCorsHandler(); interface CashfreeOrderRequest { amount: number; customerName?: string; diff --git a/functions/src/payments/cashfree/verifyPayment.ts b/functions/src/payments/cashfree/verifyPayment.ts index 1218665..ac5f1c1 100644 --- a/functions/src/payments/cashfree/verifyPayment.ts +++ b/functions/src/payments/cashfree/verifyPayment.ts @@ -1,9 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; -import { corsHandler } from "../../shared/middleware"; -import { admin, logger } from "../../shared/config"; +import { getCorsHandler } from "../../shared/middleware"; +import { getAdmin, getLogger } from "../../shared/config"; import axios from "axios"; - +const logger = getLogger(); +const corsHandler = getCorsHandler(); +const admin = getAdmin(); interface CashfreePaymentResponse { order_status: string; [key: string]: any; From 1f476a3071bb0b2f8e47526d4a77543b61284009 Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 14:46:35 +0530 Subject: [PATCH 09/10] Added the validation using id token and checking if the user is a gym_owner --- .../clientRegistration/clientRegistration.ts | 153 ++++++++++-------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/functions/src/clientRegistration/clientRegistration.ts b/functions/src/clientRegistration/clientRegistration.ts index 08dbe19..8ed51fb 100644 --- a/functions/src/clientRegistration/clientRegistration.ts +++ b/functions/src/clientRegistration/clientRegistration.ts @@ -1,10 +1,11 @@ import { onRequest } from "firebase-functions/v2/https"; -import { getAdmin } from "../shared/config"; import { getCorsHandler } from "../shared/middleware"; - +import { getAdmin, getLogger } from "../shared/config"; const corsHandler = getCorsHandler(); const admin = getAdmin(); +const logger = getLogger(); + export const registerClient = onRequest({ region: '#{SERVICES_RGN}#' }, async (req, res) => { @@ -13,80 +14,104 @@ export const registerClient = onRequest({ if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); } - - const gymUser = req.body; - if (!gymUser.phoneNumber) { - return res.status(400).json({ error: 'Phone number is required' }); + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized. Missing or invalid authorization header.' }); } - - const isdCode = gymUser.isdCode || '+1'; - const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') - ? gymUser.phoneNumber - : `${isdCode}${gymUser.phoneNumber}`; - - let uid; + const idToken = authHeader.split('Bearer ')[1]; try { - const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) - .catch(() => null); + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + const userDoc = await admin.firestore().collection('users').doc(uid).get(); - if (userRecord) { - uid = userRecord.uid; - } else { - const newUser = await admin.auth().createUser({ - phoneNumber: formattedPhoneNumber, - displayName: gymUser.name || '', - email: gymUser.email || null, + 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.phoneNumber) { + return res.status(400).json({ error: 'Phone number is required' }); + } + + const isdCode = gymUser.isdCode || '91'; + const formattedPhoneNumber = gymUser.phoneNumber.startsWith('+') + ? gymUser.phoneNumber + : `${isdCode}${gymUser.phoneNumber}`; + + let clientUid; + try { + const userRecord = await admin.auth().getUserByPhoneNumber(formattedPhoneNumber) + .catch(() => null); + + if (userRecord) { + clientUid = userRecord.uid; + } else { + const newUser = await admin.auth().createUser({ + phoneNumber: formattedPhoneNumber, + displayName: gymUser.name || '', + email: gymUser.email || null, + }); + 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 }); - uid = newUser.uid; } - } catch (error) { - console.error('Error creating authentication user:', error); - return res.status(500).json({ - error: 'Failed to create authentication user', - details: error - }); - } - - try { - gymUser.uid = 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_profile').doc(uid).set(clientData); - - return res.status(201).json({ - success: true, - message: 'Client registered successfully', - clientId: uid - }); - } catch (error) { - console.error('Error creating client profile:', error); try { - if (!gymUser.uid) { - await admin.auth().deleteUser(uid); + gymUser.uid = clientUid; + gymUser.registeredBy = uid; + + if (gymUser.name) { + gymUser.normalizedName = gymUser.name.toLowerCase(); } - } catch (deleteError) { - console.error('Error deleting auth user after failed profile creation:', deleteError); + + if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { + gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); + } + + const clientData = { + ...gymUser, + phoneNumber: formattedPhoneNumber, + }; + + await admin.firestore().collection('client_profile').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 + }); } - 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) { - console.error('Unexpected error in client registration:', error); + logger.error('Unexpected error in client registration:', error); return res.status(500).json({ error: 'Internal server error', details: error From 47851da81b460775f1d1c799cc708dc826f6910a Mon Sep 17 00:00:00 2001 From: DhanshCOSQ Date: Mon, 21 Apr 2025 16:28:03 +0530 Subject: [PATCH 10/10] changed name from corseHandler to getCorseHander in place details function --- functions/src/places/details.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/places/details.ts b/functions/src/places/details.ts index 41dbf64..4eea0a3 100644 --- a/functions/src/places/details.ts +++ b/functions/src/places/details.ts @@ -3,7 +3,7 @@ import { Request } from "firebase-functions/v2/https"; import * as express from "express"; import axios from "axios"; -const corsHandler = require('../shared/middleware').corsHandler; +const corsHandler = require('../shared/middleware').getcorsHandler(); const logger = require('../shared/config').getLogger();