Skip to content

Conversation

@spathon
Copy link
Member

@spathon spathon commented Feb 5, 2026

Preparation for Next.js version

Summary by CodeRabbit

  • New Features

    • Consolidated, idempotent database migration and README.
    • Orphaned file cleanup script; media files now cleaned up when records are removed.
  • Bug Fixes

    • Deleted-user handling shows “[deleted]” for orphaned comments.
    • Improved file path safety checks.
  • Database & Performance

    • Schema updates with cascading integrity and added performance indexes.
  • Changes

    • Rate-limiting / account lockout fields added.
    • Post slugs now optional; group descriptions required.

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Consolidates multiple DB migrations into an idempotent SQL script, updates Sequelize models (cascade FKs, schema fields, nullable changes), adds filesystem-safe media cleanup hooks and a safePath utility, and introduces a script to locate and optionally delete orphaned upload/thumbnail files.

Changes

Cohort / File(s) Summary
Migrations & Documentation
migrations/combined.sql, migrations/README.md, migrations/SCHEMA_UPDATE_SUMMARY.md
Adds a single idempotent migration consolidating prior migrations: rate-limiting fields, FK changes (CASCADE/SET NULL), index additions, backfills, and removal of posts.image_id; includes run, verify, and rollback guidance.
User model
models/users.js
Adds failed_login_attempts, last_failed_login, locked_until; removes old_uid and avatarpath.
Cascade FK updates (multiple models)
models/activation.js, models/blog.js, models/groupsUsers.js, models/media.js, models/messages.js, models/messagesSubscribers.js, models/posts.js
Adds onDelete: 'CASCADE'/onUpdate: 'CASCADE' to many user/post-related foreign keys to standardize deletion/update propagation.
Comment model
models/comment.js
Makes user_id nullable and changes FK behavior to onDelete: 'SET NULL', onUpdate: 'CASCADE'.
Groups model
models/groups.js
Changes description from nullable to required (allowNull: false).
Media associations & cleanup
models/index.js, models/media.js
Removes media_relations usage, adds media.post_id association, and a beforeDestroy hook to delete upload files and thumbnails safely with recursive thumbnail deletion helper.
GraphQL: schema & resolvers
routes/graphql/typeDefs/types.graphql, routes/graphql/mutations/posts.js, routes/graphql/mutations/media.js, routes/graphql/queries.js, routes/graphql/types.js
Makes Post.slug nullable; sanitizes title input and slug generation on create/edit; parameterizes a postsCommented query; returns a DELETED_USER and "[deleted]" content when comment user_id is absent; removes file unlink from mutation (handled by model hook).
Path safety & routes
utils/safePath.js, routes/auth.js
Adds safePath utility to prevent path traversal and replaces direct path.resolve/path.join usages in auth file-serving routes.
Orphaned file cleanup script
scripts/cleanup-orphaned-files.js
New standalone script to detect (and optionally delete) orphaned uploads and thumbnails by comparing filesystem files with Media DB records; supports dry-run, logging, and summaries.
Auth route numeric validation fix
routes/auth.js
Adjusts numeric-parameter validation (thumb route) to use OR instead of AND to correctly reject non-numeric inputs.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Application
    participant DB as Database (Media)
    participant MediaModel as Media Model / Hook
    participant FS as File System
    participant Logger as Logger

    Client->>DB: request delete media record
    DB->>MediaModel: trigger beforeDestroy hook
    activate MediaModel

    MediaModel->>Logger: check UPLOADS_DIR / THUMBNAIL_DIR
    alt UPLOADS_DIR not set
        MediaModel->>Logger: log error, skip cleanup
        MediaModel-->>DB: continue destroy
    else
        MediaModel->>MediaModel: resolve paths via safePath
        alt path traversal detected
            MediaModel->>Logger: log error, skip cleanup
            MediaModel-->>DB: continue destroy
        else
            MediaModel->>FS: fsUnlink(originalFile)
            FS-->>MediaModel: success / error (logged)
            alt THUMBNAIL_DIR set
                MediaModel->>FS: scan thumbnails dir recursively
                loop for each matching thumbnail
                    FS->>MediaModel: found file
                    MediaModel->>FS: fsUnlink(thumbnail)
                    FS-->>MediaModel: success / error (logged)
                end
            end
            MediaModel-->>DB: cleanup complete, allow destroy
        end
    end

    deactivate MediaModel
    DB->>DB: remove record
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Simplify DB #44 — Alters Media↔Post relationship by removing media_relations and introducing media.post_id; directly related to association and migration changes here.

