From e0656439d7cf62343b4eab1c7329257c747eea92 Mon Sep 17 00:00:00 2001 From: cannonerd_6002 Date: Thu, 15 Jan 2026 07:08:51 +0530 Subject: [PATCH 1/5] feat: implement 60s backend rate-limiting for OTP requests (#140) --- appwrite.json | 8 ++++++++ functions/send-otp/src/appwrite.js | 32 +++++++++++++++++++++++++++++- functions/send-otp/src/main.js | 31 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/appwrite.json b/appwrite.json index 7569a7d..92e5bfa 100644 --- a/appwrite.json +++ b/appwrite.json @@ -98,6 +98,14 @@ { "key": "OTP_COLLECTION_ID", "value": "64a7bfe3a3a7ee5bf7f8" + }, + { + "key": "UserDataDatabaseID", + "value": "64a1319104a149e16f5c" + }, + { + "key": "UsersCollectionID", + "value": "64a52f0a6c41ded09def" } ], "execute": [ diff --git a/functions/send-otp/src/appwrite.js b/functions/send-otp/src/appwrite.js index b65dcc8..6f52d37 100644 --- a/functions/send-otp/src/appwrite.js +++ b/functions/send-otp/src/appwrite.js @@ -1,4 +1,4 @@ -import { Client, Databases } from 'node-appwrite'; +import { Client, Databases, Query } from 'node-appwrite'; class AppwriteService { constructor() { @@ -23,6 +23,36 @@ class AppwriteService { } ); } + + async getUserByEmail(email) { + try { + const response = await this.databases.listDocuments( + process.env.UserDataDatabaseID, + process.env.UsersCollectionID, + [Query.equal('email', email), Query.limit(1)] + ); + return response.documents.length > 0 ? response.documents[0] : null; + } catch (e) { + // Return null if user not found or any error occurs + return null; + } + } + + async updateUserLastOtpSent(userId, timestamp) { + try { + await this.databases.updateDocument( + process.env.UserDataDatabaseID, + process.env.UsersCollectionID, + userId, + { + last_otp_sent: timestamp + } + ); + } catch (e) { + // If update fails, log but don't throw (non-critical) + throw e; + } + } } export default AppwriteService; diff --git a/functions/send-otp/src/main.js b/functions/send-otp/src/main.js index f4a95c0..29da501 100644 --- a/functions/send-otp/src/main.js +++ b/functions/send-otp/src/main.js @@ -9,6 +9,8 @@ export default async ({ req, res, log, error }) => { "OTP_COLLECTION_ID", "SENDER_MAIL", "SENDER_PASSWORD", + "UserDataDatabaseID", + "UsersCollectionID", ]); const appwrite = new AppwriteService(); @@ -18,6 +20,25 @@ export default async ({ req, res, log, error }) => { log(req.body); const { otpID, email: recipientEmail } = JSON.parse(req.body); + // Rate limit check: Check if user has requested OTP in the last 60 seconds + const userDoc = await appwrite.getUserByEmail(recipientEmail); + + if (userDoc && userDoc.last_otp_sent) { + const lastOtpSentTime = new Date(userDoc.last_otp_sent).getTime(); + const currentTime = Date.now(); + const timeDifference = (currentTime - lastOtpSentTime) / 1000; // Convert to seconds + + if (timeDifference < 60) { + const remainingSeconds = Math.ceil(60 - timeDifference); + return res.json( + { + message: `Too many requests. Please wait ${remainingSeconds} second(s) before requesting another OTP.` + }, + 429 + ); + } + } + const otp = String(Math.floor(100000 + Math.random() * 900000)); await mailService.sendMail(recipientEmail, otp); @@ -27,6 +48,16 @@ export default async ({ req, res, log, error }) => { log(`Current Date: ${currentDate}`); await appwrite.createOtpDocument(otpID, otp, currentDate); + + // Update last_otp_sent timestamp in user document + if (userDoc) { + try { + await appwrite.updateUserLastOtpSent(userDoc.$id, new Date().toISOString()); + } catch (updateError) { + // Log error but don't fail the request if timestamp update fails + log(`Warning: Failed to update last_otp_sent timestamp: ${updateError}`); + } + } } catch (e) { error(String(e)); return res.json({ message: String(e) }); From a734249c999aaca95309732ac79488433251d762 Mon Sep 17 00:00:00 2001 From: cannonerd_6002 Date: Thu, 15 Jan 2026 19:47:33 +0530 Subject: [PATCH 2/5] feat: implement 60-second rate limiting for OTP requests --- appwrite.json | 2106 +------------------ functions/send-otp-backup/README.md | 86 + functions/send-otp-backup/package-lock.json | 141 ++ functions/send-otp-backup/package.json | 18 + functions/send-otp-backup/src/appwrite.js | 58 + functions/send-otp-backup/src/mail.js | 25 + functions/send-otp-backup/src/main.js | 67 + functions/send-otp-backup/src/utils.js | 11 + functions/send-otp/.gitignore | 133 ++ functions/send-otp/.prettierrc.json | 6 + functions/send-otp/README.md | 92 +- functions/send-otp/package-lock.json | 127 +- functions/send-otp/package.json | 33 +- functions/send-otp/src/appwrite.js | 4 +- functions/send-otp/src/main.js | 5 +- 15 files changed, 618 insertions(+), 2294 deletions(-) create mode 100644 functions/send-otp-backup/README.md create mode 100644 functions/send-otp-backup/package-lock.json create mode 100644 functions/send-otp-backup/package.json create mode 100644 functions/send-otp-backup/src/appwrite.js create mode 100644 functions/send-otp-backup/src/mail.js create mode 100644 functions/send-otp-backup/src/main.js create mode 100644 functions/send-otp-backup/src/utils.js create mode 100644 functions/send-otp/.gitignore create mode 100644 functions/send-otp/.prettierrc.json diff --git a/appwrite.json b/appwrite.json index 92e5bfa..8c25af5 100644 --- a/appwrite.json +++ b/appwrite.json @@ -1,2121 +1,25 @@ { - "projectId": "resonate", - "projectName": "Resonate", + "projectId": "6968ad380036f9106f75", "functions": [ { - "$id": "65368a58ef47cf6861200", - "name": "Upcoming Rooms Time Checker", + "$id": "6968b4f000222d5d8d93", + "name": "send-otp", "runtime": "node-16.0", - "path": "functions/upcomingRoom-isTime-checker", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "UpcomingRoomsDataBaseID", - "value": "6522fcf27a1bbc4238df" - }, - { - "key": "UpcomingRoomsCollectionID", - "value": "6522fd163103bd453183" - }, - { - "key": "SubscriberCollectionID", - "value": "6522fd267db6fdad3392" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "*/5 * * * *", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "65368a58ef47cf6861206", - "name": "Upcoming Rooms Message Notification", - "runtime": "node-16.0", - "path": "functions/upcomingRoom-Message-Notification", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "UpcomingRoomsDataBaseID", - "value": "6522fcf27a1bbc4238df" - }, - { - "key": "UpcomingRoomsCollectionID", - "value": "6522fd163103bd453183" - }, - { - "key": "SubscriberCollectionID", - "value": "6522fd267db6fdad3392" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "6513e9d40b57c6ec156f", - "name": "Send OTP", - "runtime": "node-16.0", - "path": "functions/send-otp", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "SENDER_MAIL", - "value": "aossieorgforu@gmail.com" - }, - { - "key": "VERIFICATION_DATABASE_ID", - "value": "64a7bfd6b09121548bfe" - }, - { - "key": "SENDER_PASSWORD", - "value": "xizkyvzhduejxjel" - }, - { - "key": "OTP_COLLECTION_ID", - "value": "64a7bfe3a3a7ee5bf7f8" - }, - { - "key": "UserDataDatabaseID", - "value": "64a1319104a149e16f5c" - }, - { - "key": "UsersCollectionID", - "value": "64a52f0a6c41ded09def" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "6513df34a0de595ccfb3", - "name": "Verify Email", - "runtime": "node-16.0", - "path": "functions/verify-email", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "651303df122abc151bf3", - "name": "Verify OTP", - "runtime": "node-16.0", - "path": "functions/verify-otp", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "VERIFY_COLLECTION_ID", - "value": "64a7c0100eabfe8d3844" - }, - { - "key": "OTP_COLLECTION_ID", - "value": "64a7bfe3a3a7ee5bf7f8" - }, - { - "key": "VERIFICATION_DATABASE_ID", - "value": "64a7bfd6b09121548bfe" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "651e2670b1e4a26e3cf1", - "name": "Create Room", - "runtime": "node-16.0", - "path": "functions/create-room", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "MASTER_DATABASE_ID", - "value": "64a521785f5be62b796f" - }, - { - "key": "ROOMS_COLLECTION_ID", - "value": "64a5217e695bf2c4ec9c" - } - ], + "specification": "s-0.5vcpu-512mb", "execute": [ "any" ], "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "6534b7949d12c9867bd2", - "name": "Database Cleaner", - "runtime": "node-16.0", - "path": "functions/database-cleaner", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "OTP_COLLECTION_ID", - "value": "64a7bfe3a3a7ee5bf7f8" - }, - { - "key": "VERIFICATION_DATABASE_ID", - "value": "64a7bfd6b09121548bfe" - }, - { - "key": "RETENTION_PERIOD_DAYS", - "value": "0" - }, - { - "key": "PARTICIPANTS_COLLECTION_ID", - "value": "64a63e508145d1084abf" - }, - { - "key": "ROOMS_COLLECTION_ID", - "value": "64a5217e695bf2c4ec9c" - }, - { - "key": "ACTIVE_PAIRS_COLLECTION_ID", - "value": "64d980cd65ff2e08ab97" - }, - { - "key": "MASTER_DATABASE_ID", - "value": "64a521785f5be62b796f" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "0 4 * * *", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "651e348775f28d84e11e", - "name": "Delete Room", - "runtime": "node-16.0", - "path": "functions/delete-room", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "PARTICIPANTS_COLLECTION_ID", - "value": "64a63e508145d1084abf" - }, - { - "key": "ROOMS_COLLECTION_ID", - "value": "64a5217e695bf2c4ec9c" - }, - { - "key": "MASTER_DATABASE_ID", - "value": "64a521785f5be62b796f" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "651e3d8fa35c690ed957", - "name": "Join Room", - "runtime": "node-16.0", - "path": "functions/join-room", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "6513ed3a0ac8808c33e4", - "name": "Livekit Webhook", - "runtime": "node-16.0", - "path": "functions/livekit-webhook", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "ROOMS_COLLECTION_ID", - "value": "64a5217e695bf2c4ec9c" - }, - { - "key": "PARTICIPANTS_COLLECTION_ID", - "value": "64a63e508145d1084abf" - }, - { - "key": "MASTER_DATABASE_ID", - "value": "64a521785f5be62b796f" - } - ], - "execute": [ - "any" - ], - "events": [], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "6513f1648a9654b77b7c", - "name": "Match Maker", - "runtime": "node-16.0", - "path": "functions/match-maker", - "entrypoint": "src/main.js", - "ignore": [ - "node_modules", - ".npm" - ], - "vars": [ - { - "key": "ACTIVE_PAIRS_COLLECTION_ID", - "value": "64d980cd65ff2e08ab97" - }, - { - "key": "DATABASE_ID", - "value": "64a521785f5be62b796f" - }, - { - "key": "REQUESTS_COLLECTION_ID", - "value": "64d980211f1395263ebe" - }, - { - "key": "APPWRITE_FUNCTION_PROJECT_ID", - "value": "resonate" - } - ], - "execute": [ - "any" - ], - "events": [ - "databases.64a521785f5be62b796f.collections.64d980211f1395263ebe.documents.*.create" - ], - "schedule": "", - "timeout": 100, - "commands": "npm install", - "enabled": true, - "logging": true, - "scopes": [] - }, - { - "$id": "68b241f500012870fca3", - "vars": [ - { - "key": "UserDataDatabaseID", - "value": "64a1319104a149e16f5c" - }, - { - "key": "UsersCollectionID", - "value": "64a52f0a6c41ded09def" - } - ], - "execute": [ - "any" - ], - "name": "Send Story Notifications", - "enabled": true, - "logging": true, - "runtime": "node-16.0", - "scopes": [ - "sessions.write", - "users.read", - "users.write", - "teams.read", - "teams.write", - "databases.read", - "databases.write", - "collections.read", - "collections.write", - "attributes.read", - "attributes.write", - "indexes.read", - "indexes.write", - "documents.read", - "documents.write" - ], - "events": [], - "schedule": "", - "timeout": 15, - "entrypoint": "src/main.js", - "commands": "npm install", - "path": "functions/send-story-notification" - }, - { - "$id": "68b76fe00027c243610e", - "execute": [ - "any" - ], - "name": "Start Friend Call", - "enabled": true, - "logging": true, - "runtime": "node-16.0", "scopes": [ "users.read" ], - "events": [], "schedule": "", "timeout": 15, - "entrypoint": "src/main.js", - "commands": "npm install", - "path": "functions/start-friend-call" - }, - { - "$id": "68c2c4e20013c58af2c9", - "execute": [ - "any" - ], - "name": "Sync Stories with Meilisearch", "enabled": true, "logging": true, - "runtime": "node-16.0", - "scopes": [ - "databases.read", - "collections.read", - "documents.read" - ], - "events": [ - "databases.stories.collections.670259e900321c12a5a2.documents.*.create", - "databases.stories.collections.670259e900321c12a5a2.documents.*.delete", - "databases.stories.collections.670259e900321c12a5a2.documents.*.update" - ], - "schedule": "", - "timeout": 15, "entrypoint": "src/main.js", "commands": "npm install", - "path": "functions/sync-stories-with-meilisearch" - }, - { - "$id": "68c2ddc8001dded982ef", - "execute": [ - "any" - ], - "name": "Sync Users with Meilisearch", - "enabled": true, - "logging": true, - "runtime": "node-16.0", - "scopes": [ - "databases.read", - "collections.read", - "documents.read" - ], - "events": [ - "databases.64a1319104a149e16f5c.collections.64a52f0a6c41ded09def.documents.*.create", - "databases.64a1319104a149e16f5c.collections.64a52f0a6c41ded09def.documents.*.delete", - "databases.64a1319104a149e16f5c.collections.64a52f0a6c41ded09def.documents.*.update" - ], - "schedule": "", - "timeout": 15, - "entrypoint": "src/main.js", - "commands": "npm install", - "path": "functions/sync-users-with-meilisearch" - }, - { - "$id": "68c3d37e003118410c75", - "execute": [ - "any" - ], - "name": "Sync All Documents with Meilisearch", - "enabled": true, - "logging": true, - "runtime": "node-16.0", - "scopes": [ - "databases.read", - "collections.read", - "documents.read" - ], - "events": [], - "schedule": "", - "timeout": 15, - "entrypoint": "src/main.js", - "commands": "npm install", - "path": "functions/sync-all-documents-with-meilisearch" - } - ], - "buckets": [ - { - "$id": "64a13095a4c87fd78bc6", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "fileSecurity": false, - "name": "assets", - "enabled": true, - "maximumFileSize": 29000000, - "allowedFileExtensions": [], - "compression": "none", - "encryption": true, - "antivirus": true - }, - { - "$id": "64a52ab306331f167ce6", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "fileSecurity": false, - "name": "user-images", - "enabled": true, - "maximumFileSize": 29000000, - "allowedFileExtensions": [], - "compression": "none", - "encryption": true, - "antivirus": true - }, - { - "$id": "6703f4c70037edfd8429", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "fileSecurity": false, - "name": "stories", - "enabled": true, - "maximumFileSize": 30000000, - "allowedFileExtensions": [], - "compression": "none", - "encryption": true, - "antivirus": true - } - ], - "databases": [ - { - "$id": "64a1319104a149e16f5c", - "name": "user-data", - "enabled": true - }, - { - "$id": "64a521785f5be62b796f", - "name": "master", - "enabled": true - }, - { - "$id": "64a7bfd6b09121548bfe", - "name": "verification", - "enabled": true - }, - { - "$id": "6522fcf27a1bbc4238df", - "name": "upcoming-rooms", - "enabled": true - }, - { - "$id": "stories", - "name": "stories", - "enabled": true - } - ], - "collections": [ - { - "$id": "64a131980b5388c2a0af", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a1319104a149e16f5c", - "name": "usernames", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "email", - "type": "string", - "required": true, - "array": false, - "format": "email", - "default": null - } - ], - "indexes": [] - }, - { - "$id": "64a52f0a6c41ded09def", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a1319104a149e16f5c", - "name": "users", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "name", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "dob", - "type": "string", - "required": false, - "array": false, - "size": 15, - "default": null - }, - { - "key": "username", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "profileImageUrl", - "type": "string", - "required": true, - "array": false, - "size": 500, - "default": null - }, - { - "key": "email", - "type": "string", - "required": true, - "array": false, - "format": "email", - "default": null - }, - { - "key": "profileImageID", - "type": "string", - "required": false, - "array": false, - "size": 300, - "default": null - }, - { - "key": "ratingCount", - "type": "integer", - "required": false, - "array": false, - "min": -9223372036854775808, - "max": 9223372036854775807, - "default": 1 - }, - { - "key": "ratingTotal", - "type": "double", - "required": false, - "array": false, - "min": -1.7976931348623157e+308, - "max": 1.7976931348623157e+308, - "default": 5 - }, - { - "key": "followers", - "type": "relationship", - "required": false, - "array": false, - "relatedCollection": "68b16bae0027e57ba2c6", - "relationType": "oneToMany", - "twoWay": true, - "twoWayKey": "followingUserId", - "onDelete": "cascade", - "side": "parent" - }, - { - "key": "friends", - "type": "relationship", - "required": false, - "array": false, - "relatedCollection": "68b43e30002f89343479", - "relationType": "manyToMany", - "twoWay": true, - "twoWayKey": "users", - "onDelete": "cascade", - "side": "parent" - } - ], - "indexes": [ - { - "key": "name_index", - "type": "fulltext", - "status": "available", - "attributes": [ - "name" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "username_index", - "type": "fulltext", - "status": "available", - "attributes": [ - "username" - ], - "orders": [ - "ASC" - ] - } - ] - }, - { - "$id": "68b16bae0027e57ba2c6", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a1319104a149e16f5c", - "name": "followers", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "followingUserId", - "type": "relationship", - "required": false, - "array": false, - "relatedCollection": "64a52f0a6c41ded09def", - "relationType": "oneToMany", - "twoWay": true, - "twoWayKey": "followers", - "onDelete": "cascade", - "side": "child" - }, - { - "key": "followerUserId", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "followerUsername", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "followerName", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "followerFCMToken", - "type": "string", - "required": true, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "followerProfileImageUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - }, - { - "key": "followerRating", - "type": "double", - "required": true, - "array": false, - "min": 0, - "max": 5, - "default": null - } - ], - "indexes": [] - }, - { - "$id": "64a5217e695bf2c4ec9c", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "rooms", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "name", - "type": "string", - "required": true, - "array": false, - "size": 200, - "default": null - }, - { - "key": "tags", - "type": "string", - "required": false, - "array": true, - "size": 50, - "default": null - }, - { - "key": "description", - "type": "string", - "required": false, - "array": false, - "size": 1024, - "default": null - }, - { - "key": "totalParticipants", - "type": "integer", - "required": false, - "array": false, - "min": 0, - "max": 10000000, - "default": 0 - }, - { - "key": "adminUid", - "type": "string", - "required": true, - "array": false, - "size": 255, - "default": null - } - ], - "indexes": [] - }, - { - "$id": "64a63e508145d1084abf", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "participants", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "uid", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "isAdmin", - "type": "boolean", - "required": true, - "array": false, - "default": null - }, - { - "key": "isModerator", - "type": "boolean", - "required": true, - "array": false, - "default": null - }, - { - "key": "isSpeaker", - "type": "boolean", - "required": true, - "array": false, - "default": null - }, - { - "key": "isMicOn", - "type": "boolean", - "required": true, - "array": false, - "default": null - }, - { - "key": "roomId", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "hasRequestedToBeSpeaker", - "type": "boolean", - "required": false, - "array": false, - "default": null - } - ], - "indexes": [ - { - "key": "roomId", - "type": "key", - "status": "available", - "attributes": [ - "roomId" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "uid", - "type": "key", - "status": "available", - "attributes": [ - "uid" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "roomUser", - "type": "key", - "status": "available", - "attributes": [ - "uid", - "roomId" - ], - "orders": [ - "ASC", - "ASC" - ] - } - ] - }, - { - "$id": "64d980211f1395263ebe", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "pair-requests", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "languageIso", - "type": "string", - "required": true, - "array": false, - "size": 20, - "default": null - }, - { - "key": "isAnonymous", - "type": "boolean", - "required": true, - "array": false, - "default": null - }, - { - "key": "uid", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "userName", - "type": "string", - "required": false, - "array": false, - "size": 100, - "default": null - }, - { - "key": "name", - "type": "string", - "required": false, - "array": false, - "size": 100, - "default": null - }, - { - "key": "isRandom", - "type": "boolean", - "required": false, - "array": false, - "default": true - }, - { - "key": "profileImageUrl", - "type": "string", - "required": false, - "array": false, - "format": "url", - "default": null - }, - { - "key": "userRating", - "type": "double", - "required": false, - "array": false, - "min": -1.7976931348623157e+308, - "max": 1.7976931348623157e+308, - "default": null - } - ], - "indexes": [ - { - "key": "languageIso", - "type": "key", - "status": "available", - "attributes": [ - "languageIso" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "docId", - "type": "key", - "status": "available", - "attributes": [ - "$id" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "createdAt", - "type": "key", - "status": "available", - "attributes": [ - "$createdAt" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "requestIndex", - "type": "key", - "status": "available", - "attributes": [ - "$id", - "languageIso", - "$createdAt" - ], - "orders": [ - "ASC", - "ASC", - "ASC" - ] - }, - { - "key": "requestIndex1", - "type": "key", - "status": "available", - "attributes": [ - "$id", - "languageIso" - ], - "orders": [ - "ASC", - "ASC" - ] - } - ] - }, - { - "$id": "64d980cd65ff2e08ab97", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "active-pairs", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "userDocId1", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "userDocId2", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "userName1", - "type": "string", - "required": false, - "array": false, - "size": 50, - "default": "Anonymous " - }, - { - "key": "userName2", - "type": "string", - "required": false, - "array": false, - "size": 50, - "default": "Anonymous" - }, - { - "key": "uid1", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "uid2", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - } - ], - "indexes": [ - { - "key": "userDocId1", - "type": "unique", - "status": "available", - "attributes": [ - "userDocId1" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "userDocId2", - "type": "unique", - "status": "available", - "attributes": [ - "userDocId2" - ], - "orders": [ - "ASC" - ] - } - ] - }, - { - "$id": "64a7bfe3a3a7ee5bf7f8", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a7bfd6b09121548bfe", - "name": "OTP's", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "otp", - "type": "string", - "required": false, - "array": false, - "size": 100000, - "default": null - }, - { - "key": "date", - "type": "string", - "required": false, - "array": false, - "size": 50, - "default": "None" - } - ], - "indexes": [] - }, - { - "$id": "64a7c0100eabfe8d3844", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a7bfd6b09121548bfe", - "name": "Verifications", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "status", - "type": "string", - "required": false, - "array": false, - "size": 1000000, - "default": null - } - ], - "indexes": [] - }, - { - "$id": "6522fd163103bd453183", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "6522fcf27a1bbc4238df", - "name": "Upcoming Rooms", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "isTime", - "type": "boolean", - "required": false, - "array": false, - "default": false - }, - { - "key": "name", - "type": "string", - "required": false, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "scheduledDateTime", - "type": "datetime", - "required": false, - "array": false, - "format": "", - "default": null - }, - { - "key": "tags", - "type": "string", - "required": false, - "array": true, - "size": 10000, - "default": null - }, - { - "key": "description", - "type": "string", - "required": false, - "array": false, - "size": 100000, - "default": null - }, - { - "key": "creatorUid", - "type": "string", - "required": true, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "creator_fcm_tokens", - "type": "string", - "required": false, - "array": true, - "size": 1000, - "default": null - } - ], - "indexes": [] - }, - { - "$id": "6522fd267db6fdad3392", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "6522fcf27a1bbc4238df", - "name": "Subscribed Users", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "userID", - "type": "string", - "required": false, - "array": false, - "size": 10000, - "default": null - }, - { - "key": "userProfileUrl", - "type": "string", - "required": false, - "array": false, - "size": 100000, - "default": null - }, - { - "key": "upcomingRoomId", - "type": "string", - "required": false, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "registrationTokens", - "type": "string", - "required": false, - "array": true, - "size": 1000, - "default": null - } - ], - "indexes": [] - }, - { - "$id": "670259e20000ddda49a0", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "stories", - "name": "Likes", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "uId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "storyId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - } - ], - "indexes": [ - { - "key": "uId_index", - "type": "key", - "status": "available", - "attributes": [ - "uId" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "storyId_index", - "type": "key", - "status": "available", - "attributes": [ - "storyId" - ], - "orders": [ - "ASC" - ] - } - ] - }, - { - "$id": "670277ad002530531daf", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "stories", - "name": "Chapters", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "title", - "type": "string", - "required": true, - "array": false, - "size": 20, - "default": null - }, - { - "key": "description", - "type": "string", - "required": false, - "array": false, - "size": 2000, - "default": null - }, - { - "key": "coverImgUrl", - "type": "string", - "required": false, - "array": false, - "format": "url", - "default": null - }, - { - "key": "lyrics", - "type": "string", - "required": false, - "array": false, - "size": 50000, - "default": null - }, - { - "key": "storyId", - "type": "string", - "required": true, - "array": false, - "size": 20, - "default": null - }, - { - "key": "audioFileUrl", - "type": "string", - "required": false, - "array": false, - "format": "url", - "default": null - }, - { - "key": "tintColor", - "type": "string", - "required": false, - "array": false, - "size": 20, - "default": null - }, - { - "key": "playDuration", - "type": "integer", - "required": true, - "array": false, - "min": 0, - "max": 3600000, - "default": null - } - ], - "indexes": [ - { - "key": "storyId_index", - "type": "key", - "status": "available", - "attributes": [ - "storyId" - ], - "orders": [ - "ASC" - ] - } - ] - }, - { - "$id": "670259e900321c12a5a2", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "stories", - "name": "Stories", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "title", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "description", - "type": "string", - "required": false, - "array": false, - "size": 2000, - "default": null - }, - { - "key": "category", - "type": "string", - "required": true, - "array": false, - "elements": [ - "horror", - "comedy", - "thriller", - "romance", - "spiritual", - "drama" - ], - "format": "enum", - "default": null - }, - { - "key": "coverImgUrl", - "type": "string", - "required": false, - "array": false, - "format": "url", - "default": null - }, - { - "key": "creatorId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "creatorName", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "creatorImgUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - }, - { - "key": "likes", - "type": "integer", - "required": false, - "array": false, - "min": 0, - "max": 100000000000, - "default": null - }, - { - "key": "tintColor", - "type": "string", - "required": false, - "array": false, - "size": 20, - "default": null - }, - { - "key": "playDuration", - "type": "integer", - "required": true, - "array": false, - "min": 0, - "max": 3600000, - "default": null - } - ], - "indexes": [ - { - "key": "title_index", - "type": "fulltext", - "status": "available", - "attributes": [ - "title" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "creatorname_index", - "type": "fulltext", - "status": "available", - "attributes": [ - "creatorName" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "description_index", - "type": "fulltext", - "status": "available", - "attributes": [ - "description" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "creatorId", - "type": "key", - "status": "available", - "attributes": [ - "creatorId" - ], - "orders": [ - "ASC" - ] - }, - { - "key": "categoryIndex", - "type": "key", - "status": "available", - "attributes": [ - "category" - ], - "orders": [ - "ASC" - ] - } - ] - }, - { - "$id": "670d812c0002c33c09a8", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "messages", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "roomId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "creatorId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "creatorUsername", - "type": "string", - "required": true, - "array": false, - "size": 20, - "default": null - }, - { - "key": "hasValidTag", - "type": "boolean", - "required": false, - "array": false, - "default": false - }, - { - "key": "creatorName", - "type": "string", - "required": false, - "array": false, - "size": 20, - "default": null - }, - { - "key": "messageId", - "type": "string", - "required": true, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "index", - "type": "integer", - "required": true, - "array": false, - "min": 0, - "max": 1000, - "default": null - }, - { - "key": "isEdited", - "type": "boolean", - "required": false, - "array": false, - "default": false - }, - { - "key": "content", - "type": "string", - "required": true, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "creatorImgUrl", - "type": "string", - "required": false, - "array": false, - "format": "url", - "default": null - }, - { - "key": "creationDateTime", - "type": "datetime", - "required": true, - "array": false, - "format": "", - "default": null - } - ], - "indexes": [] - }, - { - "$id": "672759820027801f121f", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "reply_to", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "creatorUsername", - "type": "string", - "required": true, - "array": false, - "size": 30, - "default": null - }, - { - "key": "creatorImgUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - }, - { - "key": "index", - "type": "integer", - "required": true, - "array": false, - "min": 0, - "max": 1000, - "default": null - }, - { - "key": "content", - "type": "string", - "required": true, - "array": false, - "size": 1000, - "default": null - }, - { - "key": "messageId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - } - ], - "indexes": [] - }, - { - "$id": "68b43e30002f89343479", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a1319104a149e16f5c", - "name": "friends", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "users", - "type": "relationship", - "required": false, - "array": false, - "relatedCollection": "64a52f0a6c41ded09def", - "relationType": "manyToMany", - "twoWay": true, - "twoWayKey": "friends", - "onDelete": "cascade", - "side": "child" - }, - { - "key": "senderId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "recieverId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "senderUsername", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "recieverUsername", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "senderName", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "recieverName", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "senderFCMToken", - "type": "string", - "required": false, - "array": false, - "size": 5000, - "default": null - }, - { - "key": "recieverFCMToken", - "type": "string", - "required": false, - "array": false, - "size": 5000, - "default": null - }, - { - "key": "requestStatus", - "type": "string", - "required": true, - "array": false, - "elements": [ - "sent", - "accepted" - ], - "format": "enum", - "default": null - }, - { - "key": "requestSentByUserId", - "type": "string", - "required": true, - "array": false, - "size": 50, - "default": null - }, - { - "key": "senderRating", - "type": "double", - "required": true, - "array": false, - "min": -1.7976931348623157e+308, - "max": 1.7976931348623157e+308, - "default": null - }, - { - "key": "recieverRating", - "type": "double", - "required": true, - "array": false, - "min": -1.7976931348623157e+308, - "max": 1.7976931348623157e+308, - "default": null - }, - { - "key": "senderProfileImgUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - }, - { - "key": "recieverProfileImgUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - } - ], - "indexes": [] - }, - { - "$id": "68b764ba002794fa2f61", - "$permissions": [ - "create(\"any\")", - "read(\"any\")", - "update(\"any\")", - "delete(\"any\")" - ], - "databaseId": "64a521785f5be62b796f", - "name": "friend-calls", - "enabled": true, - "documentSecurity": false, - "attributes": [ - { - "key": "callerName", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "recieverName", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "callerUsername", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "recieverUsername", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "callerUid", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "recieverUid", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "livekitRoomId", - "type": "string", - "required": true, - "array": false, - "size": 100, - "default": null - }, - { - "key": "callStatus", - "type": "string", - "required": true, - "array": false, - "elements": [ - "waiting", - "connected", - "declined", - "ended" - ], - "format": "enum", - "default": null - }, - { - "key": "callerProfileImageUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - }, - { - "key": "recieverProfileImageUrl", - "type": "string", - "required": true, - "array": false, - "format": "url", - "default": null - } - ], - "indexes": [] + "path": "functions/send-otp" } ] } \ No newline at end of file diff --git a/functions/send-otp-backup/README.md b/functions/send-otp-backup/README.md new file mode 100644 index 0000000..be757b9 --- /dev/null +++ b/functions/send-otp-backup/README.md @@ -0,0 +1,86 @@ +# Send OTP function + +Function to send OTPs. + +## 🧰 Usage + +### POST / + +**Parameters** + +| Name | Description | Location | Type | Sample Value | +| -------------- | ------------------------- | -------- | ------ | ------------------ | +| otpId | Document ID of the otp | Body | String | `jcbd...kdsn` | +| recipientEmail | Email ID of the recipient | Body | String | `jcbd...@mail.com` | + +**Response** + +Sample `200` Response: + +```json +{ + "msg": "mail sent" +} +``` + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | ------------------------------ | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install && npm run start` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | + +## 🔒 Environment Variables + +### APPWRITE_API_KEY + +API Key to use Appwrite Sever SDK. + +| Question | Answer | +| ------------- | ------------------------------------------------------------------------ | +| Required | Yes | +| Sample Value | `62...97` | +| Documentation | [Appwrite API Keys](https://appwrite.io/docs/advanced/platform/api-keys) | + +### VERIFICATION_DATABASE_ID + +Database ID of verification database in appwrite. + +| Question | Answer | +| ------------- | --------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `Zjc...5PH` | +| Documentation | [Resonate](https://github.com/AOSSIE-Org/Resonate/blob/master/lib/utils/constants.dart) | + +### OTP_COLLECTION_ID + +Collection ID of otp collection. + +| Question | Answer | +| ------------- | --------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `NXOi3...IBHDa` | +| Documentation | [Resonate](https://github.com/AOSSIE-Org/Resonate/blob/master/lib/utils/constants.dart) | + +### SENDER_MAIL + +Email of the sender. + +| Question | Answer | +| ------------- | ------------------------------------------------ | +| Required | Yes | +| Sample Value | `jsch...@mail.com` | +| Documentation | [Discord](https://discord.com/invite/6mFZ2S846n) | + +### SENDER_PASSWORD + +Password of the sender's account. + +| Question | Answer | +| ------------- | ------------------------------------------------ | +| Required | Yes | +| Sample Value | `HC1Itf...........dAAKF5o` | +| Documentation | [Discord](https://discord.com/invite/6mFZ2S846n) | diff --git a/functions/send-otp-backup/package-lock.json b/functions/send-otp-backup/package-lock.json new file mode 100644 index 0000000..08548f1 --- /dev/null +++ b/functions/send-otp-backup/package-lock.json @@ -0,0 +1,141 @@ +{ + "name": "starter-template", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "starter-template", + "version": "1.0.0", + "dependencies": { + "node-appwrite": "^17.0.0", + "nodemailer": "^6.9.5" + }, + "devDependencies": { + "prettier": "^3.0.0" + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "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" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-appwrite": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-9.0.0.tgz", + "integrity": "sha512-iTcHbuaJfr6bP/HFkRVV+FcaumKkbINqZyypQdl+tYxv6Dx0bkB/YKUXGYfTkgP18TLPWQQB++OGQhi98dlo2w==", + "dependencies": { + "axios": "^1.3.6", + "form-data": "^4.0.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.5.tgz", + "integrity": "sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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==" + } + } +} \ No newline at end of file diff --git a/functions/send-otp-backup/package.json b/functions/send-otp-backup/package.json new file mode 100644 index 0000000..73b4a81 --- /dev/null +++ b/functions/send-otp-backup/package.json @@ -0,0 +1,18 @@ +{ + "name": "send-otp", + "version": "1.0.0", + "description": "", + "main": "src/main.js", + "type": "module", + "scripts": { + "start": "node src/main.js", + "format": "prettier --write ." + }, + "dependencies": { + "node-appwrite": "^17.0.0", + "nodemailer": "^6.9.5" + }, + "devDependencies": { + "prettier": "^3.0.0" + } +} \ No newline at end of file diff --git a/functions/send-otp-backup/src/appwrite.js b/functions/send-otp-backup/src/appwrite.js new file mode 100644 index 0000000..6f52d37 --- /dev/null +++ b/functions/send-otp-backup/src/appwrite.js @@ -0,0 +1,58 @@ +import { Client, Databases, Query } from 'node-appwrite'; + +class AppwriteService { + constructor() { + const client = new Client() + .setEndpoint( + process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1' + ) + .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID) + .setKey(process.env.APPWRITE_API_KEY); + + this.databases = new Databases(client); + } + + async createOtpDocument(otpId, otp, date) { + await this.databases.createDocument( + process.env.VERIFICATION_DATABASE_ID, + process.env.OTP_COLLECTION_ID, + otpId, + { + otp, + date + } + ); + } + + async getUserByEmail(email) { + try { + const response = await this.databases.listDocuments( + process.env.UserDataDatabaseID, + process.env.UsersCollectionID, + [Query.equal('email', email), Query.limit(1)] + ); + return response.documents.length > 0 ? response.documents[0] : null; + } catch (e) { + // Return null if user not found or any error occurs + return null; + } + } + + async updateUserLastOtpSent(userId, timestamp) { + try { + await this.databases.updateDocument( + process.env.UserDataDatabaseID, + process.env.UsersCollectionID, + userId, + { + last_otp_sent: timestamp + } + ); + } catch (e) { + // If update fails, log but don't throw (non-critical) + throw e; + } + } +} + +export default AppwriteService; diff --git a/functions/send-otp-backup/src/mail.js b/functions/send-otp-backup/src/mail.js new file mode 100644 index 0000000..30f87df --- /dev/null +++ b/functions/send-otp-backup/src/mail.js @@ -0,0 +1,25 @@ +import { createTransport } from 'nodemailer'; + +class MailService { + constructor() { + this.transporter = createTransport({ + service: 'gmail', + secure: true, + auth: { + user: process.env.SENDER_MAIL, + pass: process.env.SENDER_PASSWORD, + }, + }); + } + + async sendMail(recipientEmail, otp) { + await this.transporter.sendMail({ + from: process.env.SENDER_MAIL, // sender address + to: recipientEmail, // list of receivers + subject: 'Email Verification', // Subject line + text: `Greetings User, here is your email verification OTP: ${otp}`, // plain text body + }); + } +} + +export default MailService; diff --git a/functions/send-otp-backup/src/main.js b/functions/send-otp-backup/src/main.js new file mode 100644 index 0000000..29da501 --- /dev/null +++ b/functions/send-otp-backup/src/main.js @@ -0,0 +1,67 @@ +import { throwIfMissing } from "./utils.js"; +import AppwriteService from "./appwrite.js"; +import MailService from "./mail.js"; + +export default async ({ req, res, log, error }) => { + throwIfMissing(process.env, [ + "APPWRITE_API_KEY", + "VERIFICATION_DATABASE_ID", + "OTP_COLLECTION_ID", + "SENDER_MAIL", + "SENDER_PASSWORD", + "UserDataDatabaseID", + "UsersCollectionID", + ]); + + const appwrite = new AppwriteService(); + const mailService = new MailService(); + + try { + log(req.body); + const { otpID, email: recipientEmail } = JSON.parse(req.body); + + // Rate limit check: Check if user has requested OTP in the last 60 seconds + const userDoc = await appwrite.getUserByEmail(recipientEmail); + + if (userDoc && userDoc.last_otp_sent) { + const lastOtpSentTime = new Date(userDoc.last_otp_sent).getTime(); + const currentTime = Date.now(); + const timeDifference = (currentTime - lastOtpSentTime) / 1000; // Convert to seconds + + if (timeDifference < 60) { + const remainingSeconds = Math.ceil(60 - timeDifference); + return res.json( + { + message: `Too many requests. Please wait ${remainingSeconds} second(s) before requesting another OTP.` + }, + 429 + ); + } + } + + const otp = String(Math.floor(100000 + Math.random() * 900000)); + + await mailService.sendMail(recipientEmail, otp); + + // Logic for deleting the otp when the Date changes + const currentDate = new Date().toDateString(); + log(`Current Date: ${currentDate}`); + + await appwrite.createOtpDocument(otpID, otp, currentDate); + + // Update last_otp_sent timestamp in user document + if (userDoc) { + try { + await appwrite.updateUserLastOtpSent(userDoc.$id, new Date().toISOString()); + } catch (updateError) { + // Log error but don't fail the request if timestamp update fails + log(`Warning: Failed to update last_otp_sent timestamp: ${updateError}`); + } + } + } catch (e) { + error(String(e)); + return res.json({ message: String(e) }); + } + + return res.json({ message: "mail sent" }); +}; diff --git a/functions/send-otp-backup/src/utils.js b/functions/send-otp-backup/src/utils.js new file mode 100644 index 0000000..18f9170 --- /dev/null +++ b/functions/send-otp-backup/src/utils.js @@ -0,0 +1,11 @@ +export const throwIfMissing = (obj, keys) => { + const missing = []; + for (let key of keys) { + if (!(key in obj) || !obj[key]) { + missing.push(key); + } + } + if (missing.length > 0) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } +}; diff --git a/functions/send-otp/.gitignore b/functions/send-otp/.gitignore new file mode 100644 index 0000000..46afb6b --- /dev/null +++ b/functions/send-otp/.gitignore @@ -0,0 +1,133 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Directory used by Appwrite CLI for local development +.appwrite \ No newline at end of file diff --git a/functions/send-otp/.prettierrc.json b/functions/send-otp/.prettierrc.json new file mode 100644 index 0000000..0a72520 --- /dev/null +++ b/functions/send-otp/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/functions/send-otp/README.md b/functions/send-otp/README.md index be757b9..20052f1 100644 --- a/functions/send-otp/README.md +++ b/functions/send-otp/README.md @@ -1,17 +1,22 @@ -# Send OTP function - -Function to send OTPs. +# send-otp ## 🧰 Usage -### POST / +### GET /ping + +- Returns a "Pong" message. + +**Response** -**Parameters** +Sample `200` Response: -| Name | Description | Location | Type | Sample Value | -| -------------- | ------------------------- | -------- | ------ | ------------------ | -| otpId | Document ID of the otp | Body | String | `jcbd...kdsn` | -| recipientEmail | Email ID of the recipient | Body | String | `jcbd...@mail.com` | +```text +Pong +``` + +### GET, POST, PUT, PATCH, DELETE / + +- Returns a "Learn More" JSON response. **Response** @@ -19,68 +24,23 @@ Sample `200` Response: ```json { - "msg": "mail sent" + "motto": "Build like a team of hundreds_", + "learn": "https://appwrite.io/docs", + "connect": "https://appwrite.io/discord", + "getInspired": "https://builtwith.appwrite.io" } ``` ## ⚙️ Configuration -| Setting | Value | -| ----------------- | ------------------------------ | -| Runtime | Node (18.0) | -| Entrypoint | `src/main.js` | -| Build Commands | `npm install && npm run start` | -| Permissions | `any` | -| Timeout (Seconds) | 15 | +| Setting | Value | +| ----------------- | ------------- | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | ## 🔒 Environment Variables -### APPWRITE_API_KEY - -API Key to use Appwrite Sever SDK. - -| Question | Answer | -| ------------- | ------------------------------------------------------------------------ | -| Required | Yes | -| Sample Value | `62...97` | -| Documentation | [Appwrite API Keys](https://appwrite.io/docs/advanced/platform/api-keys) | - -### VERIFICATION_DATABASE_ID - -Database ID of verification database in appwrite. - -| Question | Answer | -| ------------- | --------------------------------------------------------------------------------------- | -| Required | Yes | -| Sample Value | `Zjc...5PH` | -| Documentation | [Resonate](https://github.com/AOSSIE-Org/Resonate/blob/master/lib/utils/constants.dart) | - -### OTP_COLLECTION_ID - -Collection ID of otp collection. - -| Question | Answer | -| ------------- | --------------------------------------------------------------------------------------- | -| Required | Yes | -| Sample Value | `NXOi3...IBHDa` | -| Documentation | [Resonate](https://github.com/AOSSIE-Org/Resonate/blob/master/lib/utils/constants.dart) | - -### SENDER_MAIL - -Email of the sender. - -| Question | Answer | -| ------------- | ------------------------------------------------ | -| Required | Yes | -| Sample Value | `jsch...@mail.com` | -| Documentation | [Discord](https://discord.com/invite/6mFZ2S846n) | - -### SENDER_PASSWORD - -Password of the sender's account. - -| Question | Answer | -| ------------- | ------------------------------------------------ | -| Required | Yes | -| Sample Value | `HC1Itf...........dAAKF5o` | -| Documentation | [Discord](https://discord.com/invite/6mFZ2S846n) | +No environment variables required. diff --git a/functions/send-otp/package-lock.json b/functions/send-otp/package-lock.json index 08548f1..c938e6c 100644 --- a/functions/send-otp/package-lock.json +++ b/functions/send-otp/package-lock.json @@ -8,119 +8,41 @@ "name": "starter-template", "version": "1.0.0", "dependencies": { - "node-appwrite": "^17.0.0", - "nodemailer": "^6.9.5" + "node-appwrite": "^20.3.0", + "nodemailer": "^7.0.12" }, "devDependencies": { - "prettier": "^3.0.0" - } - }, - "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.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "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" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" + "prettier": "^3.2.5" } }, "node_modules/node-appwrite": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-9.0.0.tgz", - "integrity": "sha512-iTcHbuaJfr6bP/HFkRVV+FcaumKkbINqZyypQdl+tYxv6Dx0bkB/YKUXGYfTkgP18TLPWQQB++OGQhi98dlo2w==", + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-20.3.0.tgz", + "integrity": "sha512-vaxlpkNTtjkccyngwc9HRSwQQasZ/CJri6KMZ62mlDAM1YdvOgqIM8EYJwvhx/fiHdyjfxithFcmnvzUR9TGTQ==", + "license": "BSD-3-Clause", "dependencies": { - "axios": "^1.3.6", - "form-data": "^4.0.0" + "node-fetch-native-with-agent": "1.7.2" } }, + "node_modules/node-fetch-native-with-agent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", + "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", + "license": "MIT" + }, "node_modules/nodemailer": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.5.tgz", - "integrity": "sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } }, "node_modules/prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", - "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -131,11 +53,6 @@ "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } - }, - "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==" } } -} \ No newline at end of file +} diff --git a/functions/send-otp/package.json b/functions/send-otp/package.json index 73b4a81..d6ade0d 100644 --- a/functions/send-otp/package.json +++ b/functions/send-otp/package.json @@ -1,18 +1,17 @@ { - "name": "send-otp", - "version": "1.0.0", - "description": "", - "main": "src/main.js", - "type": "module", - "scripts": { - "start": "node src/main.js", - "format": "prettier --write ." - }, - "dependencies": { - "node-appwrite": "^17.0.0", - "nodemailer": "^6.9.5" - }, - "devDependencies": { - "prettier": "^3.0.0" - } -} \ No newline at end of file + "name": "starter-template", + "version": "1.0.0", + "description": "", + "main": "src/main.js", + "type": "module", + "scripts": { + "format": "prettier --write ." + }, + "dependencies": { + "node-appwrite": "^20.3.0", + "nodemailer": "^7.0.12" + }, + "devDependencies": { + "prettier": "^3.2.5" + } +} diff --git a/functions/send-otp/src/appwrite.js b/functions/send-otp/src/appwrite.js index 6f52d37..a77d2e9 100644 --- a/functions/send-otp/src/appwrite.js +++ b/functions/send-otp/src/appwrite.js @@ -14,8 +14,8 @@ class AppwriteService { async createOtpDocument(otpId, otp, date) { await this.databases.createDocument( - process.env.VERIFICATION_DATABASE_ID, - process.env.OTP_COLLECTION_ID, + process.env.UserDataDatabaseID, + process.env.UsersCollectionID, otpId, { otp, diff --git a/functions/send-otp/src/main.js b/functions/send-otp/src/main.js index 29da501..b0871d3 100644 --- a/functions/send-otp/src/main.js +++ b/functions/send-otp/src/main.js @@ -5,12 +5,11 @@ import MailService from "./mail.js"; export default async ({ req, res, log, error }) => { throwIfMissing(process.env, [ "APPWRITE_API_KEY", - "VERIFICATION_DATABASE_ID", - "OTP_COLLECTION_ID", + "APPWRITE_FUNCTION_PROJECT_ID", "SENDER_MAIL", "SENDER_PASSWORD", "UserDataDatabaseID", - "UsersCollectionID", + "UsersCollectionID" ]); const appwrite = new AppwriteService(); From c46c3e864c8de5b8ca89ba0fc269b5af6bf29e7e Mon Sep 17 00:00:00 2001 From: cannonerd_6002 Date: Wed, 21 Jan 2026 18:51:21 +0530 Subject: [PATCH 3/5] cleanup: remove backup folder as requested by maintainer --- functions/send-otp-backup/README.md | 86 ------------ functions/send-otp-backup/package-lock.json | 141 -------------------- functions/send-otp-backup/package.json | 18 --- functions/send-otp-backup/src/appwrite.js | 58 -------- functions/send-otp-backup/src/mail.js | 25 ---- functions/send-otp-backup/src/main.js | 67 ---------- functions/send-otp-backup/src/utils.js | 11 -- 7 files changed, 406 deletions(-) delete mode 100644 functions/send-otp-backup/README.md delete mode 100644 functions/send-otp-backup/package-lock.json delete mode 100644 functions/send-otp-backup/package.json delete mode 100644 functions/send-otp-backup/src/appwrite.js delete mode 100644 functions/send-otp-backup/src/mail.js delete mode 100644 functions/send-otp-backup/src/main.js delete mode 100644 functions/send-otp-backup/src/utils.js diff --git a/functions/send-otp-backup/README.md b/functions/send-otp-backup/README.md deleted file mode 100644 index be757b9..0000000 --- a/functions/send-otp-backup/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Send OTP function - -Function to send OTPs. - -## 🧰 Usage - -### POST / - -**Parameters** - -| Name | Description | Location | Type | Sample Value | -| -------------- | ------------------------- | -------- | ------ | ------------------ | -| otpId | Document ID of the otp | Body | String | `jcbd...kdsn` | -| recipientEmail | Email ID of the recipient | Body | String | `jcbd...@mail.com` | - -**Response** - -Sample `200` Response: - -```json -{ - "msg": "mail sent" -} -``` - -## ⚙️ Configuration - -| Setting | Value | -| ----------------- | ------------------------------ | -| Runtime | Node (18.0) | -| Entrypoint | `src/main.js` | -| Build Commands | `npm install && npm run start` | -| Permissions | `any` | -| Timeout (Seconds) | 15 | - -## 🔒 Environment Variables - -### APPWRITE_API_KEY - -API Key to use Appwrite Sever SDK. - -| Question | Answer | -| ------------- | ------------------------------------------------------------------------ | -| Required | Yes | -| Sample Value | `62...97` | -| Documentation | [Appwrite API Keys](https://appwrite.io/docs/advanced/platform/api-keys) | - -### VERIFICATION_DATABASE_ID - -Database ID of verification database in appwrite. - -| Question | Answer | -| ------------- | --------------------------------------------------------------------------------------- | -| Required | Yes | -| Sample Value | `Zjc...5PH` | -| Documentation | [Resonate](https://github.com/AOSSIE-Org/Resonate/blob/master/lib/utils/constants.dart) | - -### OTP_COLLECTION_ID - -Collection ID of otp collection. - -| Question | Answer | -| ------------- | --------------------------------------------------------------------------------------- | -| Required | Yes | -| Sample Value | `NXOi3...IBHDa` | -| Documentation | [Resonate](https://github.com/AOSSIE-Org/Resonate/blob/master/lib/utils/constants.dart) | - -### SENDER_MAIL - -Email of the sender. - -| Question | Answer | -| ------------- | ------------------------------------------------ | -| Required | Yes | -| Sample Value | `jsch...@mail.com` | -| Documentation | [Discord](https://discord.com/invite/6mFZ2S846n) | - -### SENDER_PASSWORD - -Password of the sender's account. - -| Question | Answer | -| ------------- | ------------------------------------------------ | -| Required | Yes | -| Sample Value | `HC1Itf...........dAAKF5o` | -| Documentation | [Discord](https://discord.com/invite/6mFZ2S846n) | diff --git a/functions/send-otp-backup/package-lock.json b/functions/send-otp-backup/package-lock.json deleted file mode 100644 index 08548f1..0000000 --- a/functions/send-otp-backup/package-lock.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "name": "starter-template", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "starter-template", - "version": "1.0.0", - "dependencies": { - "node-appwrite": "^17.0.0", - "nodemailer": "^6.9.5" - }, - "devDependencies": { - "prettier": "^3.0.0" - } - }, - "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.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "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" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-appwrite": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-9.0.0.tgz", - "integrity": "sha512-iTcHbuaJfr6bP/HFkRVV+FcaumKkbINqZyypQdl+tYxv6Dx0bkB/YKUXGYfTkgP18TLPWQQB++OGQhi98dlo2w==", - "dependencies": { - "axios": "^1.3.6", - "form-data": "^4.0.0" - } - }, - "node_modules/nodemailer": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.5.tgz", - "integrity": "sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", - "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "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==" - } - } -} \ No newline at end of file diff --git a/functions/send-otp-backup/package.json b/functions/send-otp-backup/package.json deleted file mode 100644 index 73b4a81..0000000 --- a/functions/send-otp-backup/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "send-otp", - "version": "1.0.0", - "description": "", - "main": "src/main.js", - "type": "module", - "scripts": { - "start": "node src/main.js", - "format": "prettier --write ." - }, - "dependencies": { - "node-appwrite": "^17.0.0", - "nodemailer": "^6.9.5" - }, - "devDependencies": { - "prettier": "^3.0.0" - } -} \ No newline at end of file diff --git a/functions/send-otp-backup/src/appwrite.js b/functions/send-otp-backup/src/appwrite.js deleted file mode 100644 index 6f52d37..0000000 --- a/functions/send-otp-backup/src/appwrite.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Client, Databases, Query } from 'node-appwrite'; - -class AppwriteService { - constructor() { - const client = new Client() - .setEndpoint( - process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1' - ) - .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID) - .setKey(process.env.APPWRITE_API_KEY); - - this.databases = new Databases(client); - } - - async createOtpDocument(otpId, otp, date) { - await this.databases.createDocument( - process.env.VERIFICATION_DATABASE_ID, - process.env.OTP_COLLECTION_ID, - otpId, - { - otp, - date - } - ); - } - - async getUserByEmail(email) { - try { - const response = await this.databases.listDocuments( - process.env.UserDataDatabaseID, - process.env.UsersCollectionID, - [Query.equal('email', email), Query.limit(1)] - ); - return response.documents.length > 0 ? response.documents[0] : null; - } catch (e) { - // Return null if user not found or any error occurs - return null; - } - } - - async updateUserLastOtpSent(userId, timestamp) { - try { - await this.databases.updateDocument( - process.env.UserDataDatabaseID, - process.env.UsersCollectionID, - userId, - { - last_otp_sent: timestamp - } - ); - } catch (e) { - // If update fails, log but don't throw (non-critical) - throw e; - } - } -} - -export default AppwriteService; diff --git a/functions/send-otp-backup/src/mail.js b/functions/send-otp-backup/src/mail.js deleted file mode 100644 index 30f87df..0000000 --- a/functions/send-otp-backup/src/mail.js +++ /dev/null @@ -1,25 +0,0 @@ -import { createTransport } from 'nodemailer'; - -class MailService { - constructor() { - this.transporter = createTransport({ - service: 'gmail', - secure: true, - auth: { - user: process.env.SENDER_MAIL, - pass: process.env.SENDER_PASSWORD, - }, - }); - } - - async sendMail(recipientEmail, otp) { - await this.transporter.sendMail({ - from: process.env.SENDER_MAIL, // sender address - to: recipientEmail, // list of receivers - subject: 'Email Verification', // Subject line - text: `Greetings User, here is your email verification OTP: ${otp}`, // plain text body - }); - } -} - -export default MailService; diff --git a/functions/send-otp-backup/src/main.js b/functions/send-otp-backup/src/main.js deleted file mode 100644 index 29da501..0000000 --- a/functions/send-otp-backup/src/main.js +++ /dev/null @@ -1,67 +0,0 @@ -import { throwIfMissing } from "./utils.js"; -import AppwriteService from "./appwrite.js"; -import MailService from "./mail.js"; - -export default async ({ req, res, log, error }) => { - throwIfMissing(process.env, [ - "APPWRITE_API_KEY", - "VERIFICATION_DATABASE_ID", - "OTP_COLLECTION_ID", - "SENDER_MAIL", - "SENDER_PASSWORD", - "UserDataDatabaseID", - "UsersCollectionID", - ]); - - const appwrite = new AppwriteService(); - const mailService = new MailService(); - - try { - log(req.body); - const { otpID, email: recipientEmail } = JSON.parse(req.body); - - // Rate limit check: Check if user has requested OTP in the last 60 seconds - const userDoc = await appwrite.getUserByEmail(recipientEmail); - - if (userDoc && userDoc.last_otp_sent) { - const lastOtpSentTime = new Date(userDoc.last_otp_sent).getTime(); - const currentTime = Date.now(); - const timeDifference = (currentTime - lastOtpSentTime) / 1000; // Convert to seconds - - if (timeDifference < 60) { - const remainingSeconds = Math.ceil(60 - timeDifference); - return res.json( - { - message: `Too many requests. Please wait ${remainingSeconds} second(s) before requesting another OTP.` - }, - 429 - ); - } - } - - const otp = String(Math.floor(100000 + Math.random() * 900000)); - - await mailService.sendMail(recipientEmail, otp); - - // Logic for deleting the otp when the Date changes - const currentDate = new Date().toDateString(); - log(`Current Date: ${currentDate}`); - - await appwrite.createOtpDocument(otpID, otp, currentDate); - - // Update last_otp_sent timestamp in user document - if (userDoc) { - try { - await appwrite.updateUserLastOtpSent(userDoc.$id, new Date().toISOString()); - } catch (updateError) { - // Log error but don't fail the request if timestamp update fails - log(`Warning: Failed to update last_otp_sent timestamp: ${updateError}`); - } - } - } catch (e) { - error(String(e)); - return res.json({ message: String(e) }); - } - - return res.json({ message: "mail sent" }); -}; diff --git a/functions/send-otp-backup/src/utils.js b/functions/send-otp-backup/src/utils.js deleted file mode 100644 index 18f9170..0000000 --- a/functions/send-otp-backup/src/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -export const throwIfMissing = (obj, keys) => { - const missing = []; - for (let key of keys) { - if (!(key in obj) || !obj[key]) { - missing.push(key); - } - } - if (missing.length > 0) { - throw new Error(`Missing required fields: ${missing.join(', ')}`); - } -}; From a33b4c22e444893392047c356905db5112361779 Mon Sep 17 00:00:00 2001 From: cannonerd_6002 Date: Wed, 21 Jan 2026 19:03:32 +0530 Subject: [PATCH 4/5] fix: restore global variable names, update scopes, and sync documentation --- appwrite.json | 6 +++++- functions/send-otp/src/appwrite.js | 12 ++++++------ functions/send-otp/src/main.js | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/appwrite.json b/appwrite.json index 8c25af5..9635b3a 100644 --- a/appwrite.json +++ b/appwrite.json @@ -11,7 +11,11 @@ ], "events": [], "scopes": [ - "users.read" + "users.read", + "databases.read", + "databases.write", + "documents.read", + "documents.write" ], "schedule": "", "timeout": 15, diff --git a/functions/send-otp/src/appwrite.js b/functions/send-otp/src/appwrite.js index a77d2e9..5fd7337 100644 --- a/functions/send-otp/src/appwrite.js +++ b/functions/send-otp/src/appwrite.js @@ -14,8 +14,8 @@ class AppwriteService { async createOtpDocument(otpId, otp, date) { await this.databases.createDocument( - process.env.UserDataDatabaseID, - process.env.UsersCollectionID, + process.env.VERIFICATION_DATABASE_ID, + process.env.OTP_COLLECTION_ID, otpId, { otp, @@ -27,8 +27,8 @@ class AppwriteService { async getUserByEmail(email) { try { const response = await this.databases.listDocuments( - process.env.UserDataDatabaseID, - process.env.UsersCollectionID, + process.env.VERIFICATION_DATABASE_ID, + process.env.OTP_COLLECTION_ID, [Query.equal('email', email), Query.limit(1)] ); return response.documents.length > 0 ? response.documents[0] : null; @@ -41,8 +41,8 @@ class AppwriteService { async updateUserLastOtpSent(userId, timestamp) { try { await this.databases.updateDocument( - process.env.UserDataDatabaseID, - process.env.UsersCollectionID, + process.env.VERIFICATION_DATABASE_ID, + process.env.OTP_COLLECTION_ID, userId, { last_otp_sent: timestamp diff --git a/functions/send-otp/src/main.js b/functions/send-otp/src/main.js index b0871d3..94a0795 100644 --- a/functions/send-otp/src/main.js +++ b/functions/send-otp/src/main.js @@ -8,8 +8,8 @@ export default async ({ req, res, log, error }) => { "APPWRITE_FUNCTION_PROJECT_ID", "SENDER_MAIL", "SENDER_PASSWORD", - "UserDataDatabaseID", - "UsersCollectionID" + "VERIFICATION_DATABASE_ID", + "OTP_COLLECTION_ID", ]); const appwrite = new AppwriteService(); From 3d1c19ab6179dd1d5eafd9084a60d13cbe8668aa Mon Sep 17 00:00:00 2001 From: cannonerd_6002 Date: Wed, 21 Jan 2026 19:25:32 +0530 Subject: [PATCH 5/5] fix: address bot feedback on runtime, scopes, and error handling; finalize README --- appwrite.json | 2 +- functions/send-otp/README.md | 62 ++++++++++-------------------- functions/send-otp/src/appwrite.js | 3 +- 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/appwrite.json b/appwrite.json index 9635b3a..9dbdbed 100644 --- a/appwrite.json +++ b/appwrite.json @@ -4,7 +4,7 @@ { "$id": "6968b4f000222d5d8d93", "name": "send-otp", - "runtime": "node-16.0", + "runtime": "node-18.0", "specification": "s-0.5vcpu-512mb", "execute": [ "any" diff --git a/functions/send-otp/README.md b/functions/send-otp/README.md index 20052f1..a4e7e4b 100644 --- a/functions/send-otp/README.md +++ b/functions/send-otp/README.md @@ -1,46 +1,24 @@ -# send-otp +# Send OTP Function -## 🧰 Usage +This function handles the generation and delivery of a 6-digit OTP via email. It includes a **60-second backend rate-limiting** mechanism to prevent abuse. -### GET /ping - -- Returns a "Pong" message. - -**Response** - -Sample `200` Response: - -```text -Pong -``` - -### GET, POST, PUT, PATCH, DELETE / - -- Returns a "Learn More" JSON response. - -**Response** - -Sample `200` Response: - -```json -{ - "motto": "Build like a team of hundreds_", - "learn": "https://appwrite.io/docs", - "connect": "https://appwrite.io/discord", - "getInspired": "https://builtwith.appwrite.io" -} -``` +## 🚀 Features +- **Secure OTP Generation**: Creates a random 6-digit code for authentication. +- **Rate Limiting**: Checks the `last_otp_sent` timestamp in the database; returns a **429 Too Many Requests** if a request is made within 60 seconds of the last one. +- **Email Integration**: Delivers the OTP using SMTP with secure credentials. ## ⚙️ Configuration - -| Setting | Value | -| ----------------- | ------------- | -| Runtime | Node (18.0) | -| Entrypoint | `src/main.js` | -| Build Commands | `npm install` | -| Permissions | `any` | -| Timeout (Seconds) | 15 | - -## 🔒 Environment Variables - -No environment variables required. +- **Runtime**: Node.js 18.0 (Updated to address security patches) +- **Entrypoint**: `src/main.js` + +## 🔐 Environment Variables +The following variables must be configured in your Appwrite Function settings: + +| Variable | Description | +|----------|-------------| +| `APPWRITE_API_KEY` | API key with database and document scopes. | +| `APPWRITE_FUNCTION_PROJECT_ID` | Your active project ID. | +| `VERIFICATION_DATABASE_ID` | ID for the database storing OTP metadata. | +| `OTP_COLLECTION_ID` | Collection ID for storing user-specific OTP records. | +| `SENDER_MAIL` | The SMTP email address used to send the OTP. | +| `SENDER_PASSWORD` | The SMTP password or App Password for the sender. | \ No newline at end of file diff --git a/functions/send-otp/src/appwrite.js b/functions/send-otp/src/appwrite.js index 5fd7337..9050a6b 100644 --- a/functions/send-otp/src/appwrite.js +++ b/functions/send-otp/src/appwrite.js @@ -34,6 +34,7 @@ class AppwriteService { return response.documents.length > 0 ? response.documents[0] : null; } catch (e) { // Return null if user not found or any error occurs + console.error("Error getting user by email:", e); return null; } } @@ -50,7 +51,7 @@ class AppwriteService { ); } catch (e) { // If update fails, log but don't throw (non-critical) - throw e; + console.error("Error updating last_otp_sent:", e); } } }