Skip to content

feat: implement 60s backend rate-limiting for OTP requests (#140).#141

Open
cannonerd007 wants to merge 5 commits intoAOSSIE-Org:mainfrom
cannonerd007:feat/issue-140-rate-limit
Open

feat: implement 60s backend rate-limiting for OTP requests (#140).#141
cannonerd007 wants to merge 5 commits intoAOSSIE-Org:mainfrom
cannonerd007:feat/issue-140-rate-limit

Conversation

@cannonerd007
Copy link

@cannonerd007 cannonerd007 commented Jan 15, 2026

I have implemented a 60-second server-side rate limit for the send-otp function to address the security concerns raised in #140. Following the maintainer's feedback, I integrated an "early exit" strategy at the start of the function that checks the user's last_otp_sent timestamp in the database before any mail is generated or sent. If a user attempts to request an OTP too quickly, the system now returns a 429 Too Many Requests error, specifically informing them of the remaining cooldown time. To support this, I updated the AppwriteService with methods to fetch and update user metadata and ensured that the timestamp update is handled gracefully so it doesn't interrupt the core authentication flow if a database error occurs.

Summary by CodeRabbit

  • New Features

    • Added a consolidated OTP send function with 60-second rate-limiting and last-sent timestamp tracking.
  • Documentation

    • Updated README and usage/configuration guidance for the OTP flow.
  • Chores

    • Pruned legacy function entries and simplified the functions manifest; added formatting config and .gitignore; updated packaging metadata and tooling versions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Project manifest pruned to retain only a new send-otp function (projectId changed), OTP handling updated to add per-user 60s rate-limiting and timestamp tracking, and the send-otp function folder received formatting, ignore, README, and dependency updates.

Changes

Cohort / File(s) Summary
Project Manifest
appwrite.json
projectId changed from "resonate""6968ad380036f9106f75"; most functions/resources removed; only functions/send-otp retained with specification: "s-0.5vcpu-512mb".
OTP Service Methods
functions/send-otp/src/appwrite.js
Added getUserByEmail(email) to query users by email and return first match or null; added updateUserLastOtpSent(userId, timestamp) to set last_otp_sent.
OTP Rate-Limiting Logic
functions/send-otp/src/main.js
Reintroduced env validation; added rate-limit: fetch user, check last_otp_sent, block requests within 60s with 429-like response; after OTP send update user's last_otp_sent (errors logged, non-fatal).
Project Configuration
functions/send-otp/.gitignore, functions/send-otp/.prettierrc.json
Added .gitignore with standard patterns; added Prettier config (trailingComma: "es5", tabWidth: 2, semi: true, singleQuote: true).
Docs & Package Metadata
functions/send-otp/README.md, functions/send-otp/package.json
README rewritten (features, env vars, removed detailed config); package.json simplified (name → "starter-template", removed start script), dependencies updated (node-appwrite^20.3.0, nodemailer^7.0.12, prettier^3.2.5).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Handler as OTP Handler<br/>(main.js)
    participant Service as AppwriteService
    participant DB as Database
    participant Mailer as Email Service

    Client->>Handler: POST /send-otp (email)
    Handler->>Service: getUserByEmail(email)
    Service->>DB: Query documents (email filter)
    DB-->>Service: User document | null
    Service-->>Handler: User | null

    alt User exists
        Handler->>Handler: Check elapsed time since last_otp_sent
        alt Within 60 seconds
            Handler-->>Client: 429 Too Many Requests (wait time)
        else Beyond 60 seconds
            Handler->>Handler: Generate OTP
            Handler->>Mailer: Send OTP email
            Mailer-->>Handler: Email sent
            Handler->>Service: updateUserLastOtpSent(userId, timestamp)
            Service->>DB: Update last_otp_sent
            DB-->>Service: Update confirmed
            Handler-->>Client: 200 OK (OTP sent)
        end
    else User not found
        Handler->>Handler: Generate OTP
        Handler->>Mailer: Send OTP email
        Mailer-->>Handler: Email sent
        Handler-->>Client: 200 OK (OTP sent)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through manifests, trimmed leaves away,
I guarded the burrow so OTPs may play,
Sixty seconds I watch, then I nudge a small bell,
Mail wings take flight — all is tidy and well! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly summarizes the main change: implementing a 60-second backend rate-limiting mechanism for OTP requests, which matches the primary objective and the substantial code changes in the send-otp function.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@appwrite.json`:
- Around line 101-109: The users collection with ID 64a52f0a6c41ded09def is
missing the required attribute last_otp_sent used for OTP rate-limiting; update
the collection schema to add a new attribute named last_otp_sent (type:
datetime/timestamp or string in ISO format depending on your DB schema
constraints), ensure it is nullable by default and indexed if you query by it,
and deploy/migrate the collection so the UsersCollectionID referenced by the
environment variable is updated before merging.

In `@functions/send-otp/src/appwrite.js`:
- Around line 41-55: The try-catch in updateUserLastOtpSent is contradictory
(comment says "log but don't throw" yet it re-throws), so remove the unnecessary
try-catch and let errors propagate to the caller; locate the async method
updateUserLastOtpSent that calls this.databases.updateDocument and delete the
try-catch block around that call (or alternatively replace the catch body with a
proper log and return if you intend to swallow errors), ensuring main.js
continues to handle errors as currently designed.
🧹 Nitpick comments (1)
functions/send-otp/src/appwrite.js (1)

27-39: Fail-open design is acceptable but consider logging errors.

The method silently returns null for any error, which is a reasonable fail-open approach for rate-limiting (users can still authenticate if the DB is temporarily unavailable). However, consider logging the error before returning null to aid debugging connectivity issues.

♻️ Optional: Add error logging
     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 if user not found or any error occurs (fail-open for rate-limiting)
+            console.warn(`getUserByEmail failed for lookup: ${e.message}`);
             return null;
         }
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 795f29b and e065643.

📒 Files selected for processing (3)
  • appwrite.json
  • functions/send-otp/src/appwrite.js
  • functions/send-otp/src/main.js
🧰 Additional context used
🧬 Code graph analysis (2)
functions/send-otp/src/main.js (1)
functions/database-cleaner/src/main.js (1)
  • appwrite (15-15)
functions/send-otp/src/appwrite.js (1)
functions/join-room/src/main.js (1)
  • userId (19-19)
🔇 Additional comments (3)
functions/send-otp/src/appwrite.js (1)

1-1: LGTM - Query import added correctly.

functions/send-otp/src/main.js (2)

12-13: LGTM - Environment variable validation added.


23-40: Rate-limiting implementation looks correct.

The logic properly checks for existing users with a last_otp_sent timestamp and enforces the 60-second cooldown. The 429 response with remaining seconds provides good UX feedback.

Note that new users (where userDoc is null) or users without a last_otp_sent field will bypass rate-limiting on their first OTP request, which is the expected behavior.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

appwrite.json Outdated
Comment on lines 101 to 109
},
{
"key": "UserDataDatabaseID",
"value": "64a1319104a149e16f5c"
},
{
"key": "UsersCollectionID",
"value": "64a52f0a6c41ded09def"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's check if appwrite.json exists and get basic info
if [ -f appwrite.json ]; then
  echo "=== File structure ===" 
  wc -l appwrite.json
  echo ""
  echo "=== Lines 95-115 to see context ===" 
  sed -n '95,115p' appwrite.json
else
  echo "appwrite.json not found in repository root"
fi

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 804


🏁 Script executed:

#!/bin/bash
# Check if jq is available and examine the file structure
if command -v jq &> /dev/null; then
  echo "=== Checking collections ===" 
  cat appwrite.json | jq '.collections[] | {id: ."$id", name: .name, attributes: [.attributes[] | .key]}' 2>/dev/null
else
  echo "jq not available, trying alternative approach"
  cat appwrite.json | head -200
fi

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 3738


🏁 Script executed:

#!/bin/bash
# Check for the specific collection ID 64a52f0a6c41ded09def and its attributes
cat appwrite.json | jq '.collections[] | select(."$id" == "64a52f0a6c41ded09def") | {id: ."$id", name: .name, attributes: [.attributes[] | {key: .key, type: .type}]}' 2>/dev/null

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 776


Critical: Missing required last_otp_sent attribute in users collection.

The environment variables UserDataDatabaseID and UsersCollectionID are correctly configured. However, the users collection (64a52f0a6c41ded09def) is missing the last_otp_sent attribute required for the rate-limiting feature. Current collection attributes are: name, dob, username, profileImageUrl, email, profileImageID, ratingCount, ratingTotal, followers, friends. Add the last_otp_sent attribute to the collection schema before merging.

🤖 Prompt for AI Agents
In `@appwrite.json` around lines 101 - 109, The users collection with ID
64a52f0a6c41ded09def is missing the required attribute last_otp_sent used for
OTP rate-limiting; update the collection schema to add a new attribute named
last_otp_sent (type: datetime/timestamp or string in ISO format depending on
your DB schema constraints), ensure it is nullable by default and indexed if you
query by it, and deploy/migrate the collection so the UsersCollectionID
referenced by the environment variable is updated before merging.

@M4dhav
Copy link
Contributor

M4dhav commented Jan 15, 2026

Did you test your PR and this function locally?

@cannonerd007
Copy link
Author

Did you test your PR and this function locally?

Hi @M4dhav, to be honest, I haven't performed a live local execution of the function yet as I wanted to get the logic up for review first.

However, I have verified the logic through static analysis and ensured it passed the CI build checks. I’m currently setting up my local environment to perform a live test and confirm the 429 response and timestamp update behavior. I'll update you as soon as I've confirmed it on my end!

@M4dhav
Copy link
Contributor

M4dhav commented Jan 15, 2026

Did you test your PR and this function locally?

Hi @M4dhav, to be honest, I haven't performed a live local execution of the function yet as I wanted to get the logic up for review first.

However, I have verified the logic through static analysis and ensured it passed the CI build checks. I’m currently setting up my local environment to perform a live test and confirm the 429 response and timestamp update behavior. I'll update you as soon as I've confirmed it on my end!

Please note that for the future, all code should be tested locally before opening PRs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@appwrite.json`:
- Around line 13-15: The function currently only has the "users.read" scope; add
the required database scopes so the API key can perform
listDocuments/createDocument/updateDocument. Update the "scopes" array in
appwrite.json to include the appropriate database permissions for your Appwrite
version (e.g., "databases.read" and "databases.write" or, if using the newer
rows model, "rows.read" and "rows.write") alongside "users.read" so the function
can list, create, and update documents.

In `@functions/send-otp-backup/src/appwrite.js`:
- Around line 41-55: The updateUserLastOtpSent method currently re-throws errors
despite the comment and PR intent; change the catch block to log the error
instead of throwing so DB update failures don't interrupt authentication. In the
catch, call the existing logger (or console) with a clear message including the
function name, userId and timestamp and the caught error (e.g.,
"updateUserLastOtpSent failed for userId=... timestamp=..."), then swallow the
error (do not re-throw) so the primary flow continues; keep the call to
this.databases.updateDocument and the same payload structure.
- Around line 27-39: The getUserByEmail function currently swallows all
exceptions and returns null, making network/auth/config errors indistinguishable
from "user not found"; change it to distinguish those cases by catching errors,
logging the error (include the exception and context such as email and the
DB/collection IDs), and either rethrowing the error or returning a distinct
error result so callers (e.g., OTP rate-limiter) can treat failures differently
from "no user"; update the catch around this.databases.listDocuments in
getUserByEmail to log the error and propagate it (or return a structured
failure) instead of silently returning null, while keeping the existing
successful-path behavior of returning the document when found.

In `@functions/send-otp-backup/src/main.js`:
- Around line 6-14: The environment variable list passed to throwIfMissing is
inconsistent with the main send-otp function; update the array in the
throwIfMissing call to match the main function by removing
"VERIFICATION_DATABASE_ID" and "OTP_COLLECTION_ID" and adding
"APPWRITE_FUNCTION_PROJECT_ID", while keeping "APPWRITE_API_KEY", "SENDER_MAIL",
"SENDER_PASSWORD", "UserDataDatabaseID", and "UsersCollectionID" so both
functions require the same set of env vars and are drop-in compatible.

In `@functions/send-otp/README.md`:
- Around line 1-46: Update the README to reflect the actual send-otp function:
replace the generic /ping and / endpoints with the real OTP endpoint(s) and
sample request/response payloads, document rate-limiting behavior (e.g., limits,
window, and error responses), list all required env vars used in src/main.js
(APPWRITE_API_KEY, APPWRITE_FUNCTION_PROJECT_ID, SENDER_MAIL, SENDER_PASSWORD,
UserDataDatabaseID, UsersCollectionID, and any others present), and fix the
runtime to match appwrite.json (node-16.0) or update appwrite.json to Node 18 if
intended; ensure configuration (entrypoint, build commands, timeout,
permissions) matches the project files and include minimal examples for success
and error responses.

In `@functions/send-otp/src/appwrite.js`:
- Around line 15-25: createOtpDocument is writing OTPs to
process.env.UserDataDatabaseID / process.env.UsersCollectionID while verify-otp
expects them in process.env.VERIFICATION_DATABASE_ID /
process.env.OTP_COLLECTION_ID; change the database/collection arguments in
createOtpDocument to use process.env.VERIFICATION_DATABASE_ID and
process.env.OTP_COLLECTION_ID so OTP documents are stored where verifyOtp
retrieves them, keeping the same parameters (otpId, otp, date) and call to
this.databases.createDocument in the createOtpDocument method.
♻️ Duplicate comments (4)
functions/send-otp/src/appwrite.js (1)

41-55: Inconsistent error handling: comment contradicts code.

The comment on line 52 says "log but don't throw" but line 53 immediately re-throws the exception. This try-catch block is effectively a no-op.

🔧 Proposed fix: Remove unnecessary try-catch

Since the caller in main.js already handles the error with its own try-catch, let the error propagate:

     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;
-        }
+        await this.databases.updateDocument(
+            process.env.UserDataDatabaseID,
+            process.env.UsersCollectionID,
+            userId,
+            {
+                last_otp_sent: timestamp
+            }
+        );
     }