Suggested reviewers

  • Synchro

Poem

🐰 I hopped through rows of schema trees,
I nibbled bugs and cleaned lost keys,
Safe paths and cascades, tidy and bright,
No orphaned files to haunt the night.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'New version db changes' is vague and generic, using non-descriptive terms that fail to convey meaningful information about the specific database schema updates, migrations, and model changes included in this comprehensive changeset. Replace with a more specific title that highlights the primary database changes, such as 'Add rate limiting, cascade deletions, and database migrations' or 'Consolidate database migrations and update Sequelize models for deletion handling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch new-version-db-changes

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: 17

🤖 Fix all issues with AI agents
In `@IMPLEMENTATION_SUMMARY.md`:
- Around line 340-346: The SUPPORT section in IMPLEMENTATION_SUMMARY.md
references an internal plan file `.claude/plans/iridescent-painting-quail.md`
which may not exist; either confirm and add that file to the repo or
remove/replace the reference. Open IMPLEMENTATION_SUMMARY.md, locate the
"Support" block containing `.claude/plans/iridescent-painting-quail.md`, and
either (a) point to an existing document or include the missing plan in
`.claude/plans/` with the same name, or (b) remove the broken path and replace
it with a valid internal doc or a generic troubleshooting link. Ensure the final
text no longer points to a non-existent
`.claude/plans/iridescent-painting-quail.md`.

In `@migrations/002_update_comments_foreign_key.sql`:
- Around line 5-7: Replace the hard-coded DROP FOREIGN KEY with a conditional
lookup of the actual FK name in INFORMATION_SCHEMA and drop it only if found:
query INFORMATION_SCHEMA.TABLE_CONSTRAINTS / KEY_COLUMN_USAGE for the constraint
on table `comments` referencing `users` (or specifically the `user_id` column),
capture the constraint_name, and execute an ALTER TABLE `comments` DROP FOREIGN
KEY <constraint_name> when present (mirroring the safe-to-rerun pattern used in
migrations 004 and 007) so the migration will succeed even if the FK has a
different name.

In `@migrations/003_add_post_id_to_media.sql`:
- Around line 1-7: Add a backfill step to the migration that populates
media.post_id from the legacy media_relations table before enforcing any FK:
query media_relations for rows linking media_id → post_id (and decide/mention
how to pick one if multiple relations exist), update the media table's post_id
for matching media.id where post_id is NULL using that value, and run this
inside a transaction (or with appropriate batching) so existing links are
preserved; reference the media table, the new post_id column, and the legacy
media_relations table/media_id/post_id columns when adding the update/backfill
and only add/enable the FK afterward.

In `@migrations/006_add_user_deletion_cascades.sql`:
- Around line 13-17: The ALTER TABLE adding foreign key fk_posts_user_id on
table posts is not idempotent and will fail if that constraint already exists;
update the migration to query INFORMATION_SCHEMA.TABLE_CONSTRAINTS
(CONSTRAINT_SCHEMA = DATABASE(), TABLE_NAME = 'posts', CONSTRAINT_NAME =
'fk_posts_user_id') and only run the ALTER TABLE ... ADD CONSTRAINT
fk_posts_user_id ... ON DELETE CASCADE ON UPDATE CASCADE when the count is 0
(use dynamic SQL with PREPARE/EXECUTE as in migration 007 pattern), otherwise
skip or log that the constraint already exists; apply the same existence-check
pattern for any other constraints added in this migration.
- Around line 9-61: Migration 006 adds FK constraints unconditionally; update it
to first check for and/or drop any existing foreign keys before adding each
constraint (follow the pattern used in migrations 002 and 004). For each
constraint (fk_posts_user_id on posts, fk_media_user_id on media,
fk_messages_user_id on messages, fk_messages_subscribers_user_id on
messages_subscribers, fk_groups_users_user_id on groups_users, fk_blog_author_id
on blog, fk_activations_user_id on activations) either (a) DROP FOREIGN KEY if
it exists, or (b) query
INFORMATION_SCHEMA.KEY_COLUMN_USAGE/REFERENTIAL_CONSTRAINTS to conditionally run
the ALTER TABLE ... ADD CONSTRAINT only when that constraint is absent; keep the
same ON DELETE/ON UPDATE CASCADE behavior and preserve the FOREIGN_KEY_CHECKS
wrapper. Ensure you reference the exact constraint names above when implementing
the checks/drops.

