diff --git a/.github/workflows/deploy-to-dev.yml b/.github/workflows/deploy-to-dev.yml index d8e00fb..bbf97e3 100644 --- a/.github/workflows/deploy-to-dev.yml +++ b/.github/workflows/deploy-to-dev.yml @@ -25,6 +25,7 @@ jobs: run: | echo "JWT_SECRET=\"${{ secrets.DEV_JWT_SECRET }}\"" >> .env echo "DATABASE_URL=\"${{ secrets.DEV_DATABASE_URL }}\"" >> .env + echo "CRYPTO_SECRET_KEY=\"${{ secrets.DEV_CRYPTO_SECRET_KEY }}\"" >> .env echo "ACCESS_CODE_EXPIRY=\"1y\"" >> .env echo "ENV=\"development\"" >> .env diff --git a/.github/workflows/deploy-to-prod.yml b/.github/workflows/deploy-to-prod.yml index 1447075..efca0ad 100644 --- a/.github/workflows/deploy-to-prod.yml +++ b/.github/workflows/deploy-to-prod.yml @@ -25,6 +25,7 @@ jobs: run: | echo "JWT_SECRET=\"${{ secrets.PRODUCTION_JWT_SECRET }}\"" >> .env echo "DATABASE_URL=\"${{ secrets.PRODUCTION_DATABASE_URL }}\"" >> .env + echo "CRYPTO_SECRET_KEY=\"${{ secrets.PRODUCTION_CRYPTO_SECRET_KEY }}\"" >> .env echo "FIREBASE_WEB_API_KEY=\"${{ secrets.FIREBASE_WEB_API_KEY }}\"" >> .env echo "FIREBASE_PROJECT_ID=\"${{ secrets.FIREBASE_PROJECT_ID }}\"" >> .env echo "FIREBASE_CLIENT_EMAIL=\"${{ secrets.FIREBASE_CLIENT_EMAIL }}\"" >> .env diff --git a/.github/workflows/deploy-to-staging.yml b/.github/workflows/deploy-to-staging.yml index 601e18c..7542776 100644 --- a/.github/workflows/deploy-to-staging.yml +++ b/.github/workflows/deploy-to-staging.yml @@ -25,6 +25,7 @@ jobs: run: | echo "JWT_SECRET=\"${{ secrets.STAGING_JWT_SECRET }}\"" >> .env echo "DATABASE_URL=\"${{ secrets.STAGING_DATABASE_URL }}\"" >> .env + echo "CRYPTO_SECRET_KEY=\"${{ secrets.STAGING_CRYPTO_SECRET_KEY }}\"" >> .env echo "FIREBASE_WEB_API_KEY=\"${{ secrets.FIREBASE_WEB_API_KEY }}\"" >> .env echo "FIREBASE_PROJECT_ID=\"${{ secrets.FIREBASE_PROJECT_ID }}\"" >> .env echo "FIREBASE_CLIENT_EMAIL=\"${{ secrets.FIREBASE_CLIENT_EMAIL }}\"" >> .env diff --git a/package-lock.json b/package-lock.json index 27a28cd..47cb147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,13 +22,14 @@ "bcrypt": "^5.1.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", + "cryptojs": "^2.5.3", "firebase-admin": "^12.1.0", + "mern-task-backend": "file:", "mongoose": "^8.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.1", - "rxjs": "^7.8.1", - "twilio": "^4.20.0" + "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.3.0", @@ -3432,30 +3433,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "devOptional": true }, "node_modules/babel-jest": { "version": "29.7.0", @@ -4109,6 +4088,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4346,10 +4326,13 @@ "node": ">= 8" } }, - "node_modules/dayjs": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + "node_modules/cryptojs": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/cryptojs/-/cryptojs-2.5.3.tgz", + "integrity": "sha512-+rdPl1UCxE8s3R94NNn+zMKOiI4MJ7dyh3X0c5uBL3btDr4zQ6acd7f9mY7Wb5MrccZEi2Rrha3OEtLcc5XXog==", + "engines": { + "node": "*" + } }, "node_modules/debug": { "version": "4.3.5", @@ -4450,6 +4433,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -5469,25 +5453,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -7670,6 +7635,10 @@ "node": ">= 8" } }, + "node_modules/mern-task-backend": { + "resolved": "", + "link": true + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -8707,11 +8676,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -8759,11 +8723,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8924,11 +8883,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9210,11 +9164,6 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -10344,24 +10293,6 @@ "node": "*" } }, - "node_modules/twilio": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.23.0.tgz", - "integrity": "sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==", - "dependencies": { - "axios": "^1.6.0", - "dayjs": "^1.11.9", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.0", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "url-parse": "^1.5.9", - "xmlbuilder": "^13.0.2" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10497,15 +10428,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10831,14 +10753,6 @@ } } }, - "node_modules/xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", - "engines": { - "node": ">=6.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 1e0d26f..1a1b93b 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,14 @@ "bcrypt": "^5.1.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", + "cryptojs": "^2.5.3", "firebase-admin": "^12.1.0", + "mern-task-backend": "file:", "mongoose": "^8.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.1", - "rxjs": "^7.8.1", - "twilio": "^4.20.0" + "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.3.0", diff --git a/sample.env b/sample.env index c83f407..ed17167 100644 --- a/sample.env +++ b/sample.env @@ -2,8 +2,5 @@ JWT_SECRET= PORT=3000 DATABASE_URL=mongodb+srv:// ACCESS_CODE_EXPIRY=1y -TWILIO_ACCOUNT_SID= -TWILIO_AUTH_TOKEN= -TWILIO_VERIFICATION_SID= -TWILIO_PHONE_NUMBER= -ENV=development \ No newline at end of file +ENV=development +CRYPTO_SECRET_KEY=(32 digit hex) \ No newline at end of file diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7350175..2c23a7b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -3,7 +3,6 @@ import { JwtService } from '@nestjs/jwt'; import { UsersService } from 'src/users/users.service'; import * as bcrypt from 'bcrypt'; import { generalError } from 'src/utils/generalError'; -import Twilio from 'twilio'; import getCountryCodeAndPhoneNumber from 'src/utils/getCountryCodeAndPhoneNumber'; import { verifyToken } from 'src/utils/firebase'; @@ -89,42 +88,6 @@ export class AuthService { } } - async verifyOTPAndEditPhoneNumber(userId, otpBody, response) { - const { phoneNumber, countryCode, otp } = otpBody; - - if (process.env.ENV === 'production') { - await this.verifyTwilioOTP(phoneNumber, countryCode, otp, response); - } - - const newUser = await this.userService.editUser(userId, { - phoneNumber, - countryCode, - }); - response.json(newUser); - } - - async verifyTwilioOTP(phoneNumber, countryCode, otp, response) { - const client = Twilio( - process.env.TWILIO_ACCOUNT_SID, - process.env.TWILIO_AUTH_TOKEN, - ); - - const verificationToken = process.env.TWILIO_VERIFICATION_SID; - - const verificationCheck = await client.verify.v2 - .services(verificationToken) - .verificationChecks.create({ - to: `+${countryCode}${phoneNumber}`, - code: otp, - }); - - const { status } = verificationCheck; - - if (status !== 'approved') { - return response.json({ status }); - } - } - async verifyUser(login) { const user = await this.userService.findByEmail(login.email); if (!user) { diff --git a/src/group/group.controller.ts b/src/group/group.controller.ts index 1aee78b..d2e020f 100644 --- a/src/group/group.controller.ts +++ b/src/group/group.controller.ts @@ -53,9 +53,9 @@ export class GroupController { } @Post(':id/join') - joinGroup(@Req() req: Request, @Param('id') groupId) { + joinGroup(@Req() req: Request, @Param('id') hashedGroupId) { const { id } = req['user']; - return this.groupService.joinGroup(groupId, new Types.ObjectId(id)); + return this.groupService.joinGroup(hashedGroupId, new Types.ObjectId(id)); } @Post(':id/chat') @@ -77,4 +77,9 @@ export class GroupController { getAllTransactions(@Param('id') groupId) { return this.groupService.getAllTransactions(groupId); } + + @Get(':id/token') + async getGroupToken(@Param('id') groupId){ + return this.groupService.generateToken(groupId); + } } diff --git a/src/group/group.module.ts b/src/group/group.module.ts index aaf21a6..ccc7523 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -19,6 +19,8 @@ import { ChatService } from 'src/chat/chat.service'; import { ChatModule } from 'src/chat/chat.module'; import ChatSchema from 'src/chat/chat.schema'; import GroupBalanceSchema from 'src/balance/groupBalance.schema'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ TransactionModule, @@ -34,7 +36,18 @@ import GroupBalanceSchema from 'src/balance/groupBalance.schema'; ChatSchema, GroupBalanceSchema, ]), + + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => { + return { + secret: config.get('JWT_SECRET'), + }; + }, + inject: [ConfigService], + }), ], + controllers: [GroupController, UsersController], providers: [ GroupService, diff --git a/src/group/group.service.ts b/src/group/group.service.ts index a3db4d3..66b76ff 100644 --- a/src/group/group.service.ts +++ b/src/group/group.service.ts @@ -10,6 +10,8 @@ import { TransactionService } from 'src/transaction/transaction.service'; import { UsersService } from 'src/users/users.service'; import { ChatService } from 'src/chat/chat.service'; import { ActivityFeedService } from 'src/activity-feed/activity-feed.service'; +import { JwtService } from '@nestjs/jwt'; +import { decrypt, encrypt } from 'src/utils/cipher'; @Injectable() export class GroupService { constructor( @@ -19,6 +21,7 @@ export class GroupService { private userService: UsersService, private chatService: ChatService, private activityFeedService: ActivityFeedService, + private jwtService: JwtService ) {} async create(createGroupDto) { const { members, name, phoneNumbers } = createGroupDto; @@ -119,7 +122,11 @@ export class GroupService { ); } - async joinGroup(groupId, userId) { + async joinGroup(hashedGroupId, userId) { + const decodedGroupId = this.jwtService.verify(hashedGroupId); + const groupId = decrypt(decodedGroupId.groupId); + + const group = await this.groupModel.findById(groupId).exec(); if (!group) { @@ -247,4 +254,24 @@ export class GroupService { async getAllTransactions(groupId) { return this.transactionService.getTransactionsByGroupId(groupId); } + + async generateToken(groupId){ + const group = await this.groupModel.findById(groupId).populate('members').exec(); + if (!group) { + throw new Error('Group not found'); + } + + const hashedGroupId = encrypt(groupId); + + + const payload = { + groupId:hashedGroupId, + name: group.name, + memberCount: group.members.length + }; + + return this.jwtService.sign(payload) + } + + } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 8f3e37d..f7bf1a6 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import UserSchema from './users.schema'; -import Twilio from 'twilio'; import GroupSchema from 'src/group/group.schema'; + @Injectable() export class UsersService { constructor( @@ -65,8 +65,6 @@ export class UsersService { } ); } - - async createUsersAndGetIds(phoneNumbers) { let userIds = []; @@ -86,7 +84,6 @@ export class UsersService { }); user = await newUser.save(); - this.sendSMS(phoneNumbers, countryCode, 'Join our app'); } userIds.push(user._id); @@ -113,7 +110,6 @@ export class UsersService { }); user = await newUser.save(); - this.sendSMS(phoneNumbers, countryCode, 'Join our app'); } users.push(user); @@ -122,20 +118,6 @@ export class UsersService { return users; } - async sendSMS(phoneNumber, countryCode, message) { - if (process.env.ENV == 'production') { - const client = Twilio( - process.env.TWILIO_ACCOUNT_SID, - process.env.TWILIO_AUTH_TOKEN, - ); - client.messages.create({ - body: message, // Text of the SMS - to: '+' + countryCode + phoneNumber, - from: process.env.TWILIO_PHONE_NUMBER, // Replace with a valid Twilio number - }); - } - } - async findById(id) { return await this.userModel.findById(id).exec(); } diff --git a/src/utils/cipher.ts b/src/utils/cipher.ts new file mode 100644 index 0000000..1588e2e --- /dev/null +++ b/src/utils/cipher.ts @@ -0,0 +1,21 @@ +import * as crypto from 'crypto'; + +const algorithm = 'aes-256-cbc'; +const iv = crypto.randomBytes(16); + +export function encrypt(text: string): string { + const cipher = crypto.createCipheriv(algorithm, Buffer.from(process.env.CRYPTO_SECRET_KEY), iv); + let encrypted = cipher.update(text); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return iv.toString('hex') + ':' + encrypted.toString('hex'); +} + +export function decrypt(text: string): string { + const textParts = text.split(':'); + const iv = Buffer.from(textParts.shift(), 'hex'); + const encryptedText = Buffer.from(textParts.join(':'), 'hex'); + const decipher = crypto.createDecipheriv(algorithm, Buffer.from(process.env.CRYPTO_SECRET_KEY), iv); + let decrypted = decipher.update(encryptedText); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString(); +}