appwrite.json (1)

1-25: Missing last_otp_sent attribute in users collection.

As previously noted, the rate-limiting feature requires a last_otp_sent attribute in the users collection schema. Ensure this attribute is added to the Appwrite collection before deploying.

functions/send-otp-backup/src/main.js (1)

23-40: Rate-limiting gap for registration flow remains unaddressed.

The rate-limiting check only applies when userDoc exists. For registration flows where the user document hasn't been created yet, getUserByEmail returns null, completely bypassing rate limiting. An attacker could spam OTP requests for non-existent accounts without restriction.

Consider implementing email-based rate limiting (e.g., tracking OTP requests per email in a separate collection) that applies regardless of user existence.

functions/send-otp/src/main.js (1)

22-39: Rate-limiting gap for registration flow still present.

As flagged in the previous review, the rate-limiting check only applies when userDoc exists. Registration flows (where the user doesn't exist yet) bypass rate limiting entirely, allowing attackers to spam OTP requests for non-existent email addresses.

This is a known issue from the prior review that should be addressed before merging.

🧹 Nitpick comments (7)
functions/send-otp/package.json (1)

2-4: Package name inconsistent with function.

The package name "starter-template" doesn't match the actual function name "send-otp". This appears to be leftover from scaffolding.

🔧 Proposed fix
 {
-  "name": "starter-template",
+  "name": "send-otp",
   "version": "1.0.0",
-  "description": "",
+  "description": "OTP sending function with 60s rate limiting",
appwrite.json (1)

7-7: Runtime node-16.0 is end-of-life.

Node.js 16 reached end-of-life in September 2023. Consider upgrading to a supported LTS version like node-18.0 or node-20.0 for security patches and continued support.

🔧 Proposed fix
-            "runtime": "node-16.0",
+            "runtime": "node-18.0",
functions/send-otp-backup/package.json (1)

1-17: Package name should reflect the backup function identity.

The package name is "send-otp" but this is in the send-otp-backup directory. This could cause confusion or conflicts. Consider renaming to "send-otp-backup" for clarity.

Additionally, there's a significant version mismatch with the main send-otp function:

  • Backup: node-appwrite@^17.0.0, nodemailer@^6.9.5
  • Main: node-appwrite@20.3.0, nodemailer@7.0.12

This inconsistency could lead to different behavior between the two implementations.

Proposed fix
 {
-    "name": "send-otp",
+    "name": "send-otp-backup",
     "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"
+        "node-appwrite": "^20.3.0",
+        "nodemailer": "^7.0.12"
     },
     "devDependencies": {
         "prettier": "^3.0.0"
     }
 }
functions/send-otp-backup/README.md (1)

36-86: Documentation is incomplete for the new rate-limiting feature.

The README is missing several important details:

  1. Missing environment variables: The code requires UserDataDatabaseID, UsersCollectionID, and (per the main function) APPWRITE_FUNCTION_PROJECT_ID, but these are not documented.

  2. Missing 429 response documentation: The rate-limiting feature returns HTTP 429 when requests are made within 60 seconds, but only the 200 response is documented.

Consider updating the documentation to reflect the complete configuration and response behaviors.

functions/send-otp-backup/src/main.js (1)

42-42: Consider using cryptographically secure random number generation.

Math.random() is not cryptographically secure. While a 6-digit OTP has limited entropy regardless, using crypto.randomInt() would be a more secure approach for security-sensitive operations.

Suggested improvement
+import { randomInt } from 'crypto';
+
-        const otp = String(Math.floor(100000 + Math.random() * 900000));
+        const otp = String(randomInt(100000, 1000000));
functions/send-otp-backup/src/appwrite.js (2)

4-13: Constructor looks good, but missing env vars could cause silent issues.

If APPWRITE_FUNCTION_PROJECT_ID or APPWRITE_API_KEY are undefined, the client will be configured with undefined values, potentially causing cryptic errors later. Consider validating these required env vars or using the throwIfMissing utility (available in utils.js per the PR summary) at initialization.

💡 Suggested improvement
 constructor() {
+    const projectId = process.env.APPWRITE_FUNCTION_PROJECT_ID;
+    const apiKey = process.env.APPWRITE_API_KEY;
+    if (!projectId || !apiKey) {
+        throw new Error('Missing required Appwrite environment variables');
+    }
     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);
+        .setProject(projectId)
+        .setKey(apiKey);

     this.databases = new Databases(client);
 }