In `@migrations/007_remove_posts_image_id.sql`:
- Around line 11-45: The migration currently checks `@constraint_exists` and
attempts to drop a hard-coded FK `posts_image_id_media_id_fk`, which fails if
the FK has a different name; update the logic around `@constraint_exists` and
`@drop_fk_sql` to discover the actual constraint name for table `posts` and column
`image_id` (e.g., query INFORMATION_SCHEMA.KEY_COLUMN_USAGE where
TABLE_NAME='posts' AND COLUMN_NAME='image_id' AND REFERENCED_TABLE_NAME IS NOT
NULL), store that constraint name into a variable (instead of assuming
`posts_image_id_media_id_fk`), and build the ALTER TABLE `posts` DROP FOREIGN
KEY <discovered_name> statement for PREPARE/EXECUTE only when a name is found so
the subsequent DROP COLUMN `image_id` will not fail due to an existing FK.

In `@migrations/README.md`:
- Around line 31-34: Update the migrations README so it accurately describes the
actual migration files in this PR: replace the incorrect description of
"006_update_groups_users_notification_type.sql" with a summary of
006_add_user_deletion_cascades.sql (explain it adds CASCADE foreign keys for
user deletion), and add an entry for 007_remove_posts_image_id.sql describing
that it removes the image_id column and any related constraints; ensure the
filenames 006_add_user_deletion_cascades.sql and 007_remove_posts_image_id.sql
are referenced exactly and remove mention of the nonexistent enum change.
- Around line 43-44: The README example uses the wrong migration filename;
update the example command that references
006_update_groups_users_notification_type.sql to instead reference the actual
migration file 006_add_user_deletion_cascades.sql so the two example mysql
import commands match existing migration filenames (leave the other command
using 005_add_performance_indexes.sql unchanged).

In `@models/groups.js`:
- Line 6: The model change set the Groups model's description attribute
(models/groups.js -> description) to allowNull: false but there is no DB
migration; create a Sequelize migration that (1) backfills any NULL descriptions
in the groups table with a sensible default or computed value (e.g., empty
string or "No description"), (2) alters the description column to set NOT NULL
at the database level, and (3) include a down migration to revert the NOT NULL
constraint and restore previous values if needed; run the backfill before
applying the NOT NULL change and ensure the migration is referenced/deployed
alongside the models/groups.js change.

In `@models/index.js`:
- Around line 91-103: The hook using UPLOADS_DIR in
db.Media.addHook('beforeDestroy') lacks validation and can produce unexpected
paths; add a guard that verifies UPLOADS_DIR is a non-empty string before
calling path.resolve and attempting fsUnlink (or provide a clear, explicit
fallback directory), and if invalid log a distinct error via logger.error
(including UPLOADS_DIR value, media.id, media.user_id) and skip the file-delete
logic so DB deletion still proceeds; ensure the validation is performed either
at module initialization or at the start of the hook where path.resolve(...) and
fsUnlink(...) are invoked.

In `@routes/graphql/mutations/posts.js`:
- Around line 45-47: The slug is being generated from the raw args.title instead
of the sanitized title stored on the post; update the conditional that sets
post.slug to call generateSlug(Post, post.title) rather than generateSlug(Post,
args.title) so it uses the cleaned value produced by cleanContent(), referencing
post.slug, generateSlug, Post, args.title and post.title to locate the change.

In `@routes/graphql/typeDefs/types.graphql`:
- Around line 64-68: GraphQL type Post defines slug as nullable (slug: String)
but the Sequelize Post model enforces allowNull: false, causing a contract
mismatch; fix by either (A) making GraphQL non-nullable: change the Post.slug
field in types.graphql to slug: String! to match the model, or (B) allowing NULL
at the DB level: update the Sequelize Post model (the Post model definition that
sets allowNull for slug) to allowNull: true, create a migration that alters the
posts.slug column to NULLABLE, run the migration, and ensure any
resolver/validation logic that assumes non-null slug is adjusted accordingly.

