Skip to content

Remove User and UserId - #335

Merged
FrenchGithubUser merged 11 commits into
mainfrom
refactor/unwraps
Aug 31, 2025
Merged

FrenchGithubUser merged 11 commits into
mainfrom
refactor/unwraps

Conversation

@ppoliani

@ppoliani ppoliani commented Aug 28, 2025 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Tokens now include a user "class" claim.
    • “My profile” now returns unread notifications and recent uploaded/snatched items.
  • Bug Fixes

    • Improved handling and messaging for unauthorized or banned accounts.
  • Refactor

    • Authentication now consistently derives identity from JWTs across many endpoints.
    • Several user-facing flows now fetch full user details from storage before operations.
  • Chores

    • Added a runtime dependency to support async utilities.

@coderabbitai

coderabbitai Bot commented Aug 28, 2025 •

Copy link
Copy Markdown

Walkthrough

Replaces handler-level User/UserId extractors with middleware-inserted JWT Authdata { sub: i64, class: String }; handlers now accept Authdata and pass numeric user_id to storage. Storage APIs updated to accept user_id instead of User. Claims gained class; SQLx descriptor adjusted; futures-util added.

Changes

Cohort / File(s) Summary
Middleware: new Authdata
backend/api/src/middlewares/jwt_middleware.rs
Adds public Authdata { sub: i64, class: String }, FromRequest impl; inserts Authdata into request extensions for bearer and API-key flows; adjusts ban/error handling.
Handlers: switch to Authdata
backend/api/src/handlers/*/*.rs (many files)
Replaces User/UserId parameters with Authdata across many handlers (artists, conversations, forum, gifts, invitations, master_groups, search, series, subscriptions, title_groups, torrent_requests, torrents, users, wiki, etc.); updates imports, signatures, and uses user.sub / user.class; some handlers perform DB lookup using user.sub.
Removed handler extractors
backend/api/src/handlers/mod.rs
Removes public UserId and User types and their FromRequest/Deref implementations.
Auth tokens / Claims
backend/api/src/handlers/auth/*.rs, backend/storage/src/models/user.rs
Adds class: String to Claims (now Clone); login and refresh include class in token claims.
Storage: auth lookup / SQLx descriptors
backend/storage/src/repositories/auth_repository.rs, backend/storage/.sqlx/*
API-key lookup now returns full User (SELECT u.*); new SQLx descriptor added, old id-only descriptor removed.
Storage: accept user_id (repo API changes)
backend/storage/src/repositories/*_repository.rs (series, title_group*, torrent*, torrent_request*, torrent_request_vote*, title_group_comment, torrent_report, etc.)
Public repository functions changed from accepting &User/User to user_id: i64; SQL bindings updated; some repos do internal user lookups as needed.
User repo: ban check
backend/storage/src/repositories/user_repository.rs
is_user_banned signature changed from bool → Result<bool> and returns Ok(true) when user row missing.
Handlers with extra DB lookups / param changes
backend/api/src/handlers/users/get_me.rs, .../torrents/get_upload_information.rs, .../torrents/download_dottorrent_file.rs, .../gifts/create_gift.rs, .../users/get_user.rs
Some handlers now fetch full User from DB via user.sub; get_me response expanded with unread counts and recent torrents; some repo calls gained extra params (e.g., tracker URL).
Dependency
backend/api/Cargo.toml
Adds futures-util = "0.3.31".
Env examples
backend/api/.env.example, backend/storage/.env.example, backend/cron-jobs/.env.example
Removed "DO NOT EDIT" directive from DATABASE_URL comment lines.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant S as API Server
  participant MW as JWT/API-Key Middleware
  participant H as Handler
  participant DB as Storage

  C->>S: HTTP request (Authorization / API-Key)
  S->>MW: validate token / key
  MW->>DB: (optional) api_key lookup / ban check
  DB-->>MW: user (id, class)
  MW->>S: insert Authdata{sub, class} into request.extensions
  S->>H: handler extracts Authdata (FromRequest)
  H->>DB: call repo with user_id (Authdata.sub)
  DB-->>H: data/result
  H-->>C: HTTP response
Loading
sequenceDiagram
  autonumber
  participant Client
  participant AuthHandler
  participant JWTSvc

  Client->>AuthHandler: login / refresh request
  AuthHandler->>J WTSvc: build Claims { sub, class, exp }
  JWTSvc-->>AuthHandler: signed access + refresh tokens
  AuthHandler-->>Client: tokens (class embedded)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Replace User/UserId extraction with JwtAuthData to avoid DB reads where only user_id is needed (#328) ✅

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Expand get_me response with unread_notifications_amount and last_five_uploaded/snatched_torrents (backend/api/src/handlers/users/get_me.rs) Adds new DB searches and response fields beyond the objective of avoiding per-request full-user DB reads.
Add tracker URL parameter to torrent retrievals / announce URL signature changes (backend/api/src/handlers/torrents/download_dottorrent_file.rs, backend/api/src/handlers/torrents/get_upload_information.rs) Alters repository/service call signatures and parameters unrelated to switching auth extraction to JWT claims.
Add dependency futures-util = "0.3.31" (backend/api/Cargo.toml) New crate addition is orthogonal to the auth-extraction objective and not strictly required by it.

Possibly related PRs

  • Refactor/split handlers #325 — Large auth/handler refactor touching middleware and handler signatures; strong overlap with this PR's Authdata and handler changes.

Poem

I hop through claims with nimble grace,
A tiny Authdata in my embrace.
No heavy DB burrow to slow the run,
Just sub and class — the work is done.
I thump, I cheer, the handlers hum. 🐇🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecf8e18 and c113a0a.

📒 Files selected for processing (1)
  • backend/cron-jobs/.env.example (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/cron-jobs/.env.example
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: schema-check
  • GitHub Check: build
  • GitHub Check: schema-check
  • GitHub Check: build
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/unwraps

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (26)
backend/api/src/handlers/auth/refresh_token.rs (1)

32-37: Avoid unwrap on JWT encode; unify error handling and compute now once.

Unwrap can panic at runtime; prefer consistent error mapping and a single timestamp to avoid drift.

Apply:

-    let token = encode(
-        &Header::default(),
-        &token_claims,
-        &EncodingKey::from_secret(arc.jwt_secret.as_bytes()),
-    )
-    .unwrap();
+    let token = encode(
+        &Header::default(),
+        &token_claims,
+        &EncodingKey::from_secret(arc.jwt_secret.as_bytes()),
+    )
+    .map_err(Error::JwtError)?;

Optionally also normalize time base:

-    let token_claims = Claims {
+    let now = Utc::now();
+    let token_claims = Claims {
         sub: old_refresh_token.claims.sub,
-        exp: (Utc::now() + Duration::days(1)).timestamp() as usize,
+        exp: (now + Duration::days(1)).timestamp() as usize,
         class: old_refresh_token.claims.class.clone(),
     };
@@
-    let refresh_token_claims = Claims {
+    let refresh_token_claims = Claims {
         sub: old_refresh_token.claims.sub,
-        exp: (Utc::now() + Duration::days(90)).timestamp() as usize,
+        exp: (now + Duration::days(90)).timestamp() as usize,
         class: old_refresh_token.claims.class.clone(),
     };

Also applies to: 45-51

backend/api/src/handlers/conversations/create_conversation.rs (1)

6-14: Fix OpenAPI response code and declare security.

Handler returns 201 Created but docs say 200; also missing bearer security.

 #[utoipa::path(
     post,
     operation_id = "Create conversation",
     tag = "Conversation",
     path = "/api/conversations",
+    security(("bearerAuth" = [])),
     responses(
-        (status = 200, description = "Successfully created the conversation and first message", body=Conversation),
+        (status = 201, description = "Successfully created the conversation and first message", body=Conversation),
     )
 )]
backend/api/src/handlers/conversations/create_conversation_message.rs (1)

6-14: Align OpenAPI with behavior and declare security.

Docs say 200 but handler returns 201; add bearer security.

 #[utoipa::path(
     post,
     operation_id = "Create conversation message",
     tag = "Conversation",
     path = "/api/conversations/messages",
+    security(("bearerAuth" = [])),
     responses(
-        (status = 200, description = "Successfully created the conversation's message", body=ConversationMessage),
+        (status = 201, description = "Successfully created the conversation's message", body=ConversationMessage),
     )
 )]
backend/api/src/handlers/auth/login.rs (1)

38-44: Don't unwrap JWT encode; propagate errors.
encode(...).unwrap() can panic. Use the same error mapping as the access token.

-        refresh_token = encode(
+        refresh_token = encode(
             &Header::default(),
             &refresh_token_claims,
             &EncodingKey::from_secret(arc.jwt_secret.as_bytes()),
-        )
-        .unwrap();
+        )
+        .map_err(Error::JwtError)?;
backend/api/src/handlers/series/create_series.rs (1)

12-12: OpenAPI doc/handler mismatch: handler returns 201 but docs say 200.

Fix the doc to 201.

-        (status = 200, description = "Successfully created the series", body=Series),
+        (status = 201, description = "Successfully created the series", body=Series),

Also applies to: 22-22

backend/api/src/handlers/invitations/create_invitation.rs (2)

14-14: OpenAPI doc/handler mismatch: returning 201 but docs say 200.

Align docs to 201.

-        (status = 200, description = "Successfully sent the invitation", body=Invitation),
+        (status = 201, description = "Successfully sent the invitation", body=Invitation),

Also applies to: 54-54


22-30: Enforce atomic check-and-decrement for invitations

In ConnectionPool::create_invitation (backend/storage/src/repositories/invitation_repository.rs), replace the unconditional decrement call (decrement_invitations_available) with a single SQL statement that both checks invitations > 0 and decrements (e.g. UPDATE users SET invitations = invitations - 1 WHERE id = $1 AND invitations > 0 RETURNING invitations), propagate an error if no row is returned (using ? rather than let _ =), and remove or revisit the pre-check in the handler to avoid TOCTOU races.

backend/storage/src/repositories/torrent_request_repository.rs (3)

11-21: API surface simplified to user_id: LGTM; make creation + initial vote transactional

The signature and bindings look good. However, the create + initial vote path is non-transactional and risks partial writes.

-    pub async fn create_torrent_request(
+    pub async fn create_torrent_request(
         &self,
         torrent_request: &mut UserCreatedTorrentRequest,
-        user_id: i64,
+        user_id: i64,
     ) -> Result<TorrentRequest> {
-        //TODO: make those requests transactional
+        // Make the creation + initial vote atomic
         let create_torrent_request_query = r#"
             INSERT INTO torrent_requests
@@
-        let created_torrent_request =
-            sqlx::query_as::<_, TorrentRequest>(create_torrent_request_query)
+        let mut tx = <ConnectionPool as Borrow<PgPool>>::borrow(self).begin().await?;
+        let created_torrent_request =
+            sqlx::query_as::<_, TorrentRequest>(create_torrent_request_query)
                 .bind(torrent_request.title_group_id)
                 .bind(user_id)
@@
-                .fetch_one(self.borrow())
+                .fetch_one(&mut *tx)
                 .await
                 .map_err(Error::CouldNotCreateTorrentRequest)?;
@@
-        let _ = self
-            .create_torrent_request_vote(&torrent_request.initial_vote, user_id)
-            .await?;
+        let _ = self
+            .create_torrent_request_vote(&torrent_request.initial_vote, user_id)
+            .await?;
+
+        tx.commit().await.map_err(Error::CouldNotCreateTorrentRequest)?;

If create_torrent_request_vote requires a transaction handle, add an overload to accept &mut Transaction<'_, Postgres> and use it here.

Also applies to: 29-36, 50-55


123-164: Critical: bounty split over-credits on odd totals and risks overflow due to i32 cast

Current logic rounds half and applies the same share to both users, e.g., 3 → round(1.5)=2; both get 2 → 4 total credited > 3. Also casts to i32, risking truncation.

Apply deterministic integer split with remainder assigned to one party, and use i64 throughout:

-        // Calculate the share for each user (50% each).
-        // Ensure floating-point division for accurate half-shares, then cast back to i64 for database.
-        let upload_share = (bounty_summary.total_upload as f32 / 2.0).round() as i32;
-        let bonus_share = (bounty_summary.total_bonus as f32 / 2.0).round() as i32;
+        // Split pot deterministically without over-crediting.
+        // Remainder goes to the filler to incentivize fulfillment.
+        let upload_half = bounty_summary.total_upload / 2;
+        let bonus_half = bounty_summary.total_bonus / 2;
+        let upload_rem = bounty_summary.total_upload % 2;
+        let bonus_rem = bounty_summary.total_bonus % 2;
+        let uploader_upload_share: i64 = upload_half;
+        let filler_upload_share: i64 = upload_half + upload_rem;
+        let uploader_bonus_share: i64 = bonus_half;
+        let filler_bonus_share: i64 = bonus_half + bonus_rem;

And in the UPDATE use distinct shares per user (see next comment).


141-164: Use distinct shares per user and guard against races with conditional UPDATE

Use separate shares (no double-credit), and ensure the request isn’t filled concurrently.

-        sqlx::query!(
+        sqlx::query!(
             r#"
             UPDATE users
             SET
-                uploaded = users.uploaded +
-                    CASE
-                        WHEN users.id = $1 THEN $3
-                        WHEN users.id = $2 THEN $3
-                        ELSE 0
-                    END,
-                bonus_points = users.bonus_points +
-                    CASE
-                        WHEN users.id = $1 THEN $4
-                        WHEN users.id = $2 THEN $4
-                        ELSE 0
-                    END
+                uploaded = users.uploaded +
+                    CASE
+                        WHEN users.id = $1 THEN $3  -- uploader_upload_share
+                        WHEN users.id = $2 THEN $4  -- filler_upload_share
+                        ELSE 0
+                    END,
+                bonus_points = users.bonus_points +
+                    CASE
+                        WHEN users.id = $1 THEN $5  -- uploader_bonus_share
+                        WHEN users.id = $2 THEN $6  -- filler_bonus_share
+                        ELSE 0
+                    END
             WHERE
                 users.id IN ($1, $2)
                 "#,
             torrent_uploader_id,
             current_user_id,
-            upload_share,
-            bonus_share
+            uploader_upload_share,
+            filler_upload_share,
+            uploader_bonus_share,
+            filler_bonus_share
         )
         .execute(&mut *tx)
         .await?;
 
-        sqlx::query!(
+        let updated = sqlx::query!(
             r#"
             UPDATE torrent_requests tr
             SET
                 filled_by_torrent_id = $1,
                 filled_by_user_id = $2,
                 filled_at = NOW()
             WHERE
-                tr.id = $3
+                tr.id = $3 AND tr.filled_at IS NULL
                 "#,
             torrent_id,
             current_user_id,
             torrent_request_id
         )
-        .execute(&mut *tx)
-        .await?;
+        .execute(&mut *tx)
+        .await?;
+        if updated.rows_affected() == 0 {
+            // Another filler won the race; rollback crediting to be safe.
+            return Err(Error::TorrentRequestAlreadyFilled);
+        }

Also applies to: 168-181

backend/api/src/handlers/wiki/create_wiki_article.rs (1)

6-14: OpenAPI/status mismatch: return is 201 Created but docs declare 200 OK.
Align the spec with the actual response.

Apply:

-        (status = 200, description = "Successfully created the wiki article", body=WikiArticle),
+        (status = 201, description = "Successfully created the wiki article", body=WikiArticle),

Also applies to: 26-26

backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)

6-14: OpenAPI/status mismatch: handler returns 201 but docs say 200.
Update the spec to 201 to match the Created response.

-        (status = 200, description = "Successfully created the artist affiliations", body=Vec<AffiliatedArtistHierarchy>),
+        (status = 201, description = "Successfully created the artist affiliations", body=Vec<AffiliatedArtistHierarchy>),

Also applies to: 25-25

backend/api/src/handlers/edition_groups/create_edition_group.rs (1)

6-14: OpenAPI/status mismatch: returning 201 but docs state 200.
Update the spec to reflect Created.

-        (status = 200, description = "Successfully created the edition_group", body=EditionGroup),
+        (status = 201, description = "Successfully created the edition_group", body=EditionGroup),

Also applies to: 22-22

backend/storage/src/repositories/auth_repository.rs (1)

112-128: Rename method and update its usage
Update the signature in auth_repository.rs and the call site in jwt_middleware.rs to match the new name:

--- backend/storage/src/repositories/auth_repository.rs
@@ -112,7 +112,7 @@
-    pub async fn find_user_id_with_api_key(&self, api_key: &str) -> Result<User> {
+    pub async fn find_user_with_api_key(&self, api_key: &str) -> Result<User> {
--- backend/api/src/middlewares/jwt_middleware.rs
@@ -123,7 +123,7 @@
-    let user = match arc.pool.find_user_id_with_api_key(api_key).await {
+    let user = match arc.pool.find_user_with_api_key(api_key).await {
backend/api/src/handlers/title_groups/create_title_group.rs (2)

25-35: Avoid tmdb_api_key unwrap; prevent panic when not configured.

Unwrapping arc.tmdb_api_key will crash the request path if the key is missing. Either skip ratings gracefully or return a controlled error.

Apply:

-    let rating_futures: Vec<_> = form
-        .external_links
-        .iter()
-        .filter(|link| link.contains("https://www.themoviedb.org/"))
-        .map(|link| get_tmdb_rating(link, arc.tmdb_api_key.clone().unwrap()))
-        .collect();
-    let ratings: Vec<PublicRating> = join_all(rating_futures)
-        .await
-        .into_iter()
-        .filter_map(Result::ok)
-        .collect();
+    let ratings: Vec<PublicRating> = if let Some(tmdb_key) = arc.tmdb_api_key.clone() {
+        let futures = form
+            .external_links
+            .iter()
+            .filter(|link| link.contains("https://www.themoviedb.org/"))
+            .map(|link| get_tmdb_rating(link, tmdb_key.clone()))
+            .collect::<Vec<_>>();
+        join_all(futures)
+            .await
+            .into_iter()
+            .filter_map(Result::ok)
+            .collect()
+    } else {
+        Vec::new()
+    };

25-35: Panic risk inside get_tmdb_rating due to unwraps.

get_tmdb_rating currently unwraps both URL extraction and TMDB client calls (see backend/api/src/handlers/external_db/get_tmdb_data.rs lines 1-24 in the provided snippet), which can bring down this handler despite filter_map(Result::ok). Replace unwraps with Result propagation.

Outside this file, change:

let (media_type, id) = extract_media_type_and_id(tmdb_url).unwrap();

to:

let (media_type, id) = extract_media_type_and_id(tmdb_url)
    .map_err(|_| Error::InvalidTMDBUrl)?;

Similarly, swap remaining .await.unwrap() calls on TMDB client for proper error mapping or ?.

backend/api/src/handlers/user_applications/update_user_application_status.rs (1)

25-33: Don’t hardcode role as a raw string.

user.class == "staff" is fragile. Centralize an is_staff() helper or use a typed enum/const to avoid typos and future drift.

Example:

-    if user.class != "staff" {
+    const STAFF: &str = "staff";
+    if user.class.as_str() != STAFF {
         return Err(Error::InsufficientPrivileges);
     }
backend/api/src/handlers/user_applications/get_user_applications.rs (1)

14-30: OpenAPI params don’t match the handler: drop checked, type status as enum.
Doc currently advertises checked and status: String; code accepts only status: UserApplicationStatus.

 #[utoipa::path(
     get,
@@
     params(
-        ("limit" = Option<i64>, Query, description = "Maximum number of applications to return (default: 50)"),
-        ("page" = Option<i64>, Query, description = "Page (default: 1)"),
-        ("status" = Option<String>, Query, description = "Filter by application status: 'pending', 'accepted', or 'rejected'"),
-        ("checked" = Option<bool>, Query, description = "Filter by checked status: true for checked (accepted/rejected), false for unchecked (pending)")
+        ("limit" = Option<i64>, Query, description = "Maximum number of applications to return (default: 50)"),
+        ("page" = Option<i64>, Query, description = "Page (default: 1)"),
+        ("status" = Option<UserApplicationStatus>, Query, description = "Filter by status: pending | accepted | rejected")
     ),
backend/storage/src/repositories/torrent_request_vote_repository.rs (2)

14-20: TOCTOU: race between balance check and deduction can yield negatives.
Add guard conditions to the UPDATE so the single statement stays valid even under concurrency.

-        let current_user = self.find_user_with_id(user_id).await?;
-        if current_user.bonus_points - torrent_request_vote.bounty_bonus_points < 0 {
+        let current_user = self.find_user_with_id(user_id).await?;
+        if current_user.bonus_points - torrent_request_vote.bounty_bonus_points < 0 {
             return Err(Error::InsufficientBonusPointsForBounty);
         }
         if current_user.uploaded - torrent_request_vote.bounty_upload < 0 {
             return Err(Error::InsufficientUploadForBounty);
         }
@@
-                    UPDATE users u
-                    SET
-                        uploaded = u.uploaded - $3,
-                        bonus_points = u.bonus_points - $4
-                    WHERE u.id = (SELECT created_by_id FROM inserted_vote)
+                    UPDATE users u
+                    SET
+                        uploaded = u.uploaded - $3,
+                        bonus_points = u.bonus_points - $4
+                    WHERE u.id = (SELECT created_by_id FROM inserted_vote)
+                      AND u.uploaded >= $3
+                      AND u.bonus_points >= $4
+                    RETURNING u.id
                 )
                 SELECT
-                    inserted_vote.*
-                FROM inserted_vote
+                    inserted_vote.*
+                FROM inserted_vote
+                JOIN updated_user ON TRUE

15-20: Reject negative bounties early.
Ensure bounty_bonus_points and bounty_upload are non-negative to avoid accidental crediting.

-        if current_user.bonus_points - torrent_request_vote.bounty_bonus_points < 0 {
+        if torrent_request_vote.bounty_bonus_points < 0 || torrent_request_vote.bounty_upload < 0 {
+            return Err(Error::BadRequest);
+        }
+        if current_user.bonus_points - torrent_request_vote.bounty_bonus_points < 0 {
             return Err(Error::InsufficientBonusPointsForBounty);
         }
backend/api/src/handlers/forum/create_forum_thread.rs (1)

12-13: OpenAPI/docs say 200 but handler returns 201.
Align the documented response with the actual status.

-        (status = 200, description = "Successfully created the forum thread", body=ForumThread),
+        (status = 201, description = "Successfully created the forum thread", body=ForumThread),

Also applies to: 25-25

backend/api/src/handlers/users/get_me.rs (2)

13-21: OpenAPI response type is incorrect vs actual payload shape

The handler returns a composite object with peers, warnings, counts, and torrents, not a Profile. Update the schema to match the real response or refactor the handler to return a Profile.

Apply this minimal schema fix:

-    responses(
-        (status = 200, description = "Successfully got the user's profile", body=Profile),
-    )
+    responses(
+        (status = 200, description = "Successfully got the user's profile and related data", body=serde_json::Value),
+    )

65-73: Avoid unwrap() on JSON indexing; default safely to empty lists

Unwrap can panic if "title_groups" is absent/null. Use a safe default to prevent 500s.

Apply:

-    Ok(HttpResponse::Ok().json(json!({
-            "user": current_user,
-            "peers":peers,
-            "user_warnings": user_warnings,
-            "unread_conversations_amount": unread_conversations_amount,
-            "unread_notifications_amount":unread_notifications_amount,
-            "last_five_uploaded_torrents": uploaded_torrents.get("title_groups").unwrap(),
-            "last_five_snatched_torrents": snatched_torrents.get("title_groups").unwrap()
-    })))
+    let uploaded_title_groups = uploaded_torrents
+        .get("title_groups")
+        .cloned()
+        .unwrap_or_else(|| json!([]));
+    let snatched_title_groups = snatched_torrents
+        .get("title_groups")
+        .cloned()
+        .unwrap_or_else(|| json!([]));
+
+    Ok(HttpResponse::Ok().json(json!({
+        "user": current_user,
+        "peers": peers,
+        "user_warnings": user_warnings,
+        "unread_conversations_amount": unread_conversations_amount,
+        "unread_notifications_amount": unread_notifications_amount,
+        "last_five_uploaded_torrents": uploaded_title_groups,
+        "last_five_snatched_torrents": snatched_title_groups
+    })))
backend/api/src/middlewares/jwt_middleware.rs (1)

48-51: Do not panic on malformed api_key header

to_str().expect(...) can panic and 500 the request. Return 401 instead.

Apply:

-    } else if let Some(api_key) = req.headers().get("api_key") {
-        let api_key = api_key.to_str().expect("api_key malformed").to_owned();
+    } else if let Some(api_key) = req.headers().get("api_key") {
+        let api_key = match api_key.to_str() {
+            Ok(v) => v.to_owned(),
+            Err(_) => return Err((ErrorUnauthorized("api_key malformed"), req)),
+        };
backend/storage/src/repositories/torrent_repository.rs (1)

128-135: Panic risk: unwrap on enum parsing from user input

Features::from_str(...).ok().unwrap() will panic on invalid values. Either validate and error out, or safely skip invalid items.

Apply safe, non-panicking parsing (short-term fix). Prefer returning a 400 upstream after adding validation:

-            .bind(
-                torrent_form
-                    .features
-                    .split(',')
-                    .filter(|f| !f.is_empty())
-                    .map(|f| Features::from_str(f).ok().unwrap())
-                    .collect::<Vec<Features>>(),
-            )
+            .bind({
+                torrent_form
+                    .features
+                    .split(',')
+                    .filter(|f| !f.is_empty())
+                    .filter_map(|f| Features::from_str(f.trim()).ok())
+                    .collect::<Vec<Features>>()
+            })

Follow-up: add proper validation and return a domain error if any feature is invalid.

backend/storage/src/repositories/title_group_repository.rs (1)

238-242: Avoid unwrap() on possibly NULL JSON; return a typed error

Unwrap will panic if title_group_data is NULL (e.g., edge-case joins). Return a domain error instead.

Apply:

-        Ok(title_group.title_group_data.unwrap())
+        let data = title_group
+            .title_group_data
+            .ok_or(Error::TitleGroupNotFound)?;
+        Ok(data)
🧹 Nitpick comments (42)
backend/api/Cargo.toml (1)

13-13: Retain futures-util dependency
Usage found in backend/api/src/middlewares/jwt_middleware.rs (line 9), so don’t remove it.
Optional: pin futures = "0.3.31" in Cargo.toml to align its patch version with futures-util.

backend/api/src/handlers/users/get_registered_users.rs (1)

15-18: Centralize role checks: replace magic strings with a shared UserClass enum

  • Define a UserClass enum (e.g. in backend/api/src/models/user.rs) with variants for each role (Tracker, Staff, …) and implement an as_str() or Display for string-based checks.
  • In get_registered_users.rs (line 17), change
    -    if user.class != "tracker" {
    +    if user.class != UserClass::Tracker.as_str() {
          return Err(Error::InsufficientPrivileges);
       }
  • Replace all other inline user.class == "staff" and "tracker" checks in backend handlers (e.g. edit_torrent.rs, get_registered_torrents.rs) and mirror equivalent constants or generated types in the front-end.
backend/api/src/handlers/torrents/create_torrent.rs (1)

8-17: Document auth requirement in OpenAPI.

Add bearer security to the path so clients know this endpoint is authenticated.

 #[utoipa::path(
     post,
     operation_id = "Create torrent",
     tag = "Torrent",
     path = "/api/torrents",
+    security(("bearerAuth" = [])),
     request_body(content = UploadedTorrent, content_type = "multipart/form-data"),
     responses(
         (status = 201, description = "Successfully uploaded the torrent", body=Torrent),
     )
 )]
backend/api/src/handlers/search/search_torrents.rs (2)

31-34: Consider optional auth to keep search public while enabling personalization.

If search must work for anonymous users, switch to Option and forward user.as_ref().map(|u| u.sub). If auth is mandatory, document it (see next comment).

-pub async fn exec(
-    form: web::Json<TorrentSearch>,
-    arc: web::Data<Arcadia>,
-    user: JwtAuthData,
-) -> Result<HttpResponse> {
-    let search_results = arc.pool.search_torrents(&form, Some(user.sub)).await?;
+pub async fn exec(
+    form: web::Json<TorrentSearch>,
+    arc: web::Data<Arcadia>,
+    user: Option<JwtAuthData>,
+) -> Result<HttpResponse> {
+    let user_id = user.as_ref().map(|u| u.sub);
+    let search_results = arc.pool.search_torrents(&form, user_id).await?;

19-27: Clarify API design: GET with JSON body and auth docs.

  • GET with web::Json is atypical; prefer POST with request_body or move filters to Query.
  • If auth is required, add bearer security to the path.
 #[utoipa::path(
-    get,
+    post,
     operation_id = "Search torrents",
     tag = "Search",
     path = "/api/search/torrents/lite",
+    security(("bearerAuth" = [])),
+    request_body=TorrentSearch,
     responses(
         (status = 200, description = "Title groups and their torrents found", body=TorrentSearchResults),
     )
 )]

Confirm intended access policy for this route (public vs authenticated).

backend/api/src/handlers/subscriptions/create_subscription.rs (1)

26-30: Correct identity propagation; consider idempotency and enum-typing.

  • Using user.sub is correct.
  • Ensure create_subscription is idempotent (no error on duplicates).
  • Consider typing item as an enum to constrain allowed values and improve docs.

Example enum:

#[derive(Debug, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionItem { Artist, Thread, Torrent /* ... */ }