15-25: Inconsistent environment variable naming convention.

This method uses VERIFICATION_DATABASE_ID and OTP_COLLECTION_ID (SCREAMING_SNAKE_CASE), while getUserByEmail and updateUserLastOtpSent use UserDataDatabaseID and UsersCollectionID (PascalCase). Consider standardizing the naming convention across the codebase for maintainability.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e065643 and a734249.

⛔ Files ignored due to path filters (2)
  • functions/send-otp-backup/package-lock.json is excluded by !**/package-lock.json
  • functions/send-otp/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • appwrite.json
  • functions/send-otp-backup/README.md
  • functions/send-otp-backup/package.json
  • functions/send-otp-backup/src/appwrite.js
  • functions/send-otp-backup/src/mail.js
  • functions/send-otp-backup/src/main.js
  • functions/send-otp-backup/src/utils.js
  • functions/send-otp/.gitignore
  • functions/send-otp/.prettierrc.json
  • functions/send-otp/README.md
  • functions/send-otp/package.json
  • functions/send-otp/src/appwrite.js
  • functions/send-otp/src/main.js
✅ Files skipped from review due to trivial changes (2)
  • functions/send-otp/.gitignore
  • functions/send-otp/.prettierrc.json
🧰 Additional context used
🧬 Code graph analysis (5)
functions/send-otp-backup/src/mail.js (2)
functions/send-otp-backup/src/main.js (2)
  • recipientEmail (21-21)
  • otp (42-42)