In `@routes/graphql/types.js`:
- Around line 57-64: The content resolver currently masks valid text when
comment.user_id is null by returning "[deleted]"; change the content resolver
(the function assigned to content) to always return comment.content so stored
comment text is preserved, and leave the author resolver behavior as-is (author
returns DELETED_USER when comment.user_id is falsy and otherwise calls
User.findByPk(comment.user_id)); update only the content resolver logic and do
not alter DELETED_USER or User.findByPk usage.

In `@SCHEMA_UPDATE_SUMMARY.md`:
- Around line 9-10: Replace machine-specific absolute paths (e.g., the string
"/Users/spathon/Sites/cham/API/migrations/") with repo-relative or placeholder
paths throughout the file SCHEMA_UPDATE_SUMMARY.md; search for that exact path
and the other occurrences noted (lines referenced around 91-92, 132-133,
176-187) and update them to a consistent repo-relative form such as
"./migrations" or a placeholder like "${REPO_ROOT}/API/migrations" so the
document is portable for other developers.
- Around line 11-18: Update SCHEMA_UPDATE_SUMMARY.md to list seven migration
scripts (change "Six" to "Seven") and replace the current list so it includes
the actual filenames in order: 001_add_rate_limiting_fields_to_users.sql,
002_update_comments_foreign_key.sql, 003_add_post_id_to_media.sql,
004_add_posts_image_id_foreign_key.sql, 005_add_performance_indexes.sql,
006_add_user_deletion_cascades.sql, and 007_remove_posts_image_id.sql (remove or
reconcile the incorrect 006_update_groups_users_notification_type.sql entry);
also update Step 2 execution commands and any other references (lines ~89-103)
to reference the correct filenames and count so operators run migrations 001–007
in the proper order.

In `@scripts/cleanup-orphaned-files.js`:
- Around line 261-263: The summary log uses result.totalSize which can
overreport when some deletions fail; modify the deletion loop in
scripts/cleanup-orphaned-files.js to accumulate the size of only successfully
deleted files (e.g., maintain a deletedSize counter updated when a file deletion
returns success) and then, when DELETE_MODE is true, log deletedCount and
formatBytes(deletedSize) instead of result.totalSize; keep references to
DELETE_MODE, result.deletedCount and replace result.totalSize with the new
deletedSize that you compute from successful delete operations.
- Around line 153-160: The orphan-detection is using filenames only (creating
validFiles = new Set(mediaRecords.map(m => m.filename))) which causes false
negatives when different users share the same filename; change the logic to
build the Set of valid keys using the composite "{user_id}/{filename}" from
mediaRecords (e.g. map each m => `${m.user_id}/${m.filename}`), and then compare
against the same composite key produced by scanDirectory()/allFiles entries when
computing orphanedFiles; update any references to validFiles and the
orphanedFiles filter to use this composite key so files in user-specific
subdirectories are matched correctly.
🧹 Nitpick comments (3)
routes/graphql/queries.js (1)

204-217: Parameterize the raw SQL instead of string interpolation.

WHERE comments.user_id = ${me.id} should use replacements/bind parameters to avoid injection risk and improve query plan caching.

🔧 Parameterized query
-    const commentPosts = await sequelize.query(`
+    const commentPosts = await sequelize.query(`
       SELECT
         posts.id, posts.user_id, posts.slug, posts.group_id, posts.comments_count, posts.title,
         posts.created_at AS createdAt,
         EXISTS(SELECT id FROM media WHERE media.post_id = posts.id) AS hasMedia,
         COUNT(posts.id) AS commentsMade
       FROM posts
       JOIN comments ON comments.post_id = posts.id
-      WHERE comments.user_id = ${me.id}
+      WHERE comments.user_id = :userId
       AND posts.status = 'published'
       GROUP BY posts.id
       ORDER BY posts.id DESC
       LIMIT 200
-    `, { type: sequelize.QueryTypes.SELECT })
+    `, { replacements: { userId: me.id }, type: sequelize.QueryTypes.SELECT })
models/index.js (1)

97-97: Use logger.info instead of console.log for consistent structured logging.

Errors are logged via logger.error, but success messages use console.log. This creates inconsistency in log aggregation and observability.