#[derive(Debug, Deserialize, IntoParams)]
pub struct AddSubscriptionQuery {
    pub item_id: i64,
    pub item: SubscriptionItem,
}
backend/api/src/handlers/torrents/get_registered_torrents.rs (2)

16-20: Harden role checks; avoid string literals for authorization.
Using "tracker" as a raw string is brittle. Centralize roles (e.g., Role enum or JwtAuthData helpers like is_tracker()) to avoid typos and enable future changes without sweeping edits.


12-15: Document 403 in OpenAPI.
The handler returns InsufficientPrivileges but the spec only documents 200. Add 403 to keep the contract accurate.

Apply this diff:

     responses(
-        (status = 200, description = "All registered torrents", body=Vec<TorrentMinimal>),
+        (status = 200, description = "All registered torrents", body=Vec<TorrentMinimal>),
+        (status = 403, description = "Insufficient privileges"),
     )
backend/api/src/handlers/title_groups/edit_title_group.rs (2)

23-29: Auth logic is correct; consider a staff fast-path and role helpers.

  • Current check is fine. For staff, you can skip the pre-read to reduce one DB hit.
  • Prefer centralized helpers (is_staff) or a Role enum over string comparisons.

Example minimal change:

 pub async fn exec(
   form: web::Json<EditedTitleGroup>,
   arc: web::Data<Arcadia>,
   user: JwtAuthData,
 ) -> Result<HttpResponse> {
-    let title_group = arc.pool.find_title_group(form.id).await?;
-
-    if title_group.created_by_id == user.sub || user.class == "staff" {
+    if user.class == "staff" {
+        let updated_title_group = arc.pool.update_title_group(&form, form.id).await?;
+        return Ok(HttpResponse::Ok().json(updated_title_group));
+    }
+    let title_group = arc.pool.find_title_group(form.id).await?;
+    if title_group.created_by_id == user.sub {
         let updated_title_group = arc.pool.update_title_group(&form, title_group.id).await?;
         Ok(HttpResponse::Ok().json(updated_title_group))
     } else {
         Err(Error::InsufficientPrivileges)
     }
 }

12-14: Add 403 to OpenAPI responses.
The handler can return InsufficientPrivileges; document it.

     responses(
-        (status = 200, description = "Successfully edited the title group", body=TitleGroup),
+        (status = 200, description = "Successfully edited the title group", body=TitleGroup),
+        (status = 403, description = "Insufficient privileges"),
     )
backend/api/src/handlers/auth/login.rs (2)

33-37: Embedding role/class in refresh tokens risks privilege drift.
If a user's class changes after issuance, a 90-day refresh token carrying the old class can mint privileged access. Prefer one of:

  • Store only sub/exp in refresh tokens and re-fetch class from DB on refresh; or
  • Introduce a refresh-specific claims struct (no class) and/or a token_version/jti with server-side revocation.

Would you like me to draft a refresh flow change (separate RefreshClaims + refresh endpoint update)?


14-16: Augment OpenAPI with auth error responses.
Login may return 401/403 for invalid credentials or banned users; document them.

     responses(
-        (status = 200, description = "Successfully logged in", body=LoginResponse),
+        (status = 200, description = "Successfully logged in", body=LoginResponse),
+        (status = 401, description = "Invalid credentials"),
+        (status = 403, description = "Account banned"),
     )
backend/storage/.sqlx/query-c5ef39624904dbc651eb31e96568a2d98359844aa26ccb5f9b99952d3ccc3d3c.json (1)

1-250: Descriptor looks consistent; confirm API key revocation/expiry filters.
Query fetches a full user row by API key and bans filter. If the schema supports key revocation or expiry, add those predicates to the SQL to prevent stale keys from authenticating.

If only sub/class are needed by the caller, consider selecting the minimal columns to reduce I/O.

backend/storage/src/repositories/torrent_report_repository.rs (1)

21-24: SQL bind order/shape is correct; consider explicit columns in RETURNING.
Using RETURNING * couples to table shape. Prefer returning explicit columns mapped by TorrentReport to harden against schema drift.

backend/api/src/handlers/torrents/create_torrent_report.rs (2)

12-13: Use 201 Created for POST and align OpenAPI docs.

Creation endpoint should return 201. Align handler and Utoipa.

-        (status = 200, description = "Torrent successfully reported", body=TorrentReport),
+        (status = 201, description = "Torrent successfully reported", body=TorrentReport),
@@
-    Ok(HttpResponse::Ok().json(report))
+    Ok(HttpResponse::Created().json(report))

Also applies to: 22-22


18-20: Nit: prefer auth (or jwt) over user to avoid confusion with DB-loaded users.

-    user: JwtAuthData,
+    auth: JwtAuthData,
@@
-    let report = arc.pool.report_torrent(&form, user.sub).await?;
+    let report = arc.pool.report_torrent(&form, auth.sub).await?;
backend/api/src/handlers/series/create_series.rs (1)

16-16: Nit: rename serie to payload to avoid confusion with the series value.

-    serie: web::Json<UserCreatedSeries>,
+    payload: web::Json<UserCreatedSeries>,
@@
-    let series = arc.pool.create_series(&serie, user.sub).await?;
+    let series = arc.pool.create_series(&payload, user.sub).await?;
backend/api/src/handlers/title_groups/get_title_group.rs (1)

18-21: Consider REST path param instead of query param for id.

Optional: switch to /api/title-groups/{id} with web::Path<i64> for a more canonical GET by id.

Also applies to: 24-27

backend/api/src/handlers/invitations/create_invitation.rs (2)

43-49: Avoid logging PII (email addresses).

Reduce logging of recipient email; log the error only or a masked address.

-            log::warn!(
-                "Failed to send invitation email to {}: {}",
-                invitation.receiver_email,
-                e
-            );
+            log::warn!("Failed to send invitation email: {}", e);

Also applies to: 51-51


22-25: Optional: narrow the user fetch to needed fields.

Consider a repository method that returns only { id, username, invitations } to reduce payload/latency.

backend/api/src/handlers/torrents/edit_torrent.rs (1)

23-28: Avoid magic string for role checks; centralize authorization

Replace "staff" literal with a shared helper/const to prevent typos and ease future role changes.

-    if torrent.created_by_id == user.sub || user.class == "staff" {
+    if torrent.created_by_id == user.sub || is_staff(&user.class) {

Outside this file:

pub fn is_staff(class: &str) -> bool {
    matches!(class, "staff" /* | "admin" etc. */)
}
backend/api/src/handlers/master_groups/create_master_group.rs (1)

12-13: OpenAPI response code mismatch (201 vs 200)

Handler returns Created (201) but docs advertise 200. Update schema for accuracy.

-        (status = 200, description = "Successfully created the master group", body=MasterGroup),
+        (status = 201, description = "Successfully created the master group", body=MasterGroup),
backend/api/src/handlers/users/warn_user.rs (2)

1-1: JWT extractor migration + privilege gate: LGTM; avoid magic string

Change set looks correct. Consider centralizing the "staff" check as noted elsewhere.

Also applies to: 18-26


12-13: OpenAPI response code mismatch (201 vs 200)

Handler returns Created (201) but docs advertise 200. Update for accuracy.

-        (status = 200, description = "Successfully warned the user", body=UserWarning),
+        (status = 201, description = "Successfully warned the user", body=UserWarning),
backend/api/src/handlers/forum/create_forum_post.rs (1)

12-13: Fix API doc status: endpoint returns 201 but docs say 200

Update Utoipa response to match HttpResponse::Created().

-        (status = 200, description = "Successfully created the forum post", body=ForumPost),
+        (status = 201, description = "Successfully created the forum post", body=ForumPost),
backend/api/src/handlers/torrents/delete_torrent.rs (1)

22-25: Extract staff role check into a centralized helper

  • Replace every direct comparison of user.class to the "staff" literal (e.g. in delete_torrent.rs, edit_torrent.rs, get_user_applications.rs, update_user_application_status.rs, warn_user.rs, create_wiki_article.rs, edit_title_group.rs) with a single method (e.g. user.is_staff()) or a dedicated Role enum/constant to avoid string-drift and ensure consistent privilege checks.
backend/api/src/handlers/torrent_requests/create_torrent_request_vote.rs (1)

14-15: Fix API doc status: handler returns 201 but docs say 200

Align Utoipa response with HttpResponse::Created().

-        (status = 200, description = "Successfully voted on the torrent_request", body=TorrentRequestVote),
+        (status = 201, description = "Successfully voted on the torrent_request", body=TorrentRequestVote),

Also applies to: 27-27

backend/api/src/handlers/wiki/create_wiki_article.rs (1)

20-21: Avoid hard-coded role string for RBAC.
Comparing to "staff" as a raw string is brittle; prefer a Role enum or shared constant (e.g., STAFF) to prevent typos/drift across handlers.

backend/api/src/handlers/torrent_requests/fill_torrent_request.rs (1)

29-29: Fix response typo: "succes" → "success".
Minor but user-visible.

-    Ok(HttpResponse::Ok().json(json!({"result": "succes"})))
+    Ok(HttpResponse::Ok().json(json!({"result": "success"})))
backend/api/src/handlers/subscriptions/remove_subscription.rs (1)

17-17: Grammar nit in OpenAPI description.
Use “unsubscribed from the item”.

-        (status = 200, description = "Successfully unsubscribed to the item"),
+        (status = 200, description = "Successfully unsubscribed from the item"),
backend/api/src/handlers/torrent_requests/get_torrent_request.rs (1)

1-1: Auth extractor switch looks good; keep param name explicit.

Change aligns with PR goal and avoids DB reads. Minor: name the unused param _auth for clarity during future refactors.

-    _: JwtAuthData,
+    _auth: JwtAuthData,

Also applies to: 26-27

backend/api/src/handlers/users/get_user.rs (1)

67-71: Avoid unwrap on response shaping.

Unchecked unwrap may 500 if key is missing. Return null or an empty array instead.

-        "last_five_uploaded_torrents": uploaded_torrents.get("title_groups").unwrap(),
-        "last_five_snatched_torrents": snatched_torrents.get("title_groups").unwrap()
+        "last_five_uploaded_torrents": uploaded_torrents
+            .get("title_groups")
+            .cloned()
+            .unwrap_or(serde_json::Value::Null),
+        "last_five_snatched_torrents": snatched_torrents
+            .get("title_groups")
+            .cloned()
+            .unwrap_or(serde_json::Value::Null)
backend/api/src/handlers/title_groups/create_title_group.rs (1)

31-35: Log dropped rating errors for observability.

filter_map(Result::ok) silently swallows failures. Add tracing at warn/debug to surface broken links or TMDB outages.

backend/storage/src/repositories/series_repository.rs (1)

14-17: Prefer explicit column list in RETURNING.

RETURNING * is brittle against schema changes. Explicitly list columns to keep compile-time struct/DB alignment predictable.

backend/api/src/handlers/users/edit_user.rs (1)

19-20: Prefer a clearer name than user for JWT claims (e.g., auth).
Improves readability and avoids confusion with a DB-backed User.

-pub async fn exec(
-    form: web::Json<EditedUser>,
-    arc: web::Data<Arcadia>,
-    user: JwtAuthData,
-) -> Result<HttpResponse> {
-    arc.pool.update_user(user.sub, &form).await?;
+pub async fn exec(
+    form: web::Json<EditedUser>,
+    arc: web::Data<Arcadia>,
+    auth: JwtAuthData,
+) -> Result<HttpResponse> {
+    arc.pool.update_user(auth.sub, &form).await?;
backend/api/src/handlers/user_applications/get_user_applications.rs (2)

41-47: Validate page/limit inputs and cap to sane bounds.
Prevents excessive reads and undefined behavior for zero/negative values.

-        .find_user_applications(
-            query.limit.unwrap_or(50),
-            query.page.unwrap_or(1),
+        .find_user_applications(
+            query.limit.filter(|&n| n > 0).unwrap_or(50).min(200),
+            query.page.filter(|&n| n > 0).unwrap_or(1),
             query.status.clone(),
         )

37-39: Use a unified role-check helper or enum instead of hard-coded "staff"
Multiple handlers (e.g. in get_user_applications.rs, update_user_application_status.rs, create_wiki_article.rs, etc.) directly compare user.class to "staff". Extract a helper (JwtAuthData::is_staff()) or introduce a UserClass enum (e.g. UserClass::Staff) and replace all user.class ==/!= "staff" checks to prevent typos and future drift.

backend/api/src/handlers/artists/create_artists.rs (2)

21-22: Minor: avoid shadowing artists for clarity.
Rename the local to created_artists.

-    let artists = arc.pool.create_artists(&artists, user.sub).await?;
+    let created_artists = arc.pool.create_artists(&artists, user.sub).await?;
-    Ok(HttpResponse::Created().json(artists))
+    Ok(HttpResponse::Created().json(created_artists))

19-20: Consistent naming for claims object.
Consider auth: JwtAuthData across handlers to standardize signatures.

-    user: JwtAuthData,
+    auth: JwtAuthData,
@@
-    let created_artists = arc.pool.create_artists(&artists, user.sub).await?;
+    let created_artists = arc.pool.create_artists(&artists, auth.sub).await?;
backend/api/src/middlewares/jwt_middleware.rs (2)

91-98: Surface operational errors for is_user_banned without leaking details

Conflating all errors to “account does not exist” hides operational issues. At least log them.

Apply:

-    let Ok(banned) = arc.pool.is_user_banned(user_id).await else {
-        return Err((
-            actix_web::error::ErrorUnauthorized("account does not exist"),
-            req,
-        ));
-    };
+    let banned = match arc.pool.is_user_banned(user_id).await {
+        Ok(b) => b,
+        Err(e) => {
+            log::warn!("is_user_banned({}) failed: {}", user_id, e);
+            return Err((actix_web::error::ErrorUnauthorized("account does not exist"), req));
+        }
+    };

74-89: Consider stricter JWT validation (iss/aud/nbf) as defense-in-depth

Validation::default() only checks signature and exp by default. If your tokens carry iss/aud/nbf, set them in Validation to reduce token replay across environments.

Would you like me to propose a Validation configuration aligned with your token issuer/audience?

backend/storage/src/repositories/torrent_repository.rs (1)

326-349: Minor: reduce transaction hold time by moving user lookup before BEGIN

You start a transaction, then fetch the user. Move find_user_with_id(user_id) before begin() to shorten lock duration.

Proposed outline:

  • let user = self.find_user_with_id(user_id).await?;
  • let mut tx = self.borrow::().begin().await?;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2295093 and 71be1cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (57)
  • backend/api/Cargo.toml (1 hunks)
  • backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (2 hunks)
  • backend/api/src/handlers/artists/create_artists.rs (2 hunks)
  • backend/api/src/handlers/auth/login.rs (2 hunks)
  • backend/api/src/handlers/auth/refresh_token.rs (2 hunks)
  • backend/api/src/handlers/conversations/create_conversation.rs (2 hunks)
  • backend/api/src/handlers/conversations/create_conversation_message.rs (2 hunks)
  • backend/api/src/handlers/conversations/get_conversation.rs (2 hunks)
  • backend/api/src/handlers/edition_groups/create_edition_group.rs (2 hunks)
  • backend/api/src/handlers/forum/create_forum_post.rs (2 hunks)
  • backend/api/src/handlers/forum/create_forum_thread.rs (2 hunks)
  • backend/api/src/handlers/gifts/create_gift.rs (2 hunks)
  • backend/api/src/handlers/invitations/create_invitation.rs (2 hunks)
  • backend/api/src/handlers/master_groups/create_master_group.rs (2 hunks)
  • backend/api/src/handlers/mod.rs (0 hunks)
  • backend/api/src/handlers/search/search_torrents.rs (2 hunks)
  • backend/api/src/handlers/series/create_series.rs (2 hunks)
  • backend/api/src/handlers/subscriptions/create_subscription.rs (2 hunks)
  • backend/api/src/handlers/subscriptions/remove_subscription.rs (2 hunks)
  • backend/api/src/handlers/title_groups/create_title_group.rs (4 hunks)
  • backend/api/src/handlers/title_groups/create_title_group_comment.rs (2 hunks)
  • backend/api/src/handlers/title_groups/edit_title_group.rs (2 hunks)
  • backend/api/src/handlers/title_groups/get_title_group.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/create_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/create_torrent_request_vote.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/fill_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/get_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrents/create_torrent.rs (2 hunks)
  • backend/api/src/handlers/torrents/create_torrent_report.rs (2 hunks)
  • backend/api/src/handlers/torrents/delete_torrent.rs (3 hunks)
  • backend/api/src/handlers/torrents/download_dottorrent_file.rs (2 hunks)
  • backend/api/src/handlers/torrents/edit_torrent.rs (2 hunks)
  • backend/api/src/handlers/torrents/get_registered_torrents.rs (2 hunks)
  • backend/api/src/handlers/torrents/get_upload_information.rs (2 hunks)
  • backend/api/src/handlers/user_applications/get_user_applications.rs (2 hunks)
  • backend/api/src/handlers/user_applications/update_user_application_status.rs (2 hunks)
  • backend/api/src/handlers/users/create_api_key.rs (2 hunks)
  • backend/api/src/handlers/users/edit_user.rs (2 hunks)
  • backend/api/src/handlers/users/get_me.rs (3 hunks)
  • backend/api/src/handlers/users/get_registered_users.rs (2 hunks)
  • backend/api/src/handlers/users/get_user.rs (3 hunks)
  • backend/api/src/handlers/users/get_user_conversations.rs (2 hunks)
  • backend/api/src/handlers/users/warn_user.rs (2 hunks)
  • backend/api/src/handlers/wiki/create_wiki_article.rs (2 hunks)
  • backend/api/src/middlewares/jwt_middleware.rs (3 hunks)
  • backend/storage/.sqlx/query-c5ef39624904dbc651eb31e96568a2d98359844aa26ccb5f9b99952d3ccc3d3c.json (1 hunks)
  • backend/storage/.sqlx/query-d97d331262b4156f941a88ef0e6237b02f581be5d5b13e75dd1e54727175c73e.json (0 hunks)
  • backend/storage/src/models/user.rs (1 hunks)
  • backend/storage/src/repositories/auth_repository.rs (2 hunks)
  • backend/storage/src/repositories/series_repository.rs (2 hunks)
  • backend/storage/src/repositories/title_group_comment_repository.rs (3 hunks)
  • backend/storage/src/repositories/title_group_repository.rs (5 hunks)
  • backend/storage/src/repositories/torrent_report_repository.rs (3 hunks)
  • backend/storage/src/repositories/torrent_repository.rs (4 hunks)
  • backend/storage/src/repositories/torrent_request_repository.rs (4 hunks)
  • backend/storage/src/repositories/torrent_request_vote_repository.rs (2 hunks)
  • backend/storage/src/repositories/user_repository.rs (1 hunks)
💤 Files with no reviewable changes (2)
  • backend/api/src/handlers/mod.rs
  • backend/storage/.sqlx/query-d97d331262b4156f941a88ef0e6237b02f581be5d5b13e75dd1e54727175c73e.json
🧰 Additional context used
🧬 Code graph analysis (10)
backend/api/src/handlers/users/get_user_conversations.rs (5)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (15-26)
backend/api/src/handlers/conversations/create_conversation.rs (1)
  • exec (15-27)
backend/api/src/handlers/conversations/create_conversation_message.rs (1)
  • exec (15-26)
backend/api/src/handlers/conversations/get_conversation.rs (1)
  • exec (23-31)
backend/api/src/handlers/search/search_torrents.rs (1)
  • exec (28-36)
backend/storage/src/repositories/auth_repository.rs (4)
frontend/src/services/api/userService.ts (1)
  • User (18-18)
backend/storage/src/repositories/title_group_repository.rs (1)
  • sqlx (36-36)
backend/storage/src/repositories/torrent_repository.rs (1)
  • sqlx (102-102)
backend/storage/src/repositories/torrent_request_repository.rs (1)
  • sqlx (30-30)
backend/api/src/handlers/users/get_me.rs (2)
backend/api/src/handlers/conversations/create_conversation.rs (1)
  • exec (15-27)
backend/api/src/handlers/gifts/create_gift.rs (1)
  • exec (15-31)
backend/api/src/handlers/torrents/get_registered_torrents.rs (3)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (15-26)
backend/api/src/handlers/artists/create_artists.rs (1)
  • exec (16-24)
backend/api/src/handlers/search/search_torrents.rs (1)
  • exec (28-36)
backend/storage/src/repositories/series_repository.rs (1)
backend/storage/src/connection_pool.rs (1)
  • borrow (23-25)
backend/api/src/handlers/users/get_registered_users.rs (7)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (15-26)
backend/api/src/handlers/artists/create_artists.rs (1)
  • exec (16-24)
backend/api/src/handlers/auth/login.rs (1)
  • exec (18-63)
backend/api/src/handlers/auth/refresh_token.rs (1)
  • exec (18-56)
backend/api/src/handlers/conversations/create_conversation.rs (1)
  • exec (15-27)
backend/api/src/handlers/gifts/create_gift.rs (1)
  • exec (15-31)
backend/api/src/handlers/invitations/create_invitation.rs (1)
  • exec (17-55)
backend/storage/src/repositories/torrent_report_repository.rs (1)
frontend/src/services/api/torrentService.ts (2)
  • TorrentReport (121-121)
  • UserCreatedTorrentReport (119-119)
backend/api/src/handlers/torrents/get_upload_information.rs (4)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (15-26)
backend/api/src/handlers/gifts/create_gift.rs (1)
  • exec (15-31)
backend/api/src/handlers/invitations/create_invitation.rs (1)
  • exec (17-55)
backend/api/src/handlers/search/search_torrents.rs (1)
  • exec (28-36)
backend/api/src/middlewares/jwt_middleware.rs (1)
backend/api/src/handlers/auth/refresh_token.rs (1)
  • decode (19-19)
backend/api/src/handlers/title_groups/create_title_group.rs (1)
backend/api/src/handlers/external_db/get_tmdb_data.rs (1)
  • get_tmdb_rating (106-138)

Comment on lines 18 to 23
user: JwtAuthData,
) -> Result<HttpResponse> {
let affiliations = arc
.pool
.create_artists_affiliation(&artists, current_user_id.0)
.create_artists_affiliation(&artists, user.sub)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify required authorization for creating affiliations.
If this operation was previously staff-only (common in similar create endpoints), we should keep parity by checking user.class or centralizing an is_staff guard.

Run to confirm prior/other handlers’ RBAC patterns:


🏁 Script executed:

#!/bin/bash
# Inspect affiliated-artist creation and nearby RBAC checks
rg -nP --type=rust -C3 'create_artists_affiliation|affiliated-artists|Affiliated Artist' backend/api/src

# Look for common privilege checks used elsewhere
rg -nP --type=rust -C2 'InsufficientPrivileges|user\.class\s*!=\s*"staff"|is_staff' backend/api/src

Length of output: 8589


Add staff-only authorization to create_affiliated_artists handler
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (lines 19–23) lacks the user.class != "staff" check—add the same guard used in other create endpoints to prevent unauthorized affiliation creation.

🤖 Prompt for AI Agents
In backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs
around lines 18 to 23, the handler does not enforce staff-only access; add the
same guard used in other create endpoints by checking if user.class != "staff"
immediately after extracting JwtAuthData and before calling
create_artists_affiliation, and return an appropriate HTTP 403/Unauthorized
response (with the same error type/format used elsewhere) when the check fails
so only staff can create affiliations.

Comment on lines 18 to 23
user: JwtAuthData,
) -> Result<HttpResponse> {
let message = arc
.pool
.create_conversation_message(&message, current_user.id)
.create_conversation_message(&message, user.sub)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Identity propagation via user.sub is correct; ensure membership checks.

Before insert, validate that user.sub is a participant of the conversation referenced in message.


🏁 Script executed:

#!/bin/bash
# Storage must check conversation membership for the author
rg -nP -C3 '(pub\s+async\s+fn|fn)\s+create_conversation_message\s*\('
rg -nP -C3 'CHECK|ensure|validate|membership|participant'

Length of output: 43345


Enforce conversation-participation check in storage layer

In create_conversation_message (backend/storage/src/repositories/conversation_repository.rs:40), verify current_user_id is a participant of message.conversation_id before inserting the message and return a forbidden error if the user isn’t in that conversation.

🤖 Prompt for AI Agents
In backend/api/src/handlers/conversations/create_conversation_message.rs around
lines 18 to 23, update the storage layer call to enforce a participation check:
in backend/storage/src/repositories/conversation_repository.rs inside
create_conversation_message (around line 40) confirm that current_user_id is a
participant of message.conversation_id before performing the insert; if the user
is not a participant, return a forbidden error (map to an appropriate storage
error that the handler translates into an HTTP 403) otherwise proceed to insert
the message—ensure the check queries the conversation participants table (or
joins users_in_conversation) in a single transaction or prior lookup to avoid
TOCTOU and return a clear forbidden error when the check fails.

Comment on lines 18 to 21
user: JwtAuthData,
) -> Result<HttpResponse> {
let current_user = arc.pool.find_user_with_id(user.sub).await?;
if current_user.bonus_points < gift.bonus_points {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid TOCTOU on balances; enforce checks atomically in storage.

Between reading balances and creating the gift, another transfer could occur. Ensure create_gift performs validation and deduction in a single DB transaction.

I can draft a transactional storage impl that validates and decrements in one step and returns domain errors.

Also applies to: 28-31


🏁 Script executed:

#!/bin/bash
# Inspect storage impl for atomicity (transaction, SELECT ... FOR UPDATE / balance checks).
rg -nP 'fn\s+create_gift|pub\s+async\s+fn\s+create_gift' -C6 backend/storage

Length of output: 1226


🏁 Script executed:

#!/bin/bash
# Display implementation of decrement_bonus_points_and_freeleech_tokens to verify atomicity
rg -nP 'fn\s+decrement_bonus_points_and_freeleech_tokens' -C20 backend/storage/src/repositories/gift_repository.rs

Length of output: 1326


Enforce atomic balance validation and deduction within storage.create_gift
Move the bonus_points/freeleech_tokens check into the same DB transaction by updating backend/storage/src/repositories/gift_repository.rs to:

UPDATE users
   SET bonus_points      = bonus_points      - $1,
       freeleech_tokens  = freeleech_tokens  - $2
 WHERE id                = $3
   AND bonus_points     >= $1
   AND freeleech_tokens >= $2

then verify rows_affected == 1 and return an “insufficient balance” error before inserting the gift.

🤖 Prompt for AI Agents
In backend/api/src/handlers/gifts/create_gift.rs around lines 18–21: the current
pre-check reads user's bonus_points before calling storage.create_gift, which
permits race conditions; instead implement the balance check and deduction
atomically inside backend/storage/src/repositories/gift_repository.rs by
replacing the separate read-with-check with a single UPDATE that decrements
bonus_points and freeleech_tokens only when both balances are sufficient (using
WHERE bonus_points >= $1 AND freeleech_tokens >= $2 AND id = $3), then verify
rows_affected == 1 and return an “insufficient balance” error if it is 0, before
proceeding to insert the gift in the same transaction.

Comment on lines 21 to 26
user: JwtAuthData,
) -> Result<HttpResponse> {
// TODO : check if user can upload

let torrent = arc.pool.create_torrent(&form, &current_user).await?;
let torrent = arc.pool.create_torrent(&form, user.sub).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Use claims to gate upload permissions and verify storage signature.

  • Add a role/claims check before persisting (e.g., based on user.class), otherwise any authenticated account can upload.
  • Verify arcadia_storage::Pool::create_torrent now accepts an i64 user id; if it still expects a User/UserId, this will break.

Run to verify storage signatures and call sites:

Example inline guard (adjust roles/error type as needed):

 pub async fn exec(
     form: MultipartForm<UploadedTorrent>,
     arc: web::Data<Arcadia>,
     user: JwtAuthData,
 ) -> Result<HttpResponse> {
-    // TODO : check if user can upload
+    // Enforce upload permissions from claims
+    match user.class.as_str() {
+        "Uploader" | "Staff" | "Moderator" => {}
+        _ => return Ok(HttpResponse::Forbidden().finish()),
+    }
 
     let torrent = arc.pool.create_torrent(&form, user.sub).await?;

🏁 Script executed:

#!/bin/bash
# Confirm storage signature uses a scalar user id
rg -nP -C2 '(pub\s+async\s+fn|fn)\s+create_torrent\s*\('

# Ensure no remaining create_torrent calls pass User/UserId
rg -nP -C2 '\.create_torrent\s*\('

Length of output: 925


🏁 Script executed:

#!/bin/bash
sed -n '30,45p' backend/storage/src/repositories/torrent_repository.rs

Length of output: 542


Enforce upload permissions based on user claims

The storage API’s create_torrent already accepts an i64 user_id, so passing user.sub matches the signature and introduces no breaking change. You still need to gate uploads by role—insert an authorization guard using user.class (or your chosen claim) before persisting:

 pub async fn exec(
     form: MultipartForm<UploadedTorrent>,
     arc: web::Data<Arcadia>,
     user: JwtAuthData,
 ) -> Result<HttpResponse> {
-    // TODO: check if user can upload
+    // Enforce upload permissions from claims
+    match user.class.as_str() {
+        "Uploader" | "Staff" | "Moderator" => {}
+        _ => return Ok(HttpResponse::Forbidden().finish()),
+    }
     let torrent = arc.pool.create_torrent(&form, user.sub).await?;
📝 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
user: JwtAuthData,
) -> Result<HttpResponse> {
// TODO : check if user can upload
let torrent = arc.pool.create_torrent(&form, &current_user).await?;
let torrent = arc.pool.create_torrent(&form, user.sub).await?;
pub async fn exec(
form: MultipartForm<UploadedTorrent>,
arc: web::Data<Arcadia>,
user: JwtAuthData,
) -> Result<HttpResponse> {
// Enforce upload permissions from claims
match user.class.as_str() {
"Uploader" | "Staff" | "Moderator" => {}
_ => return Ok(HttpResponse::Forbidden().finish()),
}
let torrent = arc.pool.create_torrent(&form, user.sub).await?;
// …rest of handler…
}
🤖 Prompt for AI Agents
In backend/api/src/handlers/torrents/create_torrent.rs around lines 21 to 26,
add an authorization guard before calling arc.pool.create_torrent: check the
incoming JwtAuthData.user.class (or chosen claim) to ensure the user has the
required role/permission to upload, and if not return an appropriate
HttpResponse (Forbidden or Unauthorized); only proceed to call
arc.pool.create_torrent(&form, user.sub).await? when the role check passes.
Ensure the check is clear, returns early on failure, and does not change the
create_torrent signature.

Comment on lines 26 to 29
let user_url = &arc
.frontend_url
.join(&format!("/user/{}", current_user.id))
.join(&format!("/user/{}", user.sub))
.unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid potential panic on URL join; replace unwrap with safe construction or bubble error

unwrap() will crash the handler if base URL is malformed. Prefer safe string construction for this static path or map the error into your domain error.

Option A (safe string build):

-    let user_url = &arc
-        .frontend_url
-        .join(&format!("/user/{}", user.sub))
-        .unwrap();
+    let user_url = format!(
+        "{}/user/{}",
+        arc.frontend_url.as_str().trim_end_matches('/'),
+        user.sub
+    );

Then below, keep &user_url.as_str() as-is.

Option B (bubble URL error; choose the right Error variant):

-    let user_url = &arc
-        .frontend_url
-        .join(&format!("/user/{}", user.sub))
-        .unwrap();
+    let user_url = arc
+        .frontend_url
+        .join(&format!("/user/{}", user.sub))
+        .map_err(|_| Error::InvalidUrl)?; // verify the exact variant/name
+    let user_url = &user_url;

Quick check for an appropriate error variant name:


🏁 Script executed:

#!/bin/bash
# Look for a usable variant (e.g., InvalidUrl, UrlError, InternalError) in Error:
rg -nP --type=rust -C2 'enum\s+Error\b' $(fd Error -t f | tr '\n' ' ')
rg -nP --type=rust -C1 'Invalid.*Url|Url.*Invalid|Internal.*Error' arcadia_common

Length of output: 1456


🏁 Script executed:

#!/bin/bash
file=$(fd delete_torrent.rs -t f | head -n1)
echo "File: $file"
echo "---- Imports ----"
grep -n '^use ' "$file" || true
echo "---- Error imports ----"
grep -n 'use .*Error' "$file" || true
echo "---- Function signature ----"
grep -n 'fn delete_torrent' -C2 "$file" || true
echo "---- Context around join ----"
grep -nC3 'join' "$file" || true

Length of output: 1342


🏁 Script executed:

#!/bin/bash
ls -1 backend/common/src/error
echo "==== mod.rs ===="
sed -n '1,200p' backend/common/src/error/mod.rs

Length of output: 6123


Avoid panic: remove unwrap() on URL join
Replace the unwrap() at backend/api/src/handlers/torrents/delete_torrent.rs:26–29 with manual string construction:

-    let user_url = &arc
-        .frontend_url
-        .join(&format!("/user/{}", user.sub))
-        .unwrap();
+    let user_url = format!(
+        "{}/user/{}",
+        arc.frontend_url.as_str().trim_end_matches('/'),
+        user.sub
+    );

If you’d rather propagate errors, add an Error::InvalidFrontendUrl variant to arcadia_common::error::Error and map the join failure to it instead.

📝 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
let user_url = &arc
.frontend_url
.join(&format!("/user/{}", current_user.id))
.join(&format!("/user/{}", user.sub))
.unwrap();
let user_url = format!(
"{}/user/{}",
arc.frontend_url.as_str().trim_end_matches('/'),
user.sub
);
🤖 Prompt for AI Agents
In backend/api/src/handlers/torrents/delete_torrent.rs around lines 26 to 29,
remove the call to .join(...).unwrap() which can panic; instead either (A) build
the URL as a string safely using formatting (e.g. let user_url =
format!("{}/user/{}", arc.frontend_url.as_str().trim_end_matches('/'),
user.sub); and use that String/Url::parse with proper error handling) or (B)
propagate the join failure by adding an Error::InvalidFrontendUrl variant to
arcadia_common::error::Error and map the Url::join error to that variant
(returning Err(Error::InvalidFrontendUrl(...)) from this handler); ensure no
unwrap remains and handle/return the error appropriately.

Comment on lines 22 to 24
pub async fn exec(arc: web::Data<Arcadia>, user: JwtAuthData) -> Result<HttpResponse> {
let mut current_user = arc.pool.find_user_with_id(user.sub).await?;
current_user.password_hash = String::from("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid ad-hoc scrubbing; return a sanitized projection

Zeroing password_hash is brittle and risks leaking other sensitive fields (e.g., passkeys) since current_user is serialized wholesale. Prefer fetching/returning a dedicated projection (e.g., Profile) that excludes secrets.

Proposed direction:

-pub async fn exec(arc: web::Data<Arcadia>, user: JwtAuthData) -> Result<HttpResponse> {
-    let mut current_user = arc.pool.find_user_with_id(user.sub).await?;
-    current_user.password_hash = String::from("");
+pub async fn exec(arc: web::Data<Arcadia>, user: JwtAuthData) -> Result<HttpResponse> {
+    // Repository should expose a method that returns a sanitized view:
+    let current_user: Profile = arc.pool.find_user_profile(user.sub).await?;

I can add find_user_profile and wire the Profile mapping if you want.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/api/src/handlers/users/get_me.rs around lines 22 to 24, avoid ad-hoc
scrubbing by zeroing password_hash on the full user model; instead fetch and
return a dedicated sanitized projection (e.g., Profile) that omits secrets.
Implement or call a new repository method like find_user_profile(user_id) that
selects only non-sensitive fields (email, display_name, id, avatar, etc.), map
the DB row into the Profile DTO, and return that Profile from this handler
rather than serializing the full User struct with mutated fields. Ensure all
sensitive fields (password_hash, passkeys, tokens) are excluded from the
projection and update any types/handler signatures accordingly.

Comment on lines 33 to 34
_: JwtAuthData,
) -> Result<HttpResponse> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Bug: passing target user's id as viewer id to search_torrents.

search_torrents likely expects the authenticated viewer id (for visibility/flags). Use JWT subject, not the profile’s id.

-    _: JwtAuthData,
+    auth: JwtAuthData,
@@
-        .search_torrents(&torrent_search, Some(user.id))
+        .search_torrents(&torrent_search, Some(auth.sub))
@@
-        .search_torrents(&torrent_search, Some(user.id))
+        .search_torrents(&torrent_search, Some(auth.sub))

Also applies to: 55-59, 63-66


🏁 Script executed:

#!/bin/bash
# Confirm search_torrents second arg semantics (viewer/current_user id).
rg -nP 'fn\s+search_torrents\s*\(' -C3 backend/storage

Length of output: 617


🏁 Script executed:

#!/bin/bash
# Display documentation and implementation context for search_torrents
sed -n '350,420p' backend/storage/src/repositories/torrent_repository.rs

Length of output: 2607


🏁 Script executed:

#!/bin/bash
# Locate calls to search_torrents in get_user handler to verify arguments
rg -nP 'search_torrents' -C3 backend/api/src/handlers/users/get_user.rs

Length of output: 581


Use JWT subject (auth.sub) instead of user.id in search_torrents calls

Replace the unused _ : JwtAuthData binding with auth: JwtAuthData, and pass auth.sub rather than user.id to both .search_torrents invocations:

-    _: JwtAuthData,
+    auth: JwtAuthData,
@@
-        .search_torrents(&torrent_search, Some(user.id))
+        .search_torrents(&torrent_search, Some(auth.sub))
@@
-        .search_torrents(&torrent_search, Some(user.id))
+        .search_torrents(&torrent_search, Some(auth.sub))
🤖 Prompt for AI Agents
In backend/api/src/handlers/users/get_user.rs around lines 33 to 34, the
function currently binds the JWT auth as an unused `_ : JwtAuthData`; replace
that with `auth: JwtAuthData` and update both `.search_torrents` calls to pass
`auth.sub` instead of `user.id` so the searches use the JWT subject; ensure the
new `auth` variable is used and compiles.

Comment on lines +107 to 112
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: i64,
pub exp: usize,
pub class: String,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Use i64 for exp to match JWT NumericDate and avoid usize portability traps.

exp as usize can differ across platforms; i64 (or u64) is the common, portable choice and aligns with chrono timestamps.

Apply:

 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Claims {
     pub sub: i64,
-    pub exp: usize,
+    pub exp: i64,
     pub class: String,
 }

Follow-ups outside this file (remove usize casts):

// login.rs, refresh_token.rs
exp: token_expiration_date.timestamp(),
// and
exp: (now + Duration::days(90)).timestamp(),

To find all casts to usize for exp:


🏁 Script executed:

#!/bin/bash
rg -nP '\.timestamp\(\)\s+as\s+usize|pub\s+exp:\s*usize'

Length of output: 564


Use i64 for exp in JWT claims and remove usize casts

  • In backend/storage/src/models/user.rs, change
    pub exp: usize, → pub exp: i64,
  • In backend/api/src/handlers/auth/refresh_token.rs (lines 28, 41) and
    backend/api/src/handlers/auth/login.rs (lines 35, 48), remove as usize so that timestamp() (an i64) matches the new field type.
🤖 Prompt for AI Agents
In backend/storage/src/models/user.rs around lines 107 to 112, change the Claims
struct field type from `pub exp: usize,` to `pub exp: i64,`; then in
backend/api/src/handlers/auth/refresh_token.rs (around lines 28 and 41) and
backend/api/src/handlers/auth/login.rs (around lines 35 and 48) remove the `as
usize` casts when assigning `timestamp()` values so the i64 timestamp is
assigned directly to `exp`, ensuring types match and no unnecessary casting
remains.

@FrenchGithubUser

Copy link
Copy Markdown
Contributor

Could you rename it to something like AuthData ? Since we also support api keys, which are not jwt

@FrenchGithubUser

Copy link
Copy Markdown
Contributor

Also, storing other data than the user id in the token is usually a bad practice, especially the user class which might be updated in the db and not in the token, giving access to some routes while it shouldn't, and vice-versa.

Anyways, moving away from jwt is planned and might need to be done before this ?

@ppoliani

ppoliani commented Aug 28, 2025 •

Copy link
Copy Markdown
Contributor Author

I'm not very convinced that using authz is a bad practise. The fact that the role for example can change is a backend implementation detail. In any case we can implement a token invalidation logic which will render such tokens invalid shall the role change or we ban user.

In reality a full fledged Auth server should deal with all these authn/authz tasks, but for our use case, I believe we can keep it simple for the time being

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (13)
backend/api/src/handlers/conversations/create_conversation_message.rs (1)

12-13: Fix OpenAPI response code to match implementation (201 vs 200).

You return Created (201) but the spec advertises 200. Align the doc to 201.

Apply:

-        (status = 200, description = "Successfully created the conversation's message", body=ConversationMessage),
+        (status = 201, description = "Successfully created the conversation's message", body=ConversationMessage),

Also applies to: 25-25

backend/api/src/handlers/master_groups/create_master_group.rs (1)

12-13: Fix OpenAPI response code to match implementation (201 vs 200).

Handler returns Created; spec says 200.

Apply:

-        (status = 200, description = "Successfully created the master group", body=MasterGroup),
+        (status = 201, description = "Successfully created the master group", body=MasterGroup),

Also applies to: 22-22

backend/api/src/handlers/torrents/download_dottorrent_file.rs (1)

57-60: Serve the correct MIME type for .torrent files.

Use application/x-bittorrent instead of octet-stream.

Apply:

-    Ok(HttpResponse::Ok()
-        .insert_header(ContentType::octet_stream())
+    Ok(HttpResponse::Ok()
+        .insert_header((actix_web::http::header::CONTENT_TYPE, "application/x-bittorrent"))
         .insert_header(cd)
         .body(torrent.file_contents))
backend/api/src/handlers/conversations/create_conversation.rs (1)

12-13: Fix OpenAPI/status mismatch: code returns 201 but docs say 200

Update the documented response to 201 to match HttpResponse::Created.

-        (status = 200, description = "Successfully created the conversation and first message", body=Conversation),
+        (status = 201, description = "Successfully created the conversation and first message", body=Conversation),

Also applies to: 26-27

backend/api/src/handlers/wiki/create_wiki_article.rs (1)

12-13: Fix OpenAPI/status mismatch: code returns 201 but docs say 200

Align docs with the Created response.

-        (status = 200, description = "Successfully created the wiki article", body=WikiArticle),
+        (status = 201, description = "Successfully created the wiki article", body=WikiArticle),

Also applies to: 26-27

backend/api/src/handlers/forum/create_forum_post.rs (1)

12-13: Fix OpenAPI/status mismatch: code returns 201 but docs say 200

Align response docs with HttpResponse::Created.

-        (status = 200, description = "Successfully created the forum post", body=ForumPost),
+        (status = 201, description = "Successfully created the forum post", body=ForumPost),

Also applies to: 22-23

backend/api/src/handlers/title_groups/create_title_group.rs (2)

25-35: Avoid panic: don’t unwrap TMDB API key; gate rating fetches

arc.tmdb_api_key.clone().unwrap() will panic when unset. Also avoid cloning the key per link.

-    let rating_futures: Vec<_> = form
-        .external_links
-        .iter()
-        .filter(|link| link.contains("https://www.themoviedb.org/"))
-        .map(|link| get_tmdb_rating(link, arc.tmdb_api_key.clone().unwrap()))
-        .collect();
-    let ratings: Vec<PublicRating> = join_all(rating_futures)
-        .await
-        .into_iter()
-        .filter_map(Result::ok)
-        .collect();
+    let ratings: Vec<PublicRating> = if let Some(tmdb_key) = arc.tmdb_api_key.clone() {
+        let futs: Vec<_> = form
+            .external_links
+            .iter()
+            .filter(|link| link.contains("https://www.themoviedb.org/"))
+            .map(|link| get_tmdb_rating(link, tmdb_key.clone()))
+            .collect();
+        join_all(futs).await.into_iter().filter_map(Result::ok).collect()
+    } else {
+        log::warn!("TMDB API key not configured; skipping rating enrichment");
+        Vec::new()
+    };

29-31: Replace all unwraps in get_tmdb_rating with error propagation

In backend/api/src/handlers/external_db/get_tmdb_data.rs, remove every .unwrap() in get_tmdb_rating (regex parsing, ID extraction, API‐key retrieval and .await.unwrap() on HTTP calls) and propagate failures via ? so the server won’t panic on malformed URLs or network errors.

backend/api/src/middlewares/jwt_middleware.rs (2)

46-51: Don’t panic on malformed API key; accept X-API-Key too

expect() will crash the worker on invalid header encoding. Return 401 instead and accept the conventional X-API-Key header.

-} else if let Some(api_key) = req.headers().get("api_key") {
-    let api_key = api_key.to_str().expect("api_key malformed").to_owned();
-    validate_api_key(req, &api_key).await
+} else if let Some(hv) = req
+    .headers()
+    .get("api_key")
+    .or_else(|| req.headers().get("x-api-key"))
+{
+    match hv.to_str() {
+        Ok(api_key) => validate_api_key(req, api_key).await,
+        Err(_) => Err((
+            actix_web::error::ErrorUnauthorized(
+                "authentication error, malformed API key header",
+            ),
+            req,
+        )),
+    }

74-89: JWT validation should restrict algorithms and optionally iss/aud

Validation::default() accepts whatever alg the token declares. Lock this down (e.g., HS256) and, if configured, validate issuer/audience.

-use jsonwebtoken::{decode, errors::ErrorKind, DecodingKey, Validation};
+use jsonwebtoken::{decode, errors::ErrorKind, Algorithm, DecodingKey, Validation};

-    let validation = Validation::default();
+    // Restrict algorithms and explicitly validate exp; optionally set iss/aud
+    let mut validation = Validation::new(Algorithm::HS256);
+    validation.validate_exp = true;
+    // if you have these in Arcadia config, wire them:
+    // if let Some(ref iss) = arc.jwt_issuer { validation.set_issuer(&[iss]); }
+    // if let Some(ref aud) = arc.jwt_audience { validation.set_audience(&[aud]); }
backend/api/src/handlers/users/get_me.rs (3)

13-21: OpenAPI schema mismatch: response is not Profile

The handler returns a composite object (user + peers + counts + torrents) but the doc claims body=Profile. Fix the schema or the response.

-    responses(
-        (status = 200, description = "Successfully got the user's profile", body=Profile),
-    )
+    responses(
+        (status = 200, description = "Successfully got the user's dashboard data", body = GetMeResponse),
+    )

Add a typed response DTO (outside this hunk):

#[derive(serde::Serialize, utoipa::ToSchema)]
pub struct GetMeResponse {
    pub user: Profile,
    pub peers: serde_json::Value, // replace with concrete type if available
    pub user_warnings: serde_json::Value, // replace with concrete type
    pub unread_conversations_amount: i64,
    pub unread_notifications_amount: i64,
    pub last_five_uploaded_torrents: serde_json::Value, // or concrete type
    pub last_five_snatched_torrents: serde_json::Value, // or concrete type
}

45-55: Prefer typed results and avoid unwrap on map access

search_torrents(...).get("title_groups").unwrap() can panic. Either return a typed struct from storage or handle missing keys gracefully.

-    let uploaded_torrents = arc
+    let uploaded_torrents = arc
         .pool
         .search_torrents(&torrent_search, Some(current_user.id))
         .await?;
@@
-    let snatched_torrents = arc
+    let snatched_torrents = arc
         .pool
         .search_torrents(&torrent_search, Some(current_user.id))
         .await?;

And when extracting:

-            "last_five_uploaded_torrents": uploaded_torrents.get("title_groups").unwrap(),
-            "last_five_snatched_torrents": snatched_torrents.get("title_groups").unwrap()
+            "last_five_uploaded_torrents": uploaded_torrents.get("title_groups").cloned().unwrap_or_default(),
+            "last_five_snatched_torrents": snatched_torrents.get("title_groups").cloned().unwrap_or_default()

65-73: Return a typed DTO instead of ad-hoc json!

Move to a GetMeResponse struct (see above) and construct it without json!, which will catch type drift at compile time.

-    Ok(HttpResponse::Ok().json(json!({
-            "user": current_user,
-            "peers":peers,
-            "user_warnings": user_warnings,
-            "unread_conversations_amount": unread_conversations_amount,
-            "unread_notifications_amount":unread_notifications_amount,
-            "last_five_uploaded_torrents": uploaded_torrents.get("title_groups").unwrap(),
-            "last_five_snatched_torrents": snatched_torrents.get("title_groups").unwrap()
-    })))
+    Ok(HttpResponse::Ok().json(GetMeResponse {
+        user: current_user,
+        peers,
+        user_warnings,
+        unread_conversations_amount,
+        unread_notifications_amount,
+        last_five_uploaded_torrents: uploaded_torrents
+            .get("title_groups")
+            .cloned()
+            .unwrap_or_default(),
+        last_five_snatched_torrents: snatched_torrents
+            .get("title_groups")
+            .cloned()
+            .unwrap_or_default(),
+    }))
♻️ Duplicate comments (9)
backend/api/src/handlers/conversations/create_conversation_message.rs (1)

1-1: Ensure storage enforces “author is participant” before insert.

Passing user.sub is good; verify create_conversation_message checks membership and returns 403/forbidden on violation. Avoid TOCTOU by validating in the same transaction as insert.

Run:

#!/bin/bash
# Find impl and inspect for participation check
rg -nP -C3 '(pub\s+async\s+fn|fn)\s+create_conversation_message\s*\(' backend | cat
rg -nP -C3 'participant|membership|users?_in_conversation|conversation_participants' backend/storage || true

Also applies to: 18-19, 22-22

backend/api/src/handlers/torrents/create_torrent.rs (1)

21-26: Gate uploads by role using claims; replace TODO with an authorization check.

Right now any authenticated user can upload. Enforce role(s) from JWT and fail fast.

Apply:

 pub async fn exec(
     form: MultipartForm<UploadedTorrent>,
     arc: web::Data<Arcadia>,
     user: Authdata,
 ) -> Result<HttpResponse> {
-    // TODO : check if user can upload
+    // Enforce upload permissions from claims
+    let role = user.class.to_ascii_lowercase();
+    if !matches!(role.as_str(), "uploader" | "staff" | "moderator") {
+        return Ok(HttpResponse::Forbidden().finish());
+    }
 
     let torrent = arc.pool.create_torrent(&form, user.sub).await?;

Also standardize role casing across handlers to avoid brittle string compares.

backend/api/src/handlers/torrents/delete_torrent.rs (1)

26-29: Remove unwrap() on URL join to avoid panic.

Build the URL safely as a string; no runtime join, no panic.

Apply:

-    let user_url = &arc
-        .frontend_url
-        .join(&format!("/user/{}", user.sub))
-        .unwrap();
+    let user_url = format!(
+        "{}/user/{}",
+        arc.frontend_url.as_str().trim_end_matches('/'),
+        user.sub
+    );
@@
-        &user_url.as_str(),
+        &user_url.as_str(),

Also applies to: 38-39

backend/api/src/handlers/torrents/edit_torrent.rs (1)

3-3: Duplicate: Authdata naming nit noted elsewhere

See prior comment about renaming Authdata → AuthData across the crate.

backend/api/src/handlers/conversations/create_conversation.rs (1)

1-1: Duplicate: Authdata naming nit noted elsewhere

See earlier note to rename Authdata → AuthData.

backend/api/src/handlers/wiki/create_wiki_article.rs (1)

1-1: Duplicate: Authdata naming nit noted elsewhere

See earlier note to rename Authdata → AuthData.

backend/api/src/handlers/forum/create_forum_post.rs (1)

1-1: Duplicate: Authdata naming nit noted elsewhere

See earlier note to rename Authdata → AuthData.

backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)

18-23: Add staff-only (or equivalent) authorization check

Previous review requested enforcing RBAC here; please gate by user.class (e.g., staff) before calling create_artists_affiliation.

 pub async fn exec(
     artists: web::Json<Vec<UserCreatedAffiliatedArtist>>,
     arc: web::Data<Arcadia>,
     user: Authdata,
 ) -> Result<HttpResponse> {
+    if user.class != "staff" {
+        return Err(arcadia_common::error::Error::InsufficientPrivileges);
+    }
     let affiliations = arc
         .pool
         .create_artists_affiliation(&artists, user.sub)
         .await?;

If the project uses a different role or helper (e.g., is_staff(user.class)), reuse that for consistency.

backend/api/src/handlers/users/get_me.rs (1)

22-24: Avoid ad-hoc scrubbing; fetch a sanitized Profile

Zeroing password_hash is brittle and risks leaking other secrets. Fetch a Profile projection instead.

-pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
-    let mut current_user = arc.pool.find_user_with_id(user.sub).await?;
-    current_user.password_hash = String::from("");
+pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
+    let current_user: Profile = arc.pool.find_user_profile(user.sub).await?;
🧹 Nitpick comments (12)
backend/api/src/handlers/conversations/create_conversation_message.rs (1)

6-14: Document the request body in the OpenAPI path.

The handler accepts JSON; add request_body for accurate API docs.

Apply:

 #[utoipa::path(
     post,
     operation_id = "Create conversation message",
     tag = "Conversation",
     path = "/api/conversations/messages",
+    request_body = UserCreatedConversationMessage,
     responses(
         (status = 201, description = "Successfully created the conversation's message", body=ConversationMessage),
     )
 )]

Also applies to: 16-23

backend/api/src/handlers/master_groups/create_master_group.rs (1)

6-14: Add request_body to OpenAPI for accurate docs.

Declare the expected JSON payload.

Apply:

 #[utoipa::path(
     post,
     operation_id = "Create master group",
     tag = "Master Group",
     path = "/api/master-groups",
+    request_body = UserCreatedMasterGroup,
     responses(
         (status = 201, description = "Successfully created the master group", body=MasterGroup),
     )
 )]

Also applies to: 15-21

backend/api/src/handlers/torrents/edit_torrent.rs (2)

23-28: Avoid stringly-typed role checks

Comparing user.class to a string is fragile. At minimum, use a constant; ideally, model roles as an enum and parse/validate at the middleware boundary.

Apply within this function:

-    if torrent.created_by_id == user.sub || user.class == "staff" {
+    // Consider centralizing this constant or using an enum.
+    const STAFF: &str = "staff";
+    if torrent.created_by_id == user.sub || user.class == STAFF {

If you prefer an enum-based approach, I can draft it.


23-28: Optional: reduce resource-enumeration signal

Returning InsufficientPrivileges after a successful find can leak that the torrent exists to unauthorized users. Consider responding with NotFound in both cases or normalizing errors to avoid ID enumeration.

backend/api/src/handlers/conversations/create_conversation.rs (1)

21-27: Avoid variable shadowing for clarity

The created entity shadows the request payload variable name. Rename the result.

-    let conversation = arc
+    let created_conversation = arc
         .pool
-        .create_conversation(&mut conversation, user.sub)
+        .create_conversation(&mut conversation, user.sub)
         .await?;
-
-    Ok(HttpResponse::Created().json(conversation))
+    Ok(HttpResponse::Created().json(created_conversation))
backend/api/src/handlers/wiki/create_wiki_article.rs (1)

20-22: Avoid stringly-typed role checks

Use a constant or enum instead of a bare "staff" string to prevent typos and centralize role semantics.

-    if user.class != "staff" {
+    const STAFF: &str = "staff";
+    if user.class != STAFF {
         return Err(Error::InsufficientPrivileges);
     }
backend/api/src/handlers/subscriptions/remove_subscription.rs (1)

26-27: Consider surfacing a 404 when nothing was deleted

If delete_subscription returns “0 rows affected,” return a 404 Not Found instead of a generic 200 to better signal idempotent no-ops vs. successful deletes.

-    arc.pool
-        .delete_subscription(query.item_id, &query.item, user.sub)
-        .await?;
+    let affected = arc
+        .pool
+        .delete_subscription(query.item_id, &query.item, user.sub)
+        .await?;
+    if affected == 0 {
+        return Ok(HttpResponse::NotFound().finish());
+    }
backend/api/src/handlers/title_groups/create_title_group.rs (1)

17-18: Fix OpenAPI response code to match actual 201 Created

The handler returns Created but the schema advertises 200.

-        (status = 200, description = "Successfully created the title_group", body=TitleGroup),
+        (status = 201, description = "Successfully created the title_group", body=TitleGroup),

Also applies to: 53-53

backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)

11-13: Align OpenAPI status with 201 Created

Schema says 200 but the handler returns Created.

-        (status = 200, description = "Successfully created the artist affiliations", body=Vec<AffiliatedArtistHierarchy>),
+        (status = 201, description = "Successfully created the artist affiliations", body=Vec<AffiliatedArtistHierarchy>),

Also applies to: 25-25

backend/api/src/middlewares/jwt_middleware.rs (2)

12-16: Type name/style: use AuthData and consider a typed role

Rust types are CamelCase; prefer AuthData. Also consider an enum for class (e.g., UserClass) to avoid stringly-typed auth decisions.

-#[derive(Debug, Clone)]
-pub struct Authdata {
+#[derive(Debug, Clone)]
+pub struct AuthData {
     pub sub: i64,
     pub class: String,
 }

18-29: Extractor looks good; align error text with middleware

FromRequest is fine. Consider reusing the same 401 message as authenticate_user for consistency.

backend/api/src/handlers/users/get_me.rs (1)

22-64: Latency: issue N+1 and sequential awaits; batch with try_join

Most calls here are independent. Run them concurrently to cut tail latency.

-    let peers = arc.pool.get_user_peers(current_user.id).await;
-    let user_warnings = arc.pool.find_user_warnings(current_user.id).await;
+    use futures_util::try_join;
+    let (peers, user_warnings) = try_join!(
+        arc.pool.get_user_peers(current_user.id),
+        arc.pool.find_user_warnings(current_user.id),
+    )?;
@@
-    let uploaded_torrents = arc
-        .pool
-        .search_torrents(&torrent_search, Some(current_user.id))
-        .await?;
+    let uploaded_fut = arc.pool.search_torrents(&torrent_search, Some(current_user.id));
@@
-    let snatched_torrents = arc
-        .pool
-        .search_torrents(&torrent_search, Some(current_user.id))
-        .await?;
+    let snatched_fut = arc.pool.search_torrents(&torrent_search, Some(current_user.id));
+    let (uploaded_torrents, snatched_torrents) = try_join!(uploaded_fut, snatched_fut)?;
@@
-    let unread_conversations_amount = arc
-        .pool
-        .find_unread_conversations_amount(current_user.id)
-        .await?;
-    let unread_notifications_amount = arc
-        .pool
-        .find_unread_notifications_amount(current_user.id)
-        .await?;
+    let (unread_conversations_amount, unread_notifications_amount) = try_join!(
+        arc.pool.find_unread_conversations_amount(current_user.id),
+        arc.pool.find_unread_notifications_amount(current_user.id),
+    )?;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 71be1cc and 9278eed.

📒 Files selected for processing (41)
  • backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (2 hunks)
  • backend/api/src/handlers/artists/create_artists.rs (2 hunks)
  • backend/api/src/handlers/conversations/create_conversation.rs (2 hunks)
  • backend/api/src/handlers/conversations/create_conversation_message.rs (2 hunks)
  • backend/api/src/handlers/conversations/get_conversation.rs (2 hunks)
  • backend/api/src/handlers/edition_groups/create_edition_group.rs (2 hunks)
  • backend/api/src/handlers/forum/create_forum_post.rs (2 hunks)
  • backend/api/src/handlers/forum/create_forum_thread.rs (2 hunks)
  • backend/api/src/handlers/gifts/create_gift.rs (2 hunks)
  • backend/api/src/handlers/invitations/create_invitation.rs (2 hunks)
  • backend/api/src/handlers/master_groups/create_master_group.rs (2 hunks)
  • backend/api/src/handlers/search/search_torrents.rs (2 hunks)
  • backend/api/src/handlers/series/create_series.rs (2 hunks)
  • backend/api/src/handlers/subscriptions/create_subscription.rs (2 hunks)
  • backend/api/src/handlers/subscriptions/remove_subscription.rs (2 hunks)
  • backend/api/src/handlers/title_groups/create_title_group.rs (4 hunks)
  • backend/api/src/handlers/title_groups/create_title_group_comment.rs (2 hunks)
  • backend/api/src/handlers/title_groups/edit_title_group.rs (2 hunks)
  • backend/api/src/handlers/title_groups/get_title_group.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/create_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/create_torrent_request_vote.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/fill_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/get_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrents/create_torrent.rs (2 hunks)
  • backend/api/src/handlers/torrents/create_torrent_report.rs (2 hunks)
  • backend/api/src/handlers/torrents/delete_torrent.rs (3 hunks)
  • backend/api/src/handlers/torrents/download_dottorrent_file.rs (2 hunks)
  • backend/api/src/handlers/torrents/edit_torrent.rs (2 hunks)
  • backend/api/src/handlers/torrents/get_registered_torrents.rs (2 hunks)
  • backend/api/src/handlers/torrents/get_upload_information.rs (2 hunks)
  • backend/api/src/handlers/user_applications/get_user_applications.rs (2 hunks)
  • backend/api/src/handlers/user_applications/update_user_application_status.rs (2 hunks)
  • backend/api/src/handlers/users/create_api_key.rs (2 hunks)
  • backend/api/src/handlers/users/edit_user.rs (2 hunks)
  • backend/api/src/handlers/users/get_me.rs (3 hunks)
  • backend/api/src/handlers/users/get_registered_users.rs (2 hunks)
  • backend/api/src/handlers/users/get_user.rs (3 hunks)
  • backend/api/src/handlers/users/get_user_conversations.rs (2 hunks)
  • backend/api/src/handlers/users/warn_user.rs (2 hunks)
  • backend/api/src/handlers/wiki/create_wiki_article.rs (2 hunks)
  • backend/api/src/middlewares/jwt_middleware.rs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (24)
  • backend/api/src/handlers/torrents/get_registered_torrents.rs
  • backend/api/src/handlers/users/get_user_conversations.rs
  • backend/api/src/handlers/edition_groups/create_edition_group.rs
  • backend/api/src/handlers/conversations/get_conversation.rs
  • backend/api/src/handlers/forum/create_forum_thread.rs
  • backend/api/src/handlers/torrent_requests/create_torrent_request_vote.rs
  • backend/api/src/handlers/search/search_torrents.rs
  • backend/api/src/handlers/title_groups/get_title_group.rs
  • backend/api/src/handlers/users/warn_user.rs
  • backend/api/src/handlers/torrent_requests/get_torrent_request.rs
  • backend/api/src/handlers/torrent_requests/create_torrent_request.rs
  • backend/api/src/handlers/invitations/create_invitation.rs
  • backend/api/src/handlers/title_groups/create_title_group_comment.rs
  • backend/api/src/handlers/torrent_requests/fill_torrent_request.rs
  • backend/api/src/handlers/subscriptions/create_subscription.rs
  • backend/api/src/handlers/users/get_registered_users.rs
  • backend/api/src/handlers/series/create_series.rs
  • backend/api/src/handlers/user_applications/update_user_application_status.rs
  • backend/api/src/handlers/gifts/create_gift.rs
  • backend/api/src/handlers/users/edit_user.rs
  • backend/api/src/handlers/users/get_user.rs
  • backend/api/src/handlers/user_applications/get_user_applications.rs
  • backend/api/src/handlers/torrents/create_torrent_report.rs
  • backend/api/src/handlers/title_groups/edit_title_group.rs
🧰 Additional context used
🧬 Code graph analysis (4)
backend/api/src/handlers/torrents/get_upload_information.rs (4)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (15-26)
backend/api/src/handlers/gifts/create_gift.rs (1)
  • exec (15-31)
backend/api/src/handlers/invitations/create_invitation.rs (1)
  • exec (17-55)
backend/api/src/handlers/search/search_torrents.rs (1)
  • exec (28-36)
backend/api/src/handlers/users/get_me.rs (4)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (15-26)
backend/api/src/handlers/conversations/create_conversation.rs (1)
  • exec (15-27)
backend/api/src/handlers/gifts/create_gift.rs (1)
  • exec (15-31)
backend/api/src/handlers/invitations/create_invitation.rs (1)
  • exec (17-55)
backend/api/src/middlewares/jwt_middleware.rs (1)
backend/api/src/handlers/auth/refresh_token.rs (1)
  • decode (19-19)
backend/api/src/handlers/title_groups/create_title_group.rs (1)
backend/api/src/handlers/external_db/get_tmdb_data.rs (1)
  • get_tmdb_rating (106-138)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: schema-check
  • GitHub Check: build
  • GitHub Check: build
  • GitHub Check: schema-check
🔇 Additional comments (15)
backend/api/src/handlers/torrents/download_dottorrent_file.rs (1)

31-41: Confirm access control before download.
I didn’t see any ban, disabled-account, passkey or membership checks in get_torrent; ensure the caller’s rights (e.g. is_user_banned, account enabled, active passkey, membership rules) are validated before returning the torrent.

backend/api/src/handlers/torrents/delete_torrent.rs (1)

25-25: Reintroducing DB read counters PR goal; confirm necessity.

You fetch current_user from DB solely for display. If acceptable per PR scope, keep; otherwise consider passing the handler’s display info differently (e.g., claims, lookup by storage only when needed).

backend/api/src/handlers/users/create_api_key.rs (1)

19-22: LGTM: extractor swap and user.sub usage

The handler correctly switches to Authdata and passes user.sub to storage.

backend/api/src/handlers/torrents/edit_torrent.rs (1)

19-20: LGTM: handler signature updated to Authdata

Extractor change is consistent with the PR objective.

backend/api/src/handlers/conversations/create_conversation.rs (1)

23-24: No action needed: web::Json<UserCreatedConversation> implements DerefMut, so &mut conversation coerces to &mut UserCreatedConversation, matching the create_conversation signature.

backend/api/src/handlers/forum/create_forum_post.rs (1)

20-23: LGTM: switched to user.sub and returns 201

The storage call and response are consistent with the new Authdata flow.

backend/api/src/handlers/subscriptions/remove_subscription.rs (1)

2-3: Authdata substitution looks correct

Using Authdata and passing user.sub to delete_subscription aligns with the PR objective and avoids the per-request User load.

Also applies to: 23-27

backend/api/src/handlers/title_groups/create_title_group.rs (3)

23-24: RBAC: confirm required role for creating title groups

If this action is staff-only (or otherwise restricted), add an authorization guard using user.class to maintain parity with prior behavior.

Would you like me to scan the repo for where “create title group” was previously guarded and open a follow-up patch?


39-41: LGTM on identity flow

Passing user.sub into create_title_group matches the storage API changes.


47-51: LGTM on affiliated artists linkage

Setting title_group_id before create_artists_affiliation and using user.sub is correct.

backend/api/src/handlers/torrents/get_upload_information.rs (2)

22-28: Ensure tracker URL is configured; fail gracefully if missing

If arc.tracker.url can be None/misconfigured, return a 503 (or domain error) instead of relying on downstream assumptions.

-pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
+pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
     let current_user = arc.pool.find_user_with_id(user.sub).await?;
-    let announce_url = get_announce_url(
-        current_user.passkey_upper,
-        current_user.passkey_lower,
-        arc.tracker.url.as_ref(),
-    );
+    let tracker_url = match arc.tracker.url.as_ref() {
+        Some(u) => u,
+        None => {
+            log::error!("Tracker URL not configured");
+            return Err(arcadia_common::error::Error::ServiceUnavailable);
+        }
+    };
+    let announce_url = get_announce_url(current_user.passkey_upper, current_user.passkey_lower, tracker_url);

If Error::ServiceUnavailable doesn’t exist, I can wire an appropriate variant.


22-24: DB lookup is justified here

Fetching passkeys for announce_url generation requires a user load; this remains aligned with the PR’s intent to avoid unnecessary loads elsewhere.

backend/api/src/handlers/artists/create_artists.rs (2)

1-1: Authdata migration and identity propagation look good

Switch to Authdata and passing user.sub into create_artists aligns with the storage API.

Also applies to: 19-22


16-20: Confirm RBAC on artist creation

If creating artists is restricted (e.g., staff), add the same guard using user.class for parity with other create endpoints.

I can scan for existing patterns (InsufficientPrivileges checks) and propose a consistent helper.

backend/api/src/middlewares/jwt_middleware.rs (1)

103-108: LGTM: minimal claims injected into request context

Insertion of only sub and class matches the PR goal. Keep it minimal.

Comment on lines +18 to 21
user: Authdata,
) -> Result<HttpResponse> {
let master_group = arc
.pool
.create_master_group(&form, current_user_id.0)
.await?;
let master_group = arc.pool.create_master_group(&form, user.sub).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Enforce authorization (who can create master groups?).

Unless storage rejects unauthorized callers, add a claims-based guard (e.g., staff/mod only) for defense-in-depth.

Apply (adjust roles/policy as needed):

 pub async fn exec(
     form: web::Json<UserCreatedMasterGroup>,
     arc: web::Data<Arcadia>,
     user: Authdata,
 ) -> Result<HttpResponse> {
+    if user.class.to_ascii_lowercase() != "staff" {
+        return Ok(HttpResponse::Forbidden().finish());
+    }
     let master_group = arc.pool.create_master_group(&form, user.sub).await?;

🏁 Script executed:

#!/bin/bash
# Locate the definition of Authdata in the Rust codebase
rg -nP 'struct\s+Authdata\b' -C3 --type=rs

Length of output: 71


🏁 Script executed:

#!/bin/bash
rg -n "Authdata" -C3 -g '*.rs'
rg -n "AuthData" -C3 -g '*.rs'

Length of output: 48459


Enforce staff-only creation of master groups. Insert at the top of exec in backend/api/src/handlers/master_groups/create_master_group.rs before calling create_master_group:

 pub async fn exec(
     form: web::Json<UserCreatedMasterGroup>,
     arc: web::Data<Arcadia>,
     user: Authdata,
 ) -> Result<HttpResponse> {
+    if user.class.to_ascii_lowercase() != "staff" {
+        return Ok(HttpResponse::Forbidden().finish());
+    }
     let master_group = arc.pool.create_master_group(&form, user.sub).await?;
📝 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
user: Authdata,
) -> Result<HttpResponse> {
let master_group = arc
.pool
.create_master_group(&form, current_user_id.0)
.await?;
let master_group = arc.pool.create_master_group(&form, user.sub).await?;
pub async fn exec(
form: web::Json<UserCreatedMasterGroup>,
arc: web::Data<Arcadia>,
user: Authdata,
) -> Result<HttpResponse> {
if user.class.to_ascii_lowercase() != "staff" {
return Ok(HttpResponse::Forbidden().finish());
}
let master_group = arc.pool.create_master_group(&form, user.sub).await?;
🤖 Prompt for AI Agents
In backend/api/src/handlers/master_groups/create_master_group.rs around lines 18
to 21, add a staff-only guard at the top of exec before calling
arc.pool.create_master_group: check the Authdata on the incoming user (e.g.
user.is_staff or user.roles.contains("staff") depending on the Authdata shape)
and if the user is not staff return early with a 403 Forbidden (or
Err(actix_web::error::ErrorForbidden(...))) instead of proceeding; place this
check immediately before the create_master_group call so non-staff cannot create
master groups.

Comment on lines +22 to 24
if user.class != "staff" {
return Err(Error::InsufficientPrivileges);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize role comparison; avoid case sensitivity traps.

Compare on lowercased role or centralize via a helper to keep policy consistent across handlers.

Apply:

-    if user.class != "staff" {
+    if user.class.to_ascii_lowercase() != "staff" {
         return Err(Error::InsufficientPrivileges);
     }

Optionally add a shared is_staff(&str) helper and reuse.

📝 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
if user.class != "staff" {
return Err(Error::InsufficientPrivileges);
}
if user.class.to_ascii_lowercase() != "staff" {
return Err(Error::InsufficientPrivileges);
}
🤖 Prompt for AI Agents
In backend/api/src/handlers/torrents/delete_torrent.rs around lines 22 to 24,
the code compares user.class to the literal "staff" with a case-sensitive
equality which can fail for different-cased role values; change the check to a
case-insensitive comparison (e.g., compare lowercased values) or, better, call a
shared helper is_staff(&str) that centralizes role normalization and returns
true for any casing variant, then use that helper here to return
Err(Error::InsufficientPrivileges) when it returns false.

@@ -1,4 +1,4 @@
use crate::{handlers::User, Arcadia};
use crate::{middlewares::jwt_middleware::Authdata, Arcadia};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Use Rust type naming: rename Authdata → AuthData

Type names should be UpperCamelCase. Please rename the type and imports for consistency.

Run to locate all occurrences:


🏁 Script executed:

#!/bin/bash
rg -nP '\bAuthdata\b' -C1
ast-grep --pattern $'struct Authdata { $$$ }' || true

Length of output: 21798


Rename Authdata to AuthData
Update the struct declaration in backend/api/src/middlewares/jwt_middleware.rs:13 and all imports/uses of Authdata across the codebase to AuthData to comply with Rust UpperCamelCase type naming.

🤖 Prompt for AI Agents
In backend/api/src/handlers/users/create_api_key.rs around line 1, the import
uses the old type name `Authdata`; update this to `AuthData` and adjust all
other usages/imports accordingly. Also modify the struct declaration in
backend/api/src/middlewares/jwt_middleware.rs at line 13 from `Authdata` to
`AuthData`, update any pattern matches, type annotations, and imports across the
codebase to the new name, and run cargo check to catch remaining references.

Comment on lines +18 to 25
user: Authdata,
) -> Result<HttpResponse> {
if current_user.class != "staff" {
if user.class != "staff" {
return Err(Error::InsufficientPrivileges);
}

let article = arc
.pool
.create_wiki_article(&article, current_user.id)
.await?;
let article = arc.pool.create_wiki_article(&article, user.sub).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

JWT-embedded roles: ensure revocation and freshness

Since authorization depends on user.class from the token, confirm middleware verifies signature/alg, enforces short TTLs, and supports revocation on role changes (e.g., jti blacklist or auth_version/iat checks).


🏁 Script executed:

#!/bin/bash
# Inspect jwt_middleware for claim validation & revocation strategy
rg -nP 'struct\s+Authdata|Claims|class|sub|jti|iat' backend/api/src/middlewares/jwt_middleware.rs -n -C3 || true

Length of output: 1483


Enforce revocable, short-lived JWTs Signature and expiration are validated via decode() with default Validation in backend/api/src/middlewares/jwt_middleware.rs, but there’s no JTI- or auth_version/iat-based revocation—tokens remain valid even after role changes. Implement a revocation strategy (e.g. JTI blacklist or auth_version lookup against iat) and ensure issuance with a short TTL.

Comment on lines 91 to 98
let user_id = token_data.claims.sub;

let banned = arc.pool.is_user_banned(user_id).await;
let Ok(banned) = arc.pool.is_user_banned(user_id).await else {
return Err((
actix_web::error::ErrorUnauthorized("account does not exist"),
req,
));
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Don’t collapse all DB errors into “account does not exist”

Differentiate NotFound vs transient/storage errors; return 500 for backend issues, 401 only when the user truly doesn’t exist.

-    let Ok(banned) = arc.pool.is_user_banned(user_id).await else {
-        return Err((
-            actix_web::error::ErrorUnauthorized("account does not exist"),
-            req,
-        ));
-    };
+    let banned = match arc.pool.is_user_banned(user_id).await {
+        Ok(b) => b,
+        // Replace StorageError::NotFound with your concrete type
+        Err(e) if e.to_string().contains("not found") => {
+            return Err((actix_web::error::ErrorUnauthorized("account does not exist"), req))
+        }
+        Err(e) => {
+            log::warn!("is_user_banned({}) failed: {}", user_id, e);
+            return Err((actix_web::error::ErrorInternalServerError("authentication backend error"), req));
+        }
+    };

🏁 Script executed:

#!/bin/bash
# Search for occurrences and definition of `is_user_banned` to inspect its return type and error variants
rg -n --type=rs "is_user_banned" -C3

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Search for occurrences and definition of `is_user_banned` in Rust files
rg -n "is_user_banned" -C3 -g "*.rs"

Length of output: 1284


🏁 Script executed:

ls -R backend/storage/src

Length of output: 1082


🏁 Script executed:

#!/bin/bash
# Locate the definition of is_user_banned with surrounding context
grep -R -n "pub async fn is_user_banned" -C5 backend/storage/src/repositories/user_repository.rs

Length of output: 514


🏁 Script executed:

#!/bin/bash
# Find the Result alias and Error enum definitions in storage module
grep -R -n "type Result" -n backend/storage/src
grep -R -n "enum .*Error" -n backend/storage/src
# Show the full is_user_banned method implementation
sed -n '140,180p' backend/storage/src/repositories/user_repository.rs

Length of output: 1054


🏁 Script executed:

grep -R -n "type Result" backend/storage/src
grep -R -n "enum .*Error" backend/storage/src
sed -n '1,200p' backend/storage/src/lib.rs

Length of output: 214


🏁 Script executed:

#!/bin/bash
grep -R -n "token_data" -C5 backend/api/src/middlewares/jwt_middleware.rs

Length of output: 1039


🏁 Script executed:

#!/bin/bash
sed -n '1,80p' backend/storage/src/repositories/user_repository.rs

Length of output: 2392


Separate user-not-found from storage errors in is_user_banned and middleware

  • In backend/storage/src/repositories/user_repository.rs:is_user_banned, return Err(Error::UserWithIdNotFound(user_id)) instead of Ok(true) when no row is found.
  • In backend/api/src/middlewares/jwt_middleware.rs, match on the concrete Error variants:
    • Err(Error::UserWithIdNotFound(_)) → ErrorUnauthorized("account does not exist")
    • Err(_) → ErrorInternalServerError("authentication backend error")
    • Ok(true) → ErrorUnauthorized("account is banned")
    • Ok(false) → continue processing
🤖 Prompt for AI Agents
In backend/api/src/middlewares/jwt_middleware.rs around lines 91 to 98, and
backend/storage/src/repositories/user_repository.rs, change the behavior so
missing users produce a specific Error variant and the middleware matches on it:
in user_repository.rs make is_user_banned return
Err(Error::UserWithIdNotFound(user_id)) when the lookup finds no row (instead of
Ok(true)); in jwt_middleware.rs replace the current let Ok(banned) = ... else {
... } pattern with an explicit match on the repository result and map variants
as follows — Err(Error::UserWithIdNotFound(_)) -> return
ErrorUnauthorized("account does not exist"), Err(_) -> return
ErrorInternalServerError("authentication backend error"), Ok(true) -> return
ErrorUnauthorized("account is banned"), Ok(false) -> continue processing the
request.

Comment on lines 123 to 131
let user = match arc.pool.find_user_id_with_api_key(api_key).await {
Ok(user) => user,
Err(e) => return Err((actix_web::error::ErrorUnauthorized(e.to_string()), req)),
};

req.extensions_mut()
.insert(crate::handlers::UserId(user_id));
req.extensions_mut().insert(Authdata {
sub: user.id,
class: user.class,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

API key path misses ban/last_seen checks and leaks error details

  • Parity with JWT path: check bans and update last_seen.
  • Don’t surface internal error messages to clients; log them and return a generic 401.
-    let user = match arc.pool.find_user_id_with_api_key(api_key).await {
-        Ok(user) => user,
-        Err(e) => return Err((actix_web::error::ErrorUnauthorized(e.to_string()), req)),
-    };
+    let user = match arc.pool.find_user_id_with_api_key(api_key).await {
+        Ok(user) => user,
+        Err(e) => {
+            log::info!("api key auth failed: {}", e);
+            return Err((actix_web::error::ErrorUnauthorized("invalid API key"), req));
+        }
+    };
+    // Enforce bans and update last_seen just like bearer flow
+    if let Ok(true) = arc.pool.is_user_banned(user.id).await {
+        return Err((actix_web::error::ErrorUnauthorized("account banned"), req));
+    }
+    let _ = arc.pool.update_last_seen(user.id).await;
📝 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
let user = match arc.pool.find_user_id_with_api_key(api_key).await {
Ok(user) => user,
Err(e) => return Err((actix_web::error::ErrorUnauthorized(e.to_string()), req)),
};
req.extensions_mut()
.insert(crate::handlers::UserId(user_id));
req.extensions_mut().insert(Authdata {
sub: user.id,
class: user.class,
});
let user = match arc.pool.find_user_id_with_api_key(api_key).await {
Ok(user) => user,
Err(e) => {
log::info!("api key auth failed: {}", e);
return Err((actix_web::error::ErrorUnauthorized("invalid API key"), req));
}
};
// Enforce bans and update last_seen just like bearer flow
if let Ok(true) = arc.pool.is_user_banned(user.id).await {
return Err((actix_web::error::ErrorUnauthorized("account banned"), req));
}
let _ = arc.pool.update_last_seen(user.id).await;
req.extensions_mut().insert(Authdata {
sub: user.id,
class: user.class,
});
🤖 Prompt for AI Agents
In backend/api/src/middlewares/jwt_middleware.rs around lines 123–131, the
API-key branch currently returns DB error text to the client and doesn't perform
ban/last_seen handling like the JWT branch; change it so that DB errors are
logged (do not leak e.to_string() to the response) and you return a generic 401,
then after successfully loading the user check user.banned (or equivalent) and
return a generic Unauthorized if banned, and finally call the existing last_seen
update code (await the update, log any update error but do not fail the request)
before inserting Authdata into req.extensions_mut().

@FrenchGithubUser

Copy link
Copy Markdown
Contributor

Yea but having some logic to invalidate a jwt token kinda kills its advantage of not having to query the db...

This video is quite interesting imo: https://www.youtube.com/watch?v=NXXiKl8g6Rw

Sorry to insist a bit 😅 If you have good reasons to still use jwt over a more classic api key managed in the db (with expiration time etc, some of its logic is already implemented) then let me know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (12)
backend/api/src/handlers/forum/create_forum_post.rs (1)

6-17: Fix OpenAPI response code: handler returns 201 but spec documents 200.

Update the documented response to match the runtime status.

-        (status = 200, description = "Successfully created the forum post", body=ForumPost),
+        (status = 201, description = "Created forum post", body = ForumPost),
backend/api/src/handlers/edition_groups/create_edition_group.rs (2)

15-16: Fix OpenAPI response code mismatch (200 vs 201).

Handler returns 201 (Created) but the spec documents 200. Align to 201.

-        (status = 200, description = "Successfully created the edition_group", body=EditionGroup),
+        (status = 201, description = "Edition group created", body = EditionGroup),

Also applies to: 25-25


6-17: Specify the request body in the OpenAPI path.

Without request_body, clients won’t see the input schema.

 #[utoipa::path(
     post,
     operation_id = "Create edition group",
     tag = "Edition Group",
     path = "/api/edition-groups",
+    request_body = UserCreatedEditionGroup,
     security(
       ("http" = ["Bearer"])
     ),
backend/api/src/handlers/series/create_series.rs (1)

15-15: OpenAPI response code mismatch (200 vs 201 Created)

Handler returns HttpResponse::Created() (201) but the doc advertises 200. Fix the spec to avoid client confusion.

Apply:

-        (status = 200, description = "Successfully created the series", body=Series),
+        (status = 201, description = "Successfully created the series", body=Series),

Also applies to: 25-25

backend/api/src/handlers/users/get_me.rs (3)

13-24: OpenAPI schema is wrong (declares Profile, returns a composite object).

Doc says body=Profile but the handler returns a dashboard-like JSON with multiple fields. Fix the spec immediately (quick fix below), or define a proper DTO.

Apply this quick-fix diff to align the spec:

 #[utoipa::path(
@@
-    responses(
-        (status = 200, description = "Successfully got the user's profile", body=Profile),
-    )
+    responses(
+        (status = 200, description = "Successfully got the current user's dashboard object", body=serde_json::Value),
+    )
 )]

Preferable follow-up: introduce a GetMeResponse DTO with typed fields and switch body=GetMeResponse.


48-58: Avoid unwraps in HTTP response building; they can panic.

get("title_groups").unwrap() will 500 if the key is absent. Use a safe default.

Apply this diff:

@@
-    let uploaded_torrents = arc
+    let uploaded_torrents = arc
         .pool
         .search_torrents(&torrent_search, Some(current_user.id))
         .await?;
@@
-    let snatched_torrents = arc
+    let snatched_torrents = arc
         .pool
         .search_torrents(&torrent_search, Some(current_user.id))
         .await?;
+    let last_five_uploaded_torrents = uploaded_torrents
+        .get("title_groups")
+        .cloned()
+        .unwrap_or_else(|| json!([]));
+    let last_five_snatched_torrents = snatched_torrents
+        .get("title_groups")
+        .cloned()
+        .unwrap_or_else(|| json!([]));
@@
-            "last_five_uploaded_torrents": uploaded_torrents.get("title_groups").unwrap(),
-            "last_five_snatched_torrents": snatched_torrents.get("title_groups").unwrap()
+            "last_five_uploaded_torrents": last_five_uploaded_torrents,
+            "last_five_snatched_torrents": last_five_snatched_torrents

Note: This assumes search_torrents returns a serde_json::Map<String, Value>. If it returns a typed struct, expose fields instead of map access.

Also applies to: 74-75


68-76: Don’t serialize the full user model in the “user” field.

Even with password_hash blanked, other secrets/PII may leak. Ensure the “user” field is Profile (or a sanitized DTO).

If adopting Profile per earlier diff, this line stays as-is but remains safe:

-            "user": current_user,
+            "user": current_user, // Profile

Prefer also defining a typed GetMeResponse DTO and returning that instead of ad-hoc json!.

backend/api/src/handlers/title_groups/edit_title_group.rs (1)

26-31: Hide resource existence and centralize role checks

  • In the else branch, return Err(Error::NotFound) instead of Err(Error::InsufficientPrivileges) to return 404 and prevent ID enumeration:
    - Err(Error::InsufficientPrivileges)
    + Err(Error::NotFound)
  • Replace all direct user.class == "staff" comparisons with a centralized role abstraction (e.g. a Role enum or an is_staff() helper) to eliminate scattered string literals.
  • Update this endpoint’s OpenAPI responses to use 404 (“not found or unauthorized”) for unauthorized cases.
backend/api/src/handlers/torrents/delete_torrent.rs (2)

33-41: Tighten user-facing copy; fix punctuation and spacing.

Minor grammar/formatting issues in the message.

-    let displayed_reason = format!(
-        "A torrent you were a seeder on, has been deleted.
-  Please remove it from your torrent client.
-
-Reason: {}
-
-Handled by: [url={}]{}[/url]",
+    let displayed_reason = format!(
+        "A torrent you were seeding has been deleted.\n\
+Please remove it from your torrent client.\n\n\
+Reason: {}\n\n\
+Handled by: [url={}]{}[/url]",
         &form.reason,
         &user_url.as_str(),
-        current_user.username
+        staff_username
     );

33-43: Escape BBCode-sensitive characters in user inputs

User-controlled form.reason and current_user.username are embedded raw into BBCode, enabling injection or phishing.

  • Escape/sanitize form.reason before formatting.
  • Escape/sanitize current_user.username before formatting.
backend/api/src/handlers/affiliated_artists/remove_affiliated_artists.rs (1)

28-36: Plug the authorization gap: Authdata is ignored; any user can delete arbitrary affiliations by id

Right now the handler authenticates but does not authorize. Deletion is not scoped to the caller or privileged roles. Use Authdata, validate input, and scope the delete to the caller (or gate by class).

Apply a change like this (adjust repo API accordingly):

 pub async fn exec(
     query: actix_web_lab::extract::Query<RemoveAffiliatedArtistsQuery>,
     arc: web::Data<Arcadia>,
-    _: Authdata,
+    user: Authdata,
 ) -> Result<HttpResponse> {
-    // TODO: add protection based on user class
-    arc.pool
-        .delete_artists_affiliation(&query.affiliation_ids)
-        .await?;
+    // Optional: staff-only bypass based on class. Otherwise, scope to caller.
+    // if !matches!(user.class.as_str(), "Admin" | "Moderator") { ... }
+
+    // Basic validation + dedup to keep IN() lists sane.
+    let mut ids = query.affiliation_ids.clone();
+    ids.sort_unstable();
+    ids.dedup();
+    if ids.is_empty() {
+        return Ok(HttpResponse::BadRequest().json(json!({"error": "affiliation_ids cannot be empty"})));
+    }
+    if ids.len() > 1000 {
+        return Ok(HttpResponse::PayloadTooLarge().json(json!({"error": "too many affiliation_ids"})));
+    }
+
+    // Scope deletion to the caller to prevent cross-user deletes.
+    arc.pool
+        .delete_artists_affiliation_for_user(user.sub, &ids)
+        .await?;
 
-    Ok(HttpResponse::Ok().json(json!({"result": "success"})))
+    Ok(HttpResponse::NoContent().finish())
 }
backend/api/src/handlers/conversations/create_conversation.rs (1)

15-16: OpenAPI/docs mismatch: handler returns 201 but spec says 200

Clients will codegen against 200 while the server sends 201. Align the spec.

     responses(
-        (status = 200, description = "Successfully created the conversation and first message", body=Conversation),
+        (status = 201, description = "Successfully created the conversation and first message", body=Conversation),
     )
 )]
@@
-    Ok(HttpResponse::Created().json(conversation))
+    Ok(HttpResponse::Created().json(conversation))

Optionally add the request body to the spec for clarity:

 #[utoipa::path(
     post,
@@
-    responses(
+    request_body = UserCreatedConversation,
+    responses(

Also applies to: 29-30

♻️ Duplicate comments (4)
backend/api/src/handlers/users/create_api_key.rs (1)

1-1: Use Rust type naming: AuthData (UpperCamelCase).

Rename the imported type to AuthData for idiomatic Rust and consistency with prior feedback.

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::AuthData, Arcadia};
backend/api/src/handlers/users/get_me.rs (1)

25-27: Stop ad-hoc scrubbing; fetch a sanitized projection.

Zeroing password_hash on the full User is brittle and risks leaking other sensitive fields. Fetch a Profile (or similar) directly.

Apply this diff:

 pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
-    let mut current_user = arc.pool.find_user_with_id(user.sub).await?;
-    current_user.password_hash = String::from("");
+    let current_user: Profile = arc.pool.find_user_profile(user.sub).await?;
backend/api/src/handlers/torrents/delete_torrent.rs (2)

25-27: Normalize role check and avoid brittle string comparison.

Make the check case-insensitive or centralize via an is_staff(&str) helper. Also confirm your token-invalidation path covers role changes; for admin actions like deletes, consider verifying against DB if feasible.

-    if user.class != "staff" {
+    if user.class.to_ascii_lowercase() != "staff" {
         return Err(Error::InsufficientPrivileges);
     }

29-32: Remove unwrap() on URL join; avoid panic and simplify.

Build the URL string safely; also drop the unnecessary extra borrow at usage.

-    let user_url = &arc
-        .frontend_url
-        .join(&format!("/user/{}", user.sub))
-        .unwrap();
+    let user_url = format!(
+        "{}/user/{}",
+        arc.frontend_url.as_str().trim_end_matches('/'),
+        user.sub
+    );
@@
-        &user_url.as_str(),
+        user_url.as_str(),

Also applies to: 41-41

🧹 Nitpick comments (26)
backend/api/src/handlers/forum/create_forum_post.rs (1)

1-1: Type naming nit: prefer AuthData (CamelCase) over Authdata.

Rust types conventionally use CamelCase; consider renaming project-wide. If you do, this file would change as:

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::AuthData, Arcadia};
backend/api/src/handlers/users/create_api_key.rs (2)

24-27: Optional: include Location header for 201 Created and add an audit log.

Improves REST semantics and observability.

-    Ok(HttpResponse::Created().json(created_api_key))
+    // tracing::info!(user_id = auth.sub, api_key_id = created_api_key.id, "created API key");
+    Ok(HttpResponse::Created()
+        .append_header(("Location", format!("/api/users/api-keys/{}", created_api_key.id)))
+        .json(created_api_key))

22-22: Rename extractor parameter for clarity
Change the handler signature from

user: Authdata,

to

- user: Authdata,
+ auth: Authdata,

so it’s clear this isn’t a full User but JWT claims.

backend/api/src/handlers/conversations/get_conversation.rs (2)

1-1: Alias the type to idiomatic CamelCase and reduce coupling to middleware path

Rust types are CamelCase; aliasing also insulates this handler from a future move of the type.

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::Authdata as AuthData, Arcadia};

29-31: Clarify intent: name the param for claims and make the boolean self-documenting

  • "user" suggests a loaded entity; this is JWT claims.
  • Add an inline name to the boolean to avoid the “what does true mean?” question at call sites.
-pub async fn exec(
-    query: web::Query<GetConversationQuery>,
-    arc: web::Data<Arcadia>,
-    user: Authdata,
-) -> Result<HttpResponse> {
-    let conversation_with_messages = arc.pool.find_conversation(query.id, user.sub, true).await?;
+pub async fn exec(
+    query: web::Query<GetConversationQuery>,
+    arc: web::Data<Arcadia>,
+    auth: AuthData,
+) -> Result<HttpResponse> {
+    let conversation_with_messages =
+        arc.pool.find_conversation(query.id, auth.sub, /* include_messages */ true).await?;
backend/api/src/handlers/users/get_user_conversations.rs (1)

1-1: Name the type idiomatically and avoid exposing middleware path

Consider renaming Authdata to AuthData across the codebase (Rust style), or at least alias it here to keep handler signatures clean and decoupled from the middleware module path.

Possible local alias:

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::Authdata as AuthData, Arcadia};

and

-pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
+pub async fn exec(arc: web::Data<Arcadia>, user: AuthData) -> Result<HttpResponse> {
backend/api/src/handlers/edition_groups/create_edition_group.rs (4)

11-13: Document all supported auth mechanisms.

If API keys are also accepted (per PR discussion), add the corresponding security scheme to the endpoint.

     security(
-      ("http" = ["Bearer"])
+      ("http" = ["Bearer"]),
+      // Add if defined globally in the OpenAPI components
+      ("ApiKeyAuth" = [])
     ),

Please confirm the exact scheme name as defined in your utoipa components before applying.


1-1: Type naming and placement: prefer AuthData and a generic module.

Rename Authdata → AuthData (Rust type casing) and consider moving it out of jwt_middleware to a neutral auth module to reflect non-JWT sources (e.g., API keys).


21-22: Parameter naming and intent.

Rename user to auth (or auth_data) for clarity since it carries claims, not a loaded User.

-pub async fn exec(
-    form: web::Json<UserCreatedEditionGroup>,
-    arc: web::Data<Arcadia>,
-    user: Authdata,
-) -> Result<HttpResponse> {
+pub async fn exec(
+    form: web::Json<UserCreatedEditionGroup>,
+    arc: web::Data<Arcadia>,
+    auth: Authdata,
+) -> Result<HttpResponse> {
-    let edition_group = arc.pool.create_edition_group(&form, user.sub).await?;
+    let edition_group = arc.pool.create_edition_group(&form, auth.sub).await?;

1-1: Add Location header on 201 Created.

Return the URI of the newly created resource to improve client UX.

 use actix_web::{web, HttpResponse};
+use actix_web::http::header;

@@
-    Ok(HttpResponse::Created().json(edition_group))
+    let location = format!("/api/edition-groups/{}", edition_group.id);
+    Ok(HttpResponse::Created()
+        .append_header((header::LOCATION, location))
+        .json(edition_group))

Also applies to: 25-25

backend/api/src/handlers/series/create_series.rs (2)

1-1: Rust casing: rename Authdata → AuthData for type naming consistency

Matches Rust’s CamelCase type convention and improves readability. Coordinate with the type definition and imports project-wide.

Example for this file (after global rename):

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::AuthData, Arcadia};
@@
-    user: Authdata,
+    user: AuthData,

Also applies to: 21-21


19-19: Minor naming nit: rename serie to new_series or payload

“Series” is both singular and plural; serie is uncommon. A clearer name reduces confusion.

-pub async fn exec(
-    serie: web::Json<UserCreatedSeries>,
+pub async fn exec(
+    new_series: web::Json<UserCreatedSeries>,
@@
-    let series = arc.pool.create_series(&serie, user.sub).await?;
+    let series = arc.pool.create_series(&new_series, user.sub).await?;

Also applies to: 23-23

backend/api/src/handlers/users/get_me.rs (2)

40-58: Parallelize I/O-bound calls to reduce latency.

Peers, warnings, both searches, and unread counts can be fetched concurrently.

Example:

use futures::try_join;

let (peers, user_warnings) = try_join!(
    arc.pool.get_user_peers(current_user.id),
    arc.pool.find_user_warnings(current_user.id),
)?;

let uploaded = arc.pool.search_torrents(&uploaded_q, Some(current_user.id));
let snatched = arc.pool.search_torrents(&snatched_q, Some(current_user.id));
let (uploaded_torrents, snatched_torrents) = try_join!(uploaded, snatched)?;

let (unread_conv, unread_notif) = try_join!(
    arc.pool.find_unread_conversations_amount(current_user.id),
    arc.pool.find_unread_notifications_amount(current_user.id),
)?;

Also applies to: 60-66


25-25: Optional: return a typed DTO instead of serde_json::json!.

Typed responses help OpenAPI, prevent shape drift, and improve testability.

Example DTO (adjust field types as appropriate):

#[derive(serde::Serialize, utoipa::ToSchema)]
pub struct GetMeResponse {
    pub user: Profile,
    // Replace serde_json::Value with concrete types if available:
    pub peers: serde_json::Value,
    pub user_warnings: serde_json::Value,
    pub unread_conversations_amount: i64,
    pub unread_notifications_amount: i64,
    pub last_five_uploaded_torrents: serde_json::Value,
    pub last_five_snatched_torrents: serde_json::Value,
}
backend/api/src/handlers/title_groups/edit_title_group.rs (2)

4-4: Prefer idiomatic naming and a generic auth context alias

Alias the extractor to AuthData (CamelCase) and decouple callers from the jwt_middleware type name. This eases a future rename to a generic, non-JWT-specific context if API keys are also supported.

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::Authdata as AuthData, Arcadia};

22-23: Rename the parameter to ‘auth’ to avoid “user”-semantics confusion

This handler no longer receives a full User; naming it auth clarifies intent and reduces accidental usages that assume DB-backed fields.

-    user: Authdata,
+    auth: AuthData,

Also update the use-site:

-    if title_group.created_by_id == user.sub || user.class == "staff" {
+    if title_group.created_by_id == auth.sub || auth.class == "staff" {
backend/api/src/handlers/torrents/delete_torrent.rs (3)

4-4: Use Rust casing (AuthData) and decouple handler from middleware name.

Alias the import to PascalCase and use the alias in the signature. This keeps naming idiomatic and lets you later re-export AuthData from a stable module without touching handlers.

-use crate::{middlewares::jwt_middleware::Authdata, Arcadia};
+use crate::{middlewares::jwt_middleware::Authdata as AuthData, Arcadia};
@@
-    user: Authdata,
+    user: AuthData,

Also applies to: 23-23


28-28: Avoid full user fetch when only username is needed.

This endpoint still hits the DB solely to render the moderator’s name. Add a lightweight query (e.g., find_username_by_id) to reduce I/O and payload size.

-    let current_user = arc.pool.find_user_with_id(user.sub).await?;
+    let staff_username = arc.pool.find_username_by_id(user.sub).await?;
@@
-        current_user.username
+        staff_username

Also applies to: 42-42


21-21: Avoid mutating the extractor; operate on the inner payload.

Consume Json to a local payload, mutate, then pass a clear &payload to storage.

-    mut form: web::Json<TorrentToDelete>,
+    form: web::Json<TorrentToDelete>,
@@
-    form.displayed_reason = Some(displayed_reason);
-    arc.pool.remove_torrent(&form, user.sub).await?;
+    let mut payload = form.into_inner();
+    payload.displayed_reason = Some(displayed_reason);
+    arc.pool.remove_torrent(&payload, user.sub).await?;

Also applies to: 45-46

backend/api/src/handlers/affiliated_artists/remove_affiliated_artists.rs (3)

13-24: Document the query parameters and correct the success status in OpenAPI

Expose the query schema and align the documented status with the handler.

 #[utoipa::path(
     delete,
     operation_id = "Delete artist affiliation",
     tag = "Affiliated Artist",
     path = "/api/affiliated-artists",
     security(
       ("http" = ["Bearer"])
     ),
+    params(RemoveAffiliatedArtistsQuery),
     responses(
-        (status = 200, description = "Successfully removed the artist affiliations"),
+        (status = 204, description = "Successfully removed the artist affiliations"),
     )
 )]

26-27: Prefer stable extractor over labs: use web::Query

actix_web’s stable Query extractor is sufficient here.

-    query: actix_web_lab::extract::Query<RemoveAffiliatedArtistsQuery>,
+    query: web::Query<RemoveAffiliatedArtistsQuery>,

1-1: Type naming: consider AuthData instead of Authdata

Rust type names are conventionally in CamelCase with word boundaries. Renaming across the crate would improve readability.

backend/api/src/handlers/conversations/create_conversation.rs (1)

29-30: Return Location header for the created resource

Helps clients discover the new resource URI.

-    Ok(HttpResponse::Created().json(conversation))
+    Ok(
+        HttpResponse::Created()
+            .append_header(("Location", format!("/api/conversations/{}", conversation.id)))
+            .json(conversation),
+    )
backend/api/src/handlers/artists/create_artists.rs (3)

24-27: Avoid variable shadowing for clarity

Shadowing the input payload name makes the code harder to scan.

-    let artists = arc.pool.create_artists(&artists, user.sub).await?;
-
-    Ok(HttpResponse::Created().json(artists))
+    let created_artists = arc.pool.create_artists(&artists, user.sub).await?;
+
+    Ok(HttpResponse::Created().json(created_artists))

6-18: Document the request body in OpenAPI

Make the input type explicit for better codegen/dev UX.

 #[utoipa::path(
     post,
@@
-    responses(
+    request_body = Vec<UserCreatedArtist>,
+    responses(
         (status = 201, description = "Successfully created the artists, returned in the same order as the one sent.
             In the case of a db conflict (duplicate), the existing entry is returned (can be seen with the created_at attribute).", body=Vec<Artist>),
     )
 )]

1-1: Type naming: consider AuthData instead of Authdata

Consistent CamelCase word boundaries improve readability across the API surface.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9278eed and e9bb981.

📒 Files selected for processing (39)
  • backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (2 hunks)
  • backend/api/src/handlers/affiliated_artists/remove_affiliated_artists.rs (2 hunks)
  • backend/api/src/handlers/artists/create_artists.rs (2 hunks)
  • backend/api/src/handlers/conversations/create_conversation.rs (2 hunks)
  • backend/api/src/handlers/conversations/create_conversation_message.rs (2 hunks)
  • backend/api/src/handlers/conversations/get_conversation.rs (2 hunks)
  • backend/api/src/handlers/edition_groups/create_edition_group.rs (2 hunks)
  • backend/api/src/handlers/forum/create_forum_post.rs (2 hunks)
  • backend/api/src/handlers/forum/create_forum_thread.rs (2 hunks)
  • backend/api/src/handlers/gifts/create_gift.rs (2 hunks)
  • backend/api/src/handlers/invitations/create_invitation.rs (2 hunks)
  • backend/api/src/handlers/master_groups/create_master_group.rs (2 hunks)
  • backend/api/src/handlers/series/create_series.rs (2 hunks)
  • backend/api/src/handlers/subscriptions/create_subscription.rs (2 hunks)
  • backend/api/src/handlers/subscriptions/remove_subscription.rs (2 hunks)
  • backend/api/src/handlers/title_groups/create_title_group.rs (4 hunks)
  • backend/api/src/handlers/title_groups/create_title_group_comment.rs (2 hunks)
  • backend/api/src/handlers/title_groups/edit_title_group.rs (2 hunks)
  • backend/api/src/handlers/title_groups/get_title_group.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/create_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/create_torrent_request_vote.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/fill_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrent_requests/get_torrent_request.rs (2 hunks)
  • backend/api/src/handlers/torrents/create_torrent.rs (2 hunks)
  • backend/api/src/handlers/torrents/create_torrent_report.rs (2 hunks)
  • backend/api/src/handlers/torrents/delete_torrent.rs (3 hunks)
  • backend/api/src/handlers/torrents/download_dottorrent_file.rs (2 hunks)
  • backend/api/src/handlers/torrents/edit_torrent.rs (2 hunks)
  • backend/api/src/handlers/torrents/get_registered_torrents.rs (2 hunks)
  • backend/api/src/handlers/torrents/get_upload_information.rs (2 hunks)
  • backend/api/src/handlers/user_applications/update_user_application_status.rs (2 hunks)
  • backend/api/src/handlers/users/create_api_key.rs (2 hunks)
  • backend/api/src/handlers/users/edit_user.rs (2 hunks)
  • backend/api/src/handlers/users/get_me.rs (3 hunks)
  • backend/api/src/handlers/users/get_registered_users.rs (2 hunks)
  • backend/api/src/handlers/users/get_user.rs (3 hunks)
  • backend/api/src/handlers/users/get_user_conversations.rs (2 hunks)
  • backend/api/src/handlers/users/warn_user.rs (2 hunks)
  • backend/api/src/handlers/wiki/create_wiki_article.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (27)
  • backend/api/src/handlers/title_groups/create_title_group_comment.rs
  • backend/api/src/handlers/gifts/create_gift.rs
  • backend/api/src/handlers/conversations/create_conversation_message.rs
  • backend/api/src/handlers/master_groups/create_master_group.rs
  • backend/api/src/handlers/subscriptions/create_subscription.rs
  • backend/api/src/handlers/users/get_registered_users.rs
  • backend/api/src/handlers/forum/create_forum_thread.rs
  • backend/api/src/handlers/torrents/create_torrent_report.rs
  • backend/api/src/handlers/user_applications/update_user_application_status.rs
  • backend/api/src/handlers/users/edit_user.rs
  • backend/api/src/handlers/torrents/create_torrent.rs
  • backend/api/src/handlers/torrents/edit_torrent.rs
  • backend/api/src/handlers/wiki/create_wiki_article.rs
  • backend/api/src/handlers/users/get_user.rs
  • backend/api/src/handlers/torrent_requests/fill_torrent_request.rs
  • backend/api/src/handlers/torrents/get_upload_information.rs
  • backend/api/src/handlers/torrent_requests/create_torrent_request_vote.rs
  • backend/api/src/handlers/subscriptions/remove_subscription.rs
  • backend/api/src/handlers/title_groups/get_title_group.rs
  • backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs
  • backend/api/src/handlers/invitations/create_invitation.rs
  • backend/api/src/handlers/torrent_requests/get_torrent_request.rs
  • backend/api/src/handlers/torrents/download_dottorrent_file.rs
  • backend/api/src/handlers/torrents/get_registered_torrents.rs
  • backend/api/src/handlers/torrent_requests/create_torrent_request.rs
  • backend/api/src/handlers/users/warn_user.rs
  • backend/api/src/handlers/title_groups/create_title_group.rs
🧰 Additional context used
🧬 Code graph analysis (2)
backend/api/src/handlers/users/get_user_conversations.rs (3)
backend/api/src/handlers/affiliated_artists/create_affiliated_artists.rs (1)
  • exec (18-29)
backend/api/src/handlers/conversations/create_conversation.rs (1)
  • exec (18-30)
backend/api/src/handlers/conversations/get_conversation.rs (1)
  • exec (26-34)
backend/api/src/handlers/users/get_me.rs (3)
backend/api/src/handlers/artists/create_artists.rs (1)
  • exec (19-27)
backend/api/src/handlers/conversations/create_conversation.rs (1)
  • exec (18-30)
backend/api/src/handlers/gifts/create_gift.rs (1)
  • exec (18-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: schema-check
  • GitHub Check: build
  • GitHub Check: schema-check
  • GitHub Check: build
🔇 Additional comments (10)
backend/api/src/handlers/forum/create_forum_post.rs (1)

21-23: Authdata.sub is i64 and matches create_forum_post’s expected i64 parameter; no conversion required.

backend/api/src/handlers/users/create_api_key.rs (1)

24-24: Authdata.sub is already an i64 and matches the storage API; no parsing or conversion needed.

backend/api/src/handlers/conversations/get_conversation.rs (1)

29-31: Authdata.sub (i64) aligns with find_conversation’s current_user_id parameter (i64); no conversion needed.

backend/api/src/handlers/users/get_user_conversations.rs (1)

20-20: No action needed: Authdata.sub is correctly typed
Authdata.sub is defined as i64 in jwt_middleware.rs and find_user_conversations expects an i64, so the types already align.

backend/api/src/handlers/edition_groups/create_edition_group.rs (1)

23-23: Verify ID type and canonical mapping.

Ensure user.sub is the canonical DB user primary key type expected by create_edition_group (e.g., i64 vs Uuid). If not guaranteed across auth methods, add an explicit conversion/validation at the handler boundary.

backend/api/src/handlers/series/create_series.rs (1)

21-22: Direct Authdata extractor is valid
Authdata has an impl FromRequest in jwt_middleware.rs (line 18), so using it here will yield proper 401/403 on missing or invalid auth.

backend/api/src/handlers/users/get_me.rs (2)

1-1: Consider Rust naming convention for AuthData
The codebase consistently defines and imports the type as Authdata (e.g. in middlewares/jwt_middleware.rs and all handler modules). To follow CamelCase for multi-word names, rename the struct and all its references to AuthData. This is a wide-reaching change—confirm the desired canonical name and update every occurrence accordingly.


28-29: Incorrect error propagation suggestion
Both get_user_peers and find_user_warnings return Vec<…>, not Result<…, _>, so you can’t use ? here; any errors are handled (or panicked on) inside the repository layer. If you want to propagate database errors, change those methods to return Result<Vec<…>, sqlx::Error>.

Likely an incorrect or invalid review comment.

backend/api/src/handlers/torrents/delete_torrent.rs (1)

46-46: No action needed: AuthData.sub (i64) aligns with storage APIs. Verified that Authdata.sub, Claims.sub, find_user_with_id(id: i64), and remove_torrent(..., current_user_id: i64) all use i64.

backend/api/src/handlers/conversations/create_conversation.rs (1)

26-27: Types are consistent: Authdata.sub is i64 and create_conversation accepts an i64, so no changes are needed.

Comment on lines 19 to 25
pub async fn exec(
artists: web::Json<Vec<UserCreatedArtist>>,
arc: web::Data<Arcadia>,
current_user_id: UserId,
user: Authdata,
) -> Result<HttpResponse> {
let artists = arc.pool.create_artists(&artists, current_user_id.0).await?;
let artists = arc.pool.create_artists(&artists, user.sub).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add basic payload validation and upper bound

Prevent empty submissions and unbounded bulk requests.

 pub async fn exec(
     artists: web::Json<Vec<UserCreatedArtist>>,
     arc: web::Data<Arcadia>,
     user: Authdata,
 ) -> Result<HttpResponse> {
+    if artists.is_empty() {
+        return Ok(HttpResponse::BadRequest().json(json!({"error": "payload cannot be empty"})));
+    }
+    if artists.len() > 1000 {
+        return Ok(HttpResponse::PayloadTooLarge().json(json!({"error": "too many artists"})));
+    }
     let created_artists = arc.pool.create_artists(&artists, user.sub).await?;
 
     Ok(HttpResponse::Created().json(created_artists))
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/api/src/handlers/artists/create_artists.rs around lines 19 to 25, add
basic payload validation before calling arc.pool.create_artists: check that the
incoming artists list is not empty (return an HTTP 400 Bad Request with a clear
message if it is), enforce an upper bound (define a MAX_ARTISTS constant, e.g.
100, and return 400 if artists.len() > MAX_ARTISTS), and perform simple per-item
validation (e.g. required fields like name are non-empty) returning 400 on the
first invalid item; only call arc.pool.create_artists when validations pass.

Comment on lines +21 to 22
user: Authdata,
) -> Result<HttpResponse> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Param rename looks good; confirm extractor type used here.

If you adopt the AuthData rename, mirror it in the signature:

-    user: Authdata,
+    user: AuthData,

To verify types and usages, run:


🏁 Script executed:

#!/bin/bash
# Confirm Authdata/AuthData definition and `sub` field type
rg -nP -C3 'struct\s+Authdata\b|struct\s+AuthData\b'
rg -nP -C2 '\bsub\s*:\s*' | head

# Confirm storage API signature
rg -nP -C3 'fn\s+create_forum_post\s*\('

Length of output: 1811


Rename Authdata to AuthData in both definition and usage

  • backend/api/src/middlewares/jwt_middleware.rs:13 change pub struct Authdata → pub struct AuthData
  • backend/api/src/handlers/forum/create_forum_post.rs:21 update user: Authdata → user: AuthData
🤖 Prompt for AI Agents
In backend/api/src/middlewares/jwt_middleware.rs around line 13 and
backend/api/src/handlers/forum/create_forum_post.rs around lines 21-22, the
struct name `Authdata` should be renamed to `AuthData` where defined and
referenced; rename the pub struct declaration to `AuthData`, update any
derives/attributes if present, then update the handler signature from `user:
Authdata` to `user: AuthData` and fix any other references/imports across the
codebase (adjust use paths if needed) and run cargo check to ensure no remaining
references to the old name.

Comment on lines +23 to 25
let forum_post = arc.pool.create_forum_post(&forum_post, user.sub).await?;

Ok(HttpResponse::Created().json(forum_post))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Decouple storage from actix’s web::Json and avoid shadowing.

Pass the inner model to storage and use a distinct binding name.

-    let forum_post = arc.pool.create_forum_post(&forum_post, user.sub).await?;
-
-    Ok(HttpResponse::Created().json(forum_post))
+    let created = arc.pool.create_forum_post(&forum_post.into_inner(), user.sub).await?;
+    Ok(HttpResponse::Created().json(created))

If create_forum_post takes the value (not a reference), drop the &:

-    let created = arc.pool.create_forum_post(&forum_post.into_inner(), user.sub).await?;
+    let created = arc.pool.create_forum_post(forum_post.into_inner(), user.sub).await?;
📝 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
let forum_post = arc.pool.create_forum_post(&forum_post, user.sub).await?;
Ok(HttpResponse::Created().json(forum_post))
let created = arc.pool.create_forum_post(&forum_post.into_inner(), user.sub).await?;
Ok(HttpResponse::Created().json(created))
🤖 Prompt for AI Agents
In backend/api/src/handlers/forum/create_forum_post.rs around lines 23 to 25,
the handler currently passes the actix web::Json wrapper and reuses the same
name for the storage result; extract the inner model from the web::Json (e.g.,
let new_post = forum_post.into_inner();), pass that value to
arc.pool.create_forum_post (remove the & if the function takes ownership), bind
the storage result to a distinct name (e.g., created_post) and return that in
the HttpResponse::Created().json(created_post) to avoid shadowing and decouple
storage from actix types.

user: Authdata,
) -> Result<HttpResponse> {
let series = arc.pool.create_series(&serie, &current_user).await?;
let series = arc.pool.create_series(&serie, user.sub).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid “stringly-typed” IDs: pass a typed user_id instead of raw sub

user.sub is typically a string. Prefer a typed ID (e.g., Uuid/i64) at the middleware boundary and expose it as user.user_id. This prevents accidental schema drift and parse failures deep in storage.

Once you confirm the intended ID type, I can provide a precise patch. Example directions:

  • If Uuid: parse in middleware and expose Uuid in Authdata; then call create_series(&serie, user.user_id).
  • If i64: parse once in middleware; same call as above.

Comment on lines +19 to 22
pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
let conversations = arc.pool.find_user_conversations(user.sub).await?;

Ok(HttpResponse::Ok().json(json!({"conversations": conversations})))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

OpenAPI schema mismatch: handler returns a wrapper object but schema advertises raw ConversationsOverview

You return {"conversations": ...} but the utoipa response declares body=ConversationsOverview. This will generate incorrect API docs/clients.

Apply:

+#[derive(serde::Serialize, utoipa::ToSchema)]
+struct GetUserConversationsResponse {
+    conversations: ConversationsOverview,
+}
+
-pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
+pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
     let conversations = arc.pool.find_user_conversations(user.sub).await?;
-
-    Ok(HttpResponse::Ok().json(json!({"conversations": conversations})))
+    Ok(HttpResponse::Ok().json(GetUserConversationsResponse { conversations }))
 }

And outside the selected lines, update the docs and remove the now-unused import:

-    responses(
-        (status = 200, description = "Found the conversations and some of their metadata", body=ConversationsOverview),
-    )
+    responses(
+        (status = 200, description = "Found the conversations and some of their metadata", body=GetUserConversationsResponse),
+        (status = 401, description = "Unauthorized"),
+    )
-use serde_json::json;
📝 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
pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
let conversations = arc.pool.find_user_conversations(user.sub).await?;
Ok(HttpResponse::Ok().json(json!({"conversations": conversations})))
// Add at top of file, alongside other response types:
#[derive(serde::Serialize, utoipa::ToSchema)]
struct GetUserConversationsResponse {
conversations: ConversationsOverview,
}
pub async fn exec(arc: web::Data<Arcadia>, user: Authdata) -> Result<HttpResponse> {
let conversations = arc.pool.find_user_conversations(user.sub).await?;
Ok(HttpResponse::Ok().json(GetUserConversationsResponse { conversations }))
}
🤖 Prompt for AI Agents
In backend/api/src/handlers/users/get_user_conversations.rs around lines 19 to
22, the handler currently returns a wrapper object {"conversations":
conversations} which conflicts with the OpenAPI/utoipa declaration
body=ConversationsOverview; change the response to return the raw
ConversationsOverview value (i.e., respond with the conversations directly, not
wrapped), update any utoipa docs elsewhere to match this concrete return shape,
and remove any now-unused import(s) that were only needed for the wrapper.

@ppoliani ppoliani mentioned this pull request Aug 29, 2025
@ppoliani

Copy link
Copy Markdown
Contributor Author

The code at the moment has at least 2 calls on every request, one to check if use is banned and another to fetch the user from the db. Both queries are avoidable. With the two PRs we got it to 0 db requests while maintain security.

Invalidating a token (you can check the other PR) requires no interactions with the db. Instead we utilise an in memory cache (Redis) and a simple check which verifies if token is invalidated.


pub async fn find_user_id_with_api_key(&self, api_key: &str) -> Result<i64> {
let user_id = sqlx::query_scalar!(
pub async fn find_user_id_with_api_key(&self, api_key: &str) -> Result<User> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this should be renamed to find_user_with_api_key then ?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (2)
backend/storage/src/repositories/auth_repository.rs (2)

149-195: Retry loop reuses an aborted transaction after a unique violation

On a 23505, the transaction is left in an aborted state; continuing the loop with the same tx will fail with “current transaction is aborted.” Either start a new transaction per attempt or (simpler) drop the transaction entirely for this single-row insert.

Minimal fix: remove the transaction and retry on collision.

-        let mut tx = <ConnectionPool as Borrow<PgPool>>::borrow(self)
-            .begin()
-            .await?;
-
         loop {
-            let api_key: String = Alphanumeric.sample_string(&mut rng(), 40);
+            let candidate: String = Alphanumeric.sample_string(&mut rng(), 40);
 
-            let api_key = sqlx::query_as!(
+            let api_key = sqlx::query_as!(
                 APIKey,
                 r#"
                 INSERT INTO api_keys (name, value, user_id)
                 VALUES ($1, $2, $3)
                 RETURNING *
             "#,
                 created_api_key.name,
-                api_key,
+                candidate,
                 current_user_id
             )
-            .fetch_one(&mut *tx)
+            .fetch_one(self.borrow())
             .await;
 
             match api_key {
-                Ok(api_key) => {
-                    tx.commit().await?;
-
-                    return Ok(api_key);
-                }
+                Ok(api_key) => return Ok(api_key),
                 Err(api_key_error) => {
-                    return Err(match &api_key_error {
-                        sqlx::Error::Database(database_error) => {
-                            let code = database_error.code();
-                            // 23505 is the code for "unique violation", which means we didn't generate a unique API key
-                            if let Some(code) = code
-                                && code == "23505"
-                            {
-                                // Try again (jump to next iteration of loop)
-                                continue;
-                            }
-
-                            Error::CouldNotCreateAPIKey(api_key_error)
-                        }
-                        _ => Error::CouldNotCreateAPIKey(api_key_error),
-                    });
+                    if let sqlx::Error::Database(db_err) = &api_key_error {
+                        // 23505: unique violation – regenerate and retry
+                        if db_err.code().as_deref() == Some("23505") {
+                            continue;
+                        }
+                    }
+                    return Err(Error::CouldNotCreateAPIKey(api_key_error));
                 }
             }
         }

Optionally cap retries (e.g., 5) and surface a specific error if exhausted.


103-107: Avoid unwrap on parsed password hash

PasswordHash::new can fail on malformed hashes and currently panics. Map errors to WrongUsernameOrPassword.

-        let parsed_hash = PasswordHash::new(&user.password_hash);
-
-        Argon2::default()
-            .verify_password(login.password.as_bytes(), &parsed_hash.unwrap())
-            .map_err(|_| Error::WrongUsernameOrPassword)?;
+        let parsed_hash = PasswordHash::new(&user.password_hash)
+            .map_err(|_| Error::WrongUsernameOrPassword)?;
+
+        Argon2::default()
+            .verify_password(login.password.as_bytes(), &parsed_hash)
+            .map_err(|_| Error::WrongUsernameOrPassword)?;
♻️ Duplicate comments (1)
backend/storage/src/repositories/auth_repository.rs (1)

112-128: Rename and return type change look good

Renaming to find_user_with_api_key and returning User removes an extra round-trip (no follow-up find_user_with_id) and aligns with the prior naming feedback.

🧹 Nitpick comments (1)
backend/storage/src/repositories/auth_repository.rs (1)

116-121: Sanitize auth query to avoid leaking sensitive fields
Replace SELECT u.* with an explicit projection into a minimal auth-specific struct (e.g. AuthUser { id, class, username, … }) so that password_hash and other restricted fields don’t flow across layers.
The api_keys table has no revoked/active flag—if you require key revocation support, add a status column (e.g. revoked BOOLEAN) and include it in the WHERE clause.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e9bb981 and ecf8e18.

📒 Files selected for processing (4)
  • backend/api/.env.example (1 hunks)
  • backend/api/src/middlewares/jwt_middleware.rs (3 hunks)
  • backend/storage/.env.example (1 hunks)
  • backend/storage/src/repositories/auth_repository.rs (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • backend/api/.env.example
  • backend/storage/.env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/api/src/middlewares/jwt_middleware.rs
🧰 Additional context used
🧬 Code graph analysis (1)
backend/storage/src/repositories/auth_repository.rs (3)
backend/storage/src/repositories/title_group_repository.rs (1)
  • sqlx (36-36)
backend/storage/src/repositories/torrent_request_repository.rs (1)
  • sqlx (30-30)
backend/storage/src/repositories/torrent_repository.rs (1)
  • sqlx (102-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: schema-check
  • GitHub Check: build
  • GitHub Check: schema-check
  • GitHub Check: build

@FrenchGithubUser
FrenchGithubUser merged commit f72ecc3 into main Aug 31, 2025
9 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Nov 10, 2025
4 tasks
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.

Avoid reading user from db in every authenticated request

2 participants