functions/send-otp/src/main.js (2)
  • recipientEmail (20-20)
  • otp (41-41)
functions/send-otp-backup/src/appwrite.js (3)
functions/send-otp-backup/src/main.js (1)
  • otp (42-42)
functions/send-otp/src/main.js (1)
  • otp (41-41)
functions/join-room/src/main.js (1)
  • userId (19-19)
functions/send-otp-backup/src/main.js (2)
functions/send-otp-backup/src/utils.js (2)
  • throwIfMissing (1-11)
  • throwIfMissing (1-11)
functions/send-otp/src/main.js (11)
  • throwIfMissing (5-66)
  • appwrite (15-15)
  • mailService (16-16)
  • recipientEmail (20-20)
  • userDoc (23-23)
  • lastOtpSentTime (26-26)
  • currentTime (27-27)
  • timeDifference (28-28)
  • remainingSeconds (31-31)
  • otp (41-41)
  • currentDate (46-46)
functions/send-otp-backup/src/utils.js (2)
functions/send-otp-backup/src/main.js (1)
  • throwIfMissing (5-67)
functions/send-otp/src/main.js (1)
  • throwIfMissing (5-66)
functions/send-otp/src/appwrite.js (1)
functions/join-room/src/main.js (1)
  • userId (19-19)