♻️ Suggested change
-    console.log(`Deleted file: ${filePath}`)
+    logger.info('FILE_DELETED', { filePath, fileId: media.id, userId: media.user_id })

Apply the same pattern at line 139:

-        console.log(`Deleted thumbnail: ${fullPath}`)
+        logger.info('THUMBNAIL_DELETED', { path: fullPath })
scripts/cleanup-orphaned-files.js (1)

275-280: Database connection not closed on error before exit.

When an error occurs, process.exit(1) is called without closing the database connection. While the process is terminating anyway, explicitly closing the connection ensures clean shutdown and prevents potential connection leaks in pooled environments.

♻️ Proposed fix
   } catch (err) {
     log(`Fatal error: ${err.message}`, 'ERROR')
     log(err.stack, 'ERROR')
     await writeLogFile()
+    await sequelize.close().catch(() => {}) // Best effort cleanup
     process.exit(1)
   }

@spathon spathon requested a review from Synchro February 5, 2026 17:21
spathon and others added 3 commits February 6, 2026 09:41
…ion, and docs

- Use sanitized title for slug generation in createPost and editPost
- Parameterize raw SQL in postsCommented query to prevent SQL injection
- Guard UPLOADS_DIR in Media beforeDestroy hook, replace console.log with logger
- Fix orphan cleanup script to use user_id/filename composite key and track deletedSize
- Add media.post_id backfill from media_relations in combined.sql
- Update docs to reference combined.sql, remove absolute paths and stale references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
routes/graphql/mutations/posts.js (1)

36-39: ⚠️ Potential issue | 🟡 Minor

Null-check order: post could be null before accessing post.user_id.

Post.findByPk(args.id) returns null when the record doesn't exist, causing a crash on Line 39 (post.user_id). While this is pre-existing, the function is being modified in this PR — consider adding a guard.

Proposed fix
   async editPost(_, args, { me }) {
-    const post = await Post.findByPk(args.id)
     if (!me) throw new GraphQLError('You must be logged in.')
+    const post = await Post.findByPk(args.id)
+    if (!post) throw new GraphQLError('Post not found.')
     if (post.user_id !== me.id) throw new GraphQLError('You can\'t edit some one else post.')
🤖 Fix all issues with AI agents
In `@IMPLEMENTATION_SUMMARY.md`:
- Around line 69-87: Update the stale hook snippet to match the real
implementation in models/index.js: replace the console.log in the
db.Media.addHook('beforeDestroy', ...) example with logger.error usage for
failures, add the guard that checks UPLOADS_DIR exists before attempting
fsUnlink(…) and ensure the thumbnail cleanup logic using THUMBNAIL_DIR and
deleteThumbnailsRecursive(userThumbDir, media.filename) is present;
alternatively, remove the embedded snippet and add a clear reference/link to the
actual implementation in models/index.js so contributors are not misled by
outdated code.
- Around line 319-336: Update the IMPLEMENTATION_SUMMARY.md header counts and
lists so they match the actual PR contents: change "Code Changes (7 files)" to
"Code Changes (10 files)" and ensure the ten files listed include
routes/graphql/types.js, routes/graphql/queries.js, models/index.js,
models/posts.js, models/media.js, models/messages.js,
models/messagesSubscribers.js, models/groupsUsers.js, models/blog.js,
models/activation.js; change "New Files (3 files)" to "New Files (6 files)" and
list all new files including migrations/006_add_user_deletion_cascades.sql,
migrations/007_remove_posts_image_id.sql, scripts/cleanup-orphaned-files.js,
plus IMPLEMENTATION_SUMMARY.md, SCHEMA_UPDATE_SUMMARY.md, and
migrations/README.md so the counts and items are consistent.
- Around line 46-53: The doc example currently shows raw template interpolation
using ${me.id}; update it to show a parameterized SQL placeholder (e.g. a named
parameter like :meId) and explicitly state that the real implementation uses
Sequelize parameter binding/replacements, and describe passing the value (me.id)
via the replacements/bind options so readers do not mistake template-literal
interpolation for safe query construction.

In `@models/index.js`:
- Around line 90-107: The beforeDestroy hook on db.Media currently builds
filePath using media.filename allowing path traversal; update the hook around
db.Media.addHook('beforeDestroy') to validate/sanitize media.filename before
unlinking by either using a safe basename/sanitization and rejecting filenames
containing path separators or by resolving the candidate path and verifying it
is within the UPLOADS_DIR root (e.g., ensure path.resolve(UPLOADS_DIR,
String(media.user_id), media.filename).startsWith(path.resolve(UPLOADS_DIR) +
path.sep)); if validation fails, log a warning including fileId and userId and
skip fsUnlink to avoid deleting files outside the uploads dir.

In `@SCHEMA_UPDATE_SUMMARY.md`:
- Around line 195-199: The date under the "## Migration Completed" section is
inconsistent: change the line showing "2026-01-03" to the correct
implementation/migration date that matches the PR and combined.sql header
(February 2026) so the SCHEMA_UPDATE_SUMMARY.md is aligned; edit the date string
on the "Date:" line near the "## Migration Completed" heading accordingly.

In `@scripts/cleanup-orphaned-files.js`:
- Around line 160-163: The filter that builds the lookup key (in orphanedFiles)
assumes files live under a user subdirectory; add a short comment above the
block noting that files directly under UPLOADS_DIR (no user subdirectory) will
produce a key like `filename/filename` and thus be treated as orphaned and
removed, and if you want to preserve root files adjust the key generation: in
the orphanedFiles filter use the existing symbols (file.relativePath, parts,
key, validFiles) but derive key as either `${parts[0]}/${file.filename}` when
parts.length>1 or as `file.filename` when parts.length===1 so lookups match the
intended validFiles format—otherwise keep the comment documenting the
assumption.
🧹 Nitpick comments (6)
scripts/cleanup-orphaned-files.js (2)

251-287: Database connection not closed on fatal error path.

If the try block throws after sequelize.authenticate() succeeds, the catch block logs and exits without calling sequelize.close(). While process.exit(1) will terminate the process, this can leave dangling connections in connection-pooled environments.

Proposed fix — use finally
+  } finally {
+    try { await sequelize.close() } catch (_) { /* ignore */ }
+  }
-    await writeLogFile()
-    process.exit(1)
-  }
+  // Move process.exit into catch:

