diff --git a/fitlien-services-pipeline.yaml b/fitlien-services-pipeline.yaml new file mode 100644 index 0000000..48a4039 --- /dev/null +++ b/fitlien-services-pipeline.yaml @@ -0,0 +1,83 @@ +trigger: + - master + +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) + +steps: + - task: PowerShell@2 + displayName: "Setting build version" + inputs: + targetType: "inline" + script: | + Write-Host "##vso[build.updatebuildnumber]${{ parameters.buildNumber }}" + + - task: NodeTool@0 + displayName: "Install Node" + inputs: + version: "20" + + - task: Npm@1 + displayName: "npm install" + inputs: + command: "install" + + - task: PowerShell@2 + displayName: "Set version in package.json" + inputs: + targetType: "inline" + script: | + $pkg = Get-Content -Path "$(System.DefaultWorkingDirectory)/functions/package.json" -Raw | ConvertFrom-Json + $pkg.version = "${{ parameters.buildNumber }}" + $pkg | ConvertTo-Json -Depth 100 | Set-Content -Path "$(System.DefaultWorkingDirectory)/functions/package.json" + + - task: CmdLine@2 + displayName: "Copy .env.example to .env" + inputs: + script: | + cp "$(System.DefaultWorkingDirectory)/functions/.env.example" "$(System.DefaultWorkingDirectory)/functions/.env" + + - task: ReplaceTokens@3 + displayName: "Replace tokens in .env file" + inputs: + targetFiles: "$(System.DefaultWorkingDirectory)/functions/.env" + tokenPrefix: "#{" + tokenSuffix: "}#" + + - task: Npm@1 + displayName: "npm run build" + inputs: + command: "custom" + workingDir: "$(System.DefaultWorkingDirectory)/functions" + customCommand: "run build" + + - task: DeleteFiles@1 + displayName: "Remove node_modules, *.log files, src directory from functions directory" + inputs: + SourceFolder: "$(System.DefaultWorkingDirectory)/functions" + Contents: | + node_modules/** + *.log + src/** + + - task: ArchiveFiles@2 + displayName: "Archive functions directory" + inputs: + rootFolderOrFile: "$(System.DefaultWorkingDirectory)/functions" + includeRootFolder: false + archiveFile: "$(System.DefaultWorkingDirectory)/fitlien-services-$(buildNumber).zip" + compression: "zip" + + - task: CopyFiles@2 + displayName: "Copy archive to staging directory" + inputs: + SourceFolder: "$(System.DefaultWorkingDirectory)" + Contents: "fitlien-services-$(buildNumber).zip" + TargetFolder: "$(System.ArtifactsDirectory)" diff --git a/functions/.env.example b/functions/.env.example new file mode 100644 index 0000000..ed58607 --- /dev/null +++ b/functions/.env.example @@ -0,0 +1,6 @@ +MAILGUN_API_KEY=#{MAILGUN_API_KEY}# +MAILGUN_SERVER=#{MAILGUN_SERVER}# +MAILGUN_FROM_ADDRESS=#{MAILGUN_FROM_ADDRESS}# +TWILIO_ACCOUNT_SID=AC5cfaae728ba68fb1aa6756d973b6e32b +TWILIO_AUTH_TOKEN=886ed704c7918078f361f5f88b42ffc0 +TWILIO_PHONE_NUMBER=+12315005309 diff --git a/functions/package-lock.json b/functions/package-lock.json index a27a770..1165bbe 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -1,15 +1,18 @@ { "name": "functions", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "functions", + "version": "0.0.0", "dependencies": { "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", "html-to-text": "^9.0.5", + "long": "4.0.0", "mailgun.js": "^10.4.0", "twilio": "^5.4.0" }, @@ -765,6 +768,12 @@ "node": ">=6" } }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "optional": true + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2743,9 +2752,9 @@ } }, "node_modules/firebase-functions": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.2.0.tgz", - "integrity": "sha512-vfyyVHS8elxplzEQ9To+NaINRPFUsDasQrasTa2eFJBYSPzdhkw6rwLmvwyYw622+ze+g4sDIb14VZym+afqXQ==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz", + "integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==", "dependencies": { "@types/cors": "^2.8.5", "@types/express": "^4.17.21", @@ -4425,9 +4434,9 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -5035,6 +5044,11 @@ "node": ">=12.0.0" } }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/functions/package.json b/functions/package.json index 75380f3..6ad5eae 100644 --- a/functions/package.json +++ b/functions/package.json @@ -1,5 +1,6 @@ { "name": "functions", + "version": "0.0.0", "scripts": { "build": "tsc", "build:watch": "tsc --watch", @@ -18,6 +19,7 @@ "firebase-functions": "^6.0.1", "form-data": "^4.0.1", "html-to-text": "^9.0.5", + "long": "4.0.0", "mailgun.js": "^10.4.0", "twilio": "^5.4.0" }, diff --git a/functions/src/index.ts b/functions/src/index.ts index 48594d4..3a9f80c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,11 +1,16 @@ import { onRequest } from "firebase-functions/v2/https"; +import { Request } from "firebase-functions/v2/https"; +import { onDocumentCreated } from 'firebase-functions/v2/firestore'; +import * as admin from 'firebase-admin'; +import * as express from "express"; import * as logger from "firebase-functions/logger"; + const formData = require('form-data'); const Mailgun = require('mailgun.js'); const { convert } = require('html-to-text'); const twilio = require('twilio') -export const sendEmail = onRequest((request, response) => { +export const sendEmail = onRequest((request: Request, response: express.Response) => { const mailgun = new Mailgun(formData); const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); @@ -32,11 +37,9 @@ export const sendEmail = onRequest((request, response) => { }); }); -export const sendSMS = onRequest((request, response) => { +export const sendSMS = onRequest((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, @@ -52,3 +55,81 @@ export const sendSMS = onRequest((request, response) => { response.status(500).json({ success: false, error: error.message }); }); }); + + +interface Invitation { + email: string; + phoneNumber: string; + gymName: string; + invitedByName: string; +} + +export const sendInvitationNotification = onDocumentCreated( + 'invitations/{invitationId}', + async (event) => { + 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; + } + } +); \ No newline at end of file diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 57b915f..2924892 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "module": "NodeNext", "esModuleInterop": true, - "moduleResolution": "nodenext", + "allowSyntheticDefaultImports": true, + "moduleResolution": "node16", "noImplicitReturns": true, "noUnusedLocals": true, "outDir": "lib",