🪛 markdownlint-cli2 (0.18.1)
functions/send-otp/README.md

9-9: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


21-21: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

functions/send-otp-backup/README.md

9-9: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


16-16: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (9)
functions/send-otp-backup/src/utils.js (1)

1-11: LGTM!

The utility function is clean and serves its purpose for environment variable validation. Note that the condition !obj[key] will treat empty strings, 0, and false as missing, which is appropriate for the required API keys and database IDs being validated in main.js.

functions/send-otp/src/appwrite.js (1)

27-39: LGTM!

The getUserByEmail method correctly uses Query.equal and Query.limit(1) to fetch user documents. Returning null on errors is appropriate since the caller in main.js handles this case gracefully by skipping the rate limit check for non-existent users.

functions/send-otp/package.json (1)

11-12: No action needed. MailService uses the Gmail SMTP transport (service: 'gmail'), which has no breaking changes between nodemailer v6 and v7. The breaking change in v7 only affects the SES transport (switched to AWS SDK v3), which is not used here.

Likely an incorrect or invalid review comment.

functions/send-otp-backup/README.md (1)

30-32: Build command appears incorrect for serverless function.

The build command npm install && npm run start would attempt to execute the function during build. For Appwrite functions, typically only npm install is needed as the build command.

functions/send-otp-backup/src/main.js (1)