Or more practically, wrap the cleanup:

   } catch (err) {
     log(`Fatal error: ${err.message}`, 'ERROR')
     log(err.stack, 'ERROR')
     await writeLogFile()
+    try { await sequelize.close() } catch (_) { /* ignore close errors */ }
     process.exit(1)
   }

38-43: formatBytes returns Infinity or NaN for negative input.

Edge case: if bytes is negative (e.g., from an unexpected stats.size), Math.log(negative) yields NaN. This won't happen in practice but a guard would be defensive.

migrations/combined.sql (2)

92-93: MODIFY COLUMN runs unconditionally on every re-run.

Unlike the other steps that use IF/prepared-statement guards, Line 93 always executes the ALTER TABLE even when user_id is already nullable. MySQL handles this gracefully (it's a no-op in effect), but on large tables the ALTER may still acquire a metadata lock and rebuild the table depending on the MySQL version/engine.

For consistency with the rest of the script, consider wrapping this in a conditional check against IS_NULLABLE in INFORMATION_SCHEMA.COLUMNS.


334-370: Blog FK replacement will drop-and-recreate on every re-run.

The script finds the current FK by name (including the one it may have created on a previous run), drops it, then adds a new one. On repeated runs this performs unnecessary DDL. Not harmful, but a tighter guard (e.g., checking the current DELETE_RULE in INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS) would make it truly no-op on re-run.

This is a minor observation — the current approach is functionally correct.

migrations/README.md (1)

7-7: Nit: "rate limiting" → "rate-limiting" (compound adjective).

Per static analysis hint. As per coding guidelines, "rate-limiting" should be hyphenated when used as a compound modifier.

Fix
-1. Adds rate limiting fields to users table
+1. Adds rate-limiting fields to users table
SCHEMA_UPDATE_SUMMARY.md (1)

114-125: Use a generic placeholder path consistently.

Line 116 uses cd /path/to/API which is fine as a placeholder, but consider also noting that environment variables (UPLOADS_DIR, etc.) need to be set for the test scenarios described on Lines 122-124 (e.g., testing user deletion with file cleanup).

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
routes/auth.js (2)

114-116: ⚠️ Potential issue | 🟠 Major

Bug: && should be || — validation only triggers when all three params are non-numeric.

The current condition throws only if userId, w, and h are all non-numeric simultaneously. If only one is non-numeric (e.g., a malicious userId), execution continues unchecked. This weakens the downstream path construction on Line 128 (absThumbPath), which does not use safePath.

🐛 Proposed fix
   if (
-    !isNumber.test(userId) && !isNumber.test(w) && !isNumber.test(h)
+    !isNumber.test(userId) || !isNumber.test(w) || !isNumber.test(h)
   ) throw new Error('Has to be numeric')

127-128: ⚠️ Potential issue | 🟠 Major

absThumbPath is constructed with path.join — not guarded by safePath.

While UPLOADS_DIR is now protected via safePath on Line 124, the THUMBNAIL_DIR path on Line 128 uses raw path.join. Combined with the &&/|| bug on Line 115, a non-numeric userId could traverse outside THUMBNAIL_DIR. Consider applying safePath here as well for consistency.

🛡️ Proposed fix
   const relThumbPath = path.join(userId, h, w)
-  const absThumbPath = path.join(process.env.THUMBNAIL_DIR, relThumbPath)
+  const absThumbPath = safePath(process.env.THUMBNAIL_DIR, userId, h, w)
routes/graphql/mutations/media.js (1)

69-77: ⚠️ Potential issue | 🟡 Minor

Double file deletion: explicit fsUnlink on Line 71, then beforeDestroy hook fires again on Line 77.

media.destroy({ force: true }) triggers the beforeDestroy hook in models/index.js, which also attempts to delete the same file. The second attempt will fail with ENOENT and log a FILE_CLEANUP_ERROR on every successful delete — producing noisy false-alarm logs.

Options:

  1. Remove the explicit fsUnlink here and let the hook handle cleanup.
  2. Pass an option to skip hook cleanup (e.g., { hooks: false }), keeping the explicit delete.
  3. Use { individualHooks: false } or a custom flag to signal the hook.
♻️ Option 1: Let the hook handle it
   async deleteFile(_, { id }, { me }) {
     const media = await Media.findByPk(id)
     if (!media) throw new GraphQLError('No file found')
     if (media.user_id !== me.id) throw new GraphQLError('No, just no!')
 
-    const filePath = safePath(UPLOADS_DIR, String(media.user_id), media.filename)
-    try {
-      await fsUnlink(filePath)
-    } catch (err) {
-      logger.error('DELETE_FILE_ERROR', {
-        error: err, filePath, fileId: media.id, userId: me.id,
-      })
-    }
     await media.destroy({ force: true })
 
     return id
   },
🧹 Nitpick comments (3)
models/index.js (2)

3-6: Minor: mixed promisify(fs.unlink) and fs.promises.* APIs.

Lines 114 and 155 use fsUnlink (promisified), while lines 127 and 144 use fs.promises.stat / fs.promises.readdir. Consider using fs.promises.unlink throughout for consistency.

♻️ Proposed fix
-const { promisify } = require('util')
 const Sequelize = require('sequelize')
 
-const fsUnlink = promisify(fs.unlink)

Then replace fsUnlink(...) calls with fs.promises.unlink(...).

Also applies to: 114-114, 127-127, 144-144


141-165: Recursive thumbnail deletion could follow symlinks into unintended directories.

Dirent.isDirectory() in Node.js returns false for symlinks, so the current code won't follow symlinks by default — which is the safe behavior. However, if followSymlinks behavior ever changes or readdir options change, this could become a risk. A brief comment noting the symlink assumption would aid future maintainers.

routes/graphql/mutations/media.js (1)

34-35: uploadFile still uses string concatenation for paths instead of safePath.

While me.id is an authenticated integer and newFilename is UUID-generated (low risk), using safePath here would be consistent with the safety pattern established across the rest of this PR.

♻️ Proposed fix
-    const absPath = `${UPLOADS_DIR}${me.id}/`
-    const newFilePath = `${absPath}${newFilename}`
+    const absPath = safePath(UPLOADS_DIR, String(me.id))
+    const newFilePath = safePath(UPLOADS_DIR, String(me.id), newFilename)

Note: The fsMkdir call on Line 47 would need absPath without the trailing separator, which safePath already provides.

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.

2 participants