52-59: Graceful error handling for timestamp update is appropriate.

The try-catch wrapper ensures that failures in updating the last_otp_sent timestamp don't break the primary OTP flow. This is the correct approach for non-critical metadata updates.

functions/send-otp-backup/src/mail.js (1)

3-23: Implementation looks correct.

The MailService correctly initializes a nodemailer transport with Gmail and provides an async sendMail method. Errors will propagate to the caller which handles them appropriately in main.js.

functions/send-otp/src/main.js (2)

6-13: Environment variable changes look appropriate.

The updated required environment variables (APPWRITE_FUNCTION_PROJECT_ID, UserDataDatabaseID, UsersCollectionID) align with the new AppwriteService methods for user lookup and timestamp updates.


51-59: Timestamp update with graceful error handling is well implemented.

The try-catch wrapper correctly ensures that timestamp update failures don't interrupt the primary OTP flow, with appropriate warning logging.

functions/send-otp-backup/src/appwrite.js (1)

57-58: LGTM!

Default export is appropriate for this service class.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +27 to +39
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;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Swallowing all errors may mask real issues and bypass rate limiting.

Returning null for any error means network failures, authentication issues, or misconfiguration are indistinguishable from "user not found." For the rate-limiting feature, if this query fails, the calling code may allow an OTP to be sent (bypassing the rate limit).

Consider logging the error for observability, or distinguishing between "not found" and actual failures.

🔧 Suggested improvement
 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
+        // Log error for debugging but return null to allow flow to continue
+        console.error('getUserByEmail failed:', e.message);
         return null;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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) {
// Log error for debugging but return null to allow flow to continue
console.error('getUserByEmail failed:', e.message);
return null;
}
}
🤖 Prompt for AI Agents
In `@functions/send-otp-backup/src/appwrite.js` around lines 27 - 39, The
getUserByEmail function currently swallows all exceptions and returns null,
making network/auth/config errors indistinguishable from "user not found";
change it to distinguish those cases by catching errors, logging the error
(include the exception and context such as email and the DB/collection IDs), and
either rethrowing the error or returning a distinct error result so callers
(e.g., OTP rate-limiter) can treat failures differently from "no user"; update
the catch around this.databases.listDocuments in getUserByEmail to log the error
and propagate it (or return a structured failure) instead of silently returning
null, while keeping the existing successful-path behavior of returning the
document when found.

Comment on lines +41 to +55
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;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Comment contradicts the actual code behavior.

The comment states "If update fails, log but don't throw (non-critical)" but the code does the opposite: it throws the error and performs no logging. The PR description states that "database errors during the update do not interrupt the primary authentication flow," which aligns with the comment, not the code.

🐛 Fix to align code with intended behavior per PR description
 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;
+        // If update fails, log but don't throw (non-critical for auth flow)
+        console.error('Failed to update last_otp_sent:', e.message);
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
}
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 for auth flow)
console.error('Failed to update last_otp_sent:', e.message);
}
}
🤖 Prompt for AI Agents
In `@functions/send-otp-backup/src/appwrite.js` around lines 41 - 55, The
updateUserLastOtpSent method currently re-throws errors despite the comment and
PR intent; change the catch block to log the error instead of throwing so DB
update failures don't interrupt authentication. In the catch, call the existing
logger (or console) with a clear message including the function name, userId and
timestamp and the caught error (e.g., "updateUserLastOtpSent failed for
userId=... timestamp=..."), then swallow the error (do not re-throw) so the
primary flow continues; keep the call to this.databases.updateDocument and the
same payload structure.

Comment on lines +6 to +14
throwIfMissing(process.env, [
"APPWRITE_API_KEY",
"VERIFICATION_DATABASE_ID",
"OTP_COLLECTION_ID",
"SENDER_MAIL",
"SENDER_PASSWORD",
"UserDataDatabaseID",
"UsersCollectionID",
]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Environment variable requirements differ from main send-otp function.

The backup function requires VERIFICATION_DATABASE_ID and OTP_COLLECTION_ID, but the main send-otp function (per the AI summary) removed these in favor of only UserDataDatabaseID and UsersCollectionID. Also, the main function requires APPWRITE_FUNCTION_PROJECT_ID which is missing here.

This inconsistency means the two functions aren't drop-in replacements for each other.

🤖 Prompt for AI Agents
In `@functions/send-otp-backup/src/main.js` around lines 6 - 14, The environment
variable list passed to throwIfMissing is inconsistent with the main send-otp
function; update the array in the throwIfMissing call to match the main function
by removing "VERIFICATION_DATABASE_ID" and "OTP_COLLECTION_ID" and adding
"APPWRITE_FUNCTION_PROJECT_ID", while keeping "APPWRITE_API_KEY", "SENDER_MAIL",
"SENDER_PASSWORD", "UserDataDatabaseID", and "UsersCollectionID" so both
functions require the same set of env vars and are drop-in compatible.

@cannonerd007
Copy link
Author

Did you test your PR and this function locally?

Hi @M4dhav, to be honest, I haven't performed a live local execution of the function yet as I wanted to get the logic up for review first.
However, I have verified the logic through static analysis and ensured it passed the CI build checks. I’m currently setting up my local environment to perform a live test and confirm the 429 response and timestamp update behavior. I'll update you as soon as I've confirmed it on my end!

Please note that for the future, all code should be tested locally before opening PRs

Hi @M4dhav, I apologize for not providing the local test results earlier. I have now successfully set up the local environment and verified the rate-limiting logic.

As shown in the attached execution logs, the function now correctly returns a 429 Too Many Requests status code when a user attempts to request a second OTP within the 60-second window. I have also verified that the database correctly creates and updates the last_otp_sent timestamp to manage this behavior.

Thank you for the guidance, and I will ensure all future PRs are tested locally before submission. Ready for your review!

Tested successfully

Copy link
Contributor

@M4dhav M4dhav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the send_otp_backup funtion?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@appwrite.json`:
- Line 7: Update the Node runtime declared in appwrite.json by replacing the
outdated "node-16.0" value with a supported LTS version string such as "node-20"
or "node-22" (e.g., change the "runtime" property from "node-16.0" to "node-22"
or "node-20"); ensure any deployment/CI configs and Dockerfiles that rely on the
runtime are checked for compatibility and updated consistently after changing
the "runtime" value.

In `@functions/send-otp/src/appwrite.js`:
- Around line 27-39: getUserByEmail is querying
VERIFICATION_DATABASE_ID/OTP_COLLECTION_ID which currently mixes ephemeral OTP
records with persistent user rate-limit metadata; separate concerns by creating
a dedicated users collection (e.g., USERS_COLLECTION_ID) to store per-email
state like last_otp_sent and change getUserByEmail to query that users
collection for the email (or, if you must keep a single collection, change
getUserByEmail to return the most-recent OTP document by email sorted by
timestamp); also fix updateUserLastOtpSent so it does not re-throw exceptions
(remove the re-throw and handle/log the error or return a failure indicator) to
match the "don't throw" comment.
♻️ Duplicate comments (2)
functions/send-otp/src/appwrite.js (1)

41-55: Inconsistent error handling remains unaddressed.

The comment on line 52 says "log but don't throw" but line 53 immediately re-throws. This was flagged in a previous review and still needs to be fixed.

functions/send-otp/src/main.js (1)

22-39: Rate limiting bypassed for unregistered emails.

When userDoc is null (no existing document found), the rate-limiting check is entirely skipped. This was flagged in a previous review—an attacker can spam OTP requests for any email address that doesn't have an existing record.

Consider tracking rate limits per email in a dedicated collection, independent of user document existence.

appwrite.json Outdated
"name": "Upcoming Rooms Time Checker",
"$id": "6968b4f000222d5d8d93",
"name": "send-otp",
"runtime": "node-16.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

**Consider upgrading Node.js runtime.**Node.js 16 reached end-of-life on September 11, 2023, and "there have not been any bug fixes or security updates to these versions." Appwrite now supports Node.js 22 runtime (22.18).

Upgrade to a supported LTS version (Node.js 20 or 22) for security patches and continued support.

🔧 Suggested fix
-            "runtime": "node-16.0",
+            "runtime": "node-22.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"runtime": "node-16.0",
"runtime": "node-22.0",
🤖 Prompt for AI Agents
In `@appwrite.json` at line 7, Update the Node runtime declared in appwrite.json
by replacing the outdated "node-16.0" value with a supported LTS version string
such as "node-20" or "node-22" (e.g., change the "runtime" property from
"node-16.0" to "node-22" or "node-20"); ensure any deployment/CI configs and
Dockerfiles that rely on the runtime are checked for compatibility and updated
consistently after changing the "runtime" value.

Comment on lines +27 to +39
async getUserByEmail(email) {
try {
const response = await this.databases.listDocuments(
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;
} catch (e) {
// Return null if user not found or any error occurs
return null;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Locating appwrite.json ==="
find . -name "appwrite.json" -o -name "*appwrite*" -type f | head -20

echo ""
echo "=== Checking for database/collection configuration files ==="
find . -name "*.json" | grep -E "(config|db|collection|schema)" | head -20

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 345


🏁 Script executed:

#!/bin/bash

echo "=== Searching for collection ID references in JS files ==="
rg "COLLECTION_ID|DATABASE_ID" functions/send-otp/src/ -A 2 -B 2

echo ""
echo "=== Searching for users collection or user data references ==="
rg -i "(users|user_data|profile).*collection" functions/send-otp/src/ -B 2 -A 2

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 1825


🏁 Script executed:

#!/bin/bash

echo "=== Examining the full appwrite.js file to see all collection usage ==="
cat -n functions/send-otp/src/appwrite.js

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 2272


🏁 Script executed:

#!/bin/bash

echo "=== Searching for last_otp_sent field usage across codebase ==="
rg "last_otp_sent" . -n

echo ""
echo "=== Searching for updateUserLastOtpSent implementation ==="
rg "updateUserLastOtpSent" . -B 5 -A 10

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 2881


🏁 Script executed:

#!/bin/bash

echo "=== Examining appwrite.json for collection schemas ==="
cat appwrite.json | head -200

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 902


🏁 Script executed:

#!/bin/bash

echo "=== Searching for main.js to understand the flow ==="
cat -n functions/send-otp/src/main.js | head -80

Repository: AOSSIE-Org/Resonate-Backend

Length of output: 2942


Data model design issue: OTP_COLLECTION_ID dual-purposes for both OTP records and user rate-limit metadata.

getUserByEmail queries VERIFICATION_DATABASE_ID/OTP_COLLECTION_ID, which stores both OTP data (created per-request) and user metadata (last_otp_sent). This conflates concerns:

  1. Unclear semantics: Documents are created by unique otpID but queried/updated by email, conflating per-request OTP records with user state.
  2. Rate limiting fails on first request: New users have no existing document, so rate limiting is bypassed for their first OTP request (only works from second request onward).
  3. Fragile design: Storing user rate-limit metadata in a collection designed for ephemeral OTP records couples unrelated concerns.

Separate user state (email, last_otp_sent) into a dedicated users collection, or redesign the rate-limiting approach to store state alongside each OTP record (and check the most recent OTP's timestamp instead of querying by email).

Also fix error handling in updateUserLastOtpSent (lines 51–55): comment says "don't throw" but code re-throws the exception.

🤖 Prompt for AI Agents
In `@functions/send-otp/src/appwrite.js` around lines 27 - 39, getUserByEmail is
querying VERIFICATION_DATABASE_ID/OTP_COLLECTION_ID which currently mixes
ephemeral OTP records with persistent user rate-limit metadata; separate
concerns by creating a dedicated users collection (e.g., USERS_COLLECTION_ID) to
store per-email state like last_otp_sent and change getUserByEmail to query that
users collection for the email (or, if you must keep a single collection, change
getUserByEmail to return the most-recent OTP document by email sorted by
timestamp); also fix updateUserLastOtpSent so it does not re-throw exceptions
(remove the re-throw and handle/log the error or return a failure indicator) to
match the "don't throw" comment.

@cannonerd007
Copy link
Author

Hey @M4dhav, sorry for the delay!

To answer your question—that send-otp-backup folder was just a local backup I made while testing that I accidentally included in the commit. I've gone ahead and deleted it now.

I also took a look at the automated feedback and finished polishing the rest of the PR. I restored the standard variable names like VERIFICATION_DATABASE_ID, updated the runtime to Node 18, and fixed the database scopes in appwrite.json. I also did a full write-up for the README so the environment variables and rate-limiting are properly documented.

Everything is passing the checks now and should be ready for review!

@cannonerd007 cannonerd007 requested a review from M4dhav January 22, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants