Skip to content

feat: implement backend authentication foundation for small-team sharing#290

Open
Kishalll wants to merge 7 commits into
Abhash-Chakraborty:mainfrom
Kishalll:backend-owned-auth
Open

feat: implement backend authentication foundation for small-team sharing#290
Kishalll wants to merge 7 commits into
Abhash-Chakraborty:mainfrom
Kishalll:backend-owned-auth

Conversation

@Kishalll
Copy link
Copy Markdown
Contributor

@Kishalll Kishalll commented May 29, 2026

Summary

This implements the backend foundation for small-team instance sharing as detailed in the design docs. By default, a fresh install remains a frictionless, local, single-user instance. However, if a user calls /api/auth/setup to create an admin account, the instance switches to "shared mode" and requires authentication for protected endpoints.

It introduces the User, AuthSession, InviteToken, and JoinRequest models alongside secure password hashing (bcrypt) and opaque session tokens. Media uploads are also now tagged with the uploader_user_id when authenticated.

Fixes #262

Type of change

  • Bug fix
  • Feature
  • Documentation update
  • Refactor
  • CI / tooling

What changed

  • Data Layer: Added User, AuthSession, InviteToken, and JoinRequest SQLAlchemy models. Modified Media to track the uploader.
  • Security: Implemented bcrypt password hashing and token generation in core/auth.py. Added FastAPI dependencies (get_optional_user, get_required_user, get_admin_user) for robust access control.
  • Routing: Created /api/auth endpoints for setup, login, logout, generating invites, submitting join requests, and admin approvals. Wired the auth dependency into routers/upload.py.
  • Testing: Added 23 comprehensive tests in tests/test_auth.py covering all new flows.

Screenshots / recordings (for UI changes)

N/A - This is a backend-only foundation PR. No frontend changes have been made yet (the UI to interact with these endpoints will be a separate issue).

How to test

The complete backend test suite guarantees these workflows. To verify manually, run the local backend server and follow this flow using curl.

1. Create the initial admin account (triggers Shared Mode)

curl -s -X POST http://localhost:8000/api/auth/setup \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "mysecurepassword", "display_name": "Server Admin"}'

Expected Output:

{
  "user": {"id": 1, "username": "admin", "display_name": "Server Admin", "role": "admin"},
  "token": "U_5x1Z9_...",
  "expires_at": "2026-05-30T10:00:00Z"
}

2. Generate a single-use invite code (Admin only) (Replace with the token from Step 1)*

curl -s -X POST http://localhost:8000/api/auth/invites \
  -H "Authorization: Bearer <TOKEN>"

Expected Output:

{
  "id": 1,
  "invite_token": "j9P_k2...",
  "expires_at": "2026-05-31T10:00:00Z"
}

3. Submit a Join Request as a new user (Replace with the invite_token from Step 2)

curl -s -X POST http://localhost:8000/api/auth/join \
  -H "Content-Type: application/json" \
  -d '{"invite_token": "<INVITE>", "username": "newuser", "password": "userpass123", "display_name": "New Friend"}'

Expected Output:

{
  "join_request_id": 1,
  "status": "pending"
}

4. Approve the join request (Admin only)

curl -s -X POST http://localhost:8000/api/auth/join-requests/1/approve \
  -H "Authorization: Bearer <TOKEN>"

Expected Output:

{
  "user": {"id": 2, "username": "newuser", "display_name": "New Friend", "role": "member"}
}

Checklist

  • I linked the related issue
  • I ran required checks from CONTRIBUTING.md
  • I updated docs/env notes if needed
  • My PR is scoped to a single issue
  • I followed commit message conventions
  • I am not committing secrets or local artifacts

GSSoC'26 checklist

  • I requested issue assignment before starting
  • I have meaningful commits (no spam commits)
  • I am ready to explain my implementation in review comments

Summary by CodeRabbit

  • New Features

    • Instance setup with admin creation to enable shared mode
    • User authentication (login/logout) with expiring session tokens
    • Role-based access (admin/member) and admin-only invite & join-approval workflows
    • Invite-based onboarding and upload attribution for authenticated users
    • Configurable session and invite lifetimes
  • Tests

    • Comprehensive auth and invite/join workflow test suite validating security and behavior
  • Documentation

    • Small-team authentication plan status updated to In progress

Copilot AI review requested due to automatic review settings May 29, 2026 05:24
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 29, 2026

PR Context Summary

Suggested issue links

  • No strong issue match found yet.

Use Fixes #123 or Closes #123 in the PR body when one of the suggestions is the intended issue.
Manual rerun: Actions > PR Context Triage > Run workflow > set pr_number and force_review=true.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbc2c27e-72d1-48f1-8591-ba16fa67dab6

📥 Commits

Reviewing files that changed from the base of the PR and between e9fc85a and d1703ab.

📒 Files selected for processing (2)
  • .env.example
  • backend/src/find_api/core/config.py
✅ Files skipped from review due to trivial changes (1)
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/find_api/core/config.py

📝 Walkthrough

Walkthrough

Implements backend small-team authentication: user/session/invite models, bcrypt+SHA‑256 utilities, session token creation/validation, FastAPI auth endpoints and dependencies, DB wiring for uploader attribution, tests for full flows, and documentation status update.

Changes

Small-Team Authentication System

Layer / File(s) Summary
Configuration Settings and Core Data Models
.env.example, backend/src/find_api/core/config.py, backend/src/find_api/models/user.py, backend/src/find_api/models/session.py, backend/src/find_api/models/invite.py, backend/src/find_api/models/join_request.py, backend/src/find_api/models/media.py, backend/src/find_api/models/__init__.py
Adds SESSION_TTL_HOURS and INVITE_TTL_HOURS (plus .env examples); introduces User, AuthSession, InviteToken, and JoinRequest models; adds Media.uploader_user_id; exports new models.
Database Initialization and Model Exports
backend/src/find_api/core/database.py
Registers new models in init_db() and adds PostgreSQL normalization to create media.uploader_user_id column, index, and foreign key with IF NOT EXISTS guards.
Authentication Utilities and Session Management
backend/src/find_api/core/auth.py
SHA‑256 pre-hash + bcrypt password hashing/verification; token SHA‑256 hashing; session/invite token generators returning raw+hash; shared-mode detection; bearer-token resolution to active User; create_session with TTL-derived expiry.
FastAPI Dependency Injection for Auth
backend/src/find_api/core/dependencies.py
get_optional_user, get_required_user, and get_admin_user dependency helpers that conditionally raise 401/403 based on shared/local mode and user role.
Authentication Endpoints and Router
backend/src/find_api/routers/auth.py
Router with one-time admin POST /auth/setup, rate-limited POST /auth/login, POST /auth/logout, GET /auth/me; admin-only invite creation/listing; POST /auth/join join-request submission and listing; admin approve/reject join-request endpoints and related state transitions.
Upload Router Integration with User Attribution
backend/src/find_api/routers/upload.py
Thread optional get_optional_user into /upload and /upload/bulk; _ingest_image accepts uploader_user_id and conditionally persists it on Media records.
App Router Mounting
backend/src/find_api/main.py
Mounts auth.router under /api with auth tag.
Testing: Setup Fixtures and Helpers
backend/tests/conftest.py
Adds TEST_PASSWORD, resets auth rate limiter per test, and adds admin_setup fixture to create admin via API.
Testing: Integration Test Suite
backend/tests/test_auth.py
Comprehensive tests covering setup, login/logout, /me mode behavior, invite single-use semantics, join request submission and admin approval/rejection, upload attribution, and security properties (password hashing, bcrypt length handling, token hashing in DB).
Documentation Status Update
docs/plans/not-started/small-team-authentication.md
Updates plan status to In Progress, records implemented backend auth components, and notes remaining frontend/deletion/audit work.

Sequence Diagram(s)

sequenceDiagram
  participant ComponentA
  participant ComponentB
  ComponentA->>ComponentB: observable interaction
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

type:security, type:testing, quality:clean

Suggested reviewers

  • Abhash-Chakraborty

Poem

🐰 I hopped through files and stitched a key,
Hashing secrets safely with bcrypt and glee.
Invites bloom once, join requests wait,
Admins approve — new members celebrate.
Uploads remember who brought each piece of art.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: implementing backend authentication for small-team sharing, which aligns with the core objective.
Description check ✅ Passed The PR description comprehensively covers all required sections: summary of changes, type of change (feature), detailed what-changed breakdown, test documentation, and completed checklists for both standard and GSSoC'26 requirements.
Linked Issues check ✅ Passed All coding requirements from issue #262 are met: User/AuthSession/InviteToken/JoinRequest models added, bcrypt password hashing implemented, token-based auth with session management, setup/login/logout/invite/join endpoints created, admin approvals implemented, uploader tracking added, and comprehensive test coverage provided.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #262 objectives: user models, auth core utilities, dependencies, routers, database initialization, configuration, and comprehensive tests. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 89.39% which is sufficient. The required threshold is 80.00%.

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


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

❤️ Share

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

@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented May 29, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Username Password ec92d8b backend/tests/test_auth.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 29, 2026

Approvability

Verdict: Needs human review

This PR implements a complete authentication system (users, sessions, invites, join requests) which falls under security/auth changes requiring human review. Multiple unresolved comments raise security concerns including missing rate limiting on login, CodeQL alerts about password hashing, and bcrypt 72-byte limit issues.

No code changes detected at 1cc0e64. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements the backend foundation for “small-team instance sharing” authentication, including admin setup, token-based sessions, invite + join-request flows, and uploader attribution on uploads.

Changes:

  • Added auth core helpers, FastAPI dependencies, router endpoints, and persistence models for users/sessions/invites/join requests.
  • Updated upload endpoints to optionally record uploader_user_id on Media rows.
  • Added a comprehensive backend test suite for the new authentication flows and updated planning docs status.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
docs/plans/not-started/small-team-authentication.md Updates plan status and current implementation notes to reflect backend progress.
backend/tests/test_auth.py Adds end-to-end tests for setup/login/logout/invites/join requests/permissions and security properties.
backend/tests/conftest.py Adds an admin_setup fixture to bootstrap an admin account in tests.
backend/src/find_api/routers/upload.py Records uploader (when authenticated) while keeping upload working unauthenticated.
backend/src/find_api/routers/auth.py Adds auth API endpoints: setup, login/logout, invites, join requests, approvals/rejections, and /me.
backend/src/find_api/models/user.py Introduces User model for shared-mode accounts.
backend/src/find_api/models/session.py Introduces hashed-token AuthSession model for server-side sessions.
backend/src/find_api/models/media.py Adds uploader_user_id FK column for uploader attribution.
backend/src/find_api/models/join_request.py Introduces JoinRequest model for approval flow.
backend/src/find_api/models/invite.py Introduces single-use InviteToken model (hash stored, raw returned once).
backend/src/find_api/models/init.py Exports new auth-related models.
backend/src/find_api/main.py Registers the new auth router under /api.
backend/src/find_api/core/dependencies.py Adds reusable dependencies for optional/required/admin auth behavior.
backend/src/find_api/core/database.py Ensures auth models are registered and adds uploader column/index for existing DBs.
backend/src/find_api/core/config.py Adds configurable session/invite TTL settings.
backend/src/find_api/core/auth.py Implements password hashing, token hashing, session creation, and shared-mode detection.
.env.example Documents optional env vars for auth TTL configuration.

Comment on lines +108 to +118
@router.post("/auth/login")
def login(
request: Request,
body: LoginRequest,
db: Session = Depends(get_db),
):
"""Authenticate with username and password.

Returns a bearer token on success. Rate-limited to discourage
brute-force attempts.
"""
Comment on lines +170 to +175
@router.post("/auth/invites")
def create_invite(
body: InviteCreateRequest = InviteCreateRequest(),
admin: User = Depends(get_admin_user),
db: Session = Depends(get_db),
):
Comment on lines +155 to +158
@router.get("/auth/me")
def get_me(
user: User = Depends(get_required_user),
):
Comment on lines +176 to +183
"""Generate a single-use invite token.

Admin only. The raw token is returned once and never stored.
"""
if admin is None:
raise HTTPException(403, "Admin access required")

ttl = body.ttl_hours or settings.INVITE_TTL_HOURS
Comment on lines +122 to +133
conn.execute(
text(
"ALTER TABLE IF EXISTS media "
"ADD COLUMN IF NOT EXISTS uploader_user_id INTEGER"
)
)
conn.execute(
text(
"CREATE INDEX IF NOT EXISTS ix_media_uploader_user_id "
"ON media (uploader_user_id)"
)
)
Comment thread backend/tests/test_auth.py Outdated
Comment on lines +204 to +207
join_resp = _join(client, inv_token, username="member1")
req_id = join_resp.json()["join_request_id"]

client.post(
Comment thread backend/tests/test_auth.py Outdated
Comment on lines +417 to +418
# bcrypt hashes always start with $2b$
assert admin.password_hash.startswith("$2b$")
Comment thread backend/tests/conftest.py
Comment on lines +126 to +138
@pytest.fixture()
def admin_setup(client):
"""Set up an admin account and return (client, token, user_data)."""
resp = client.post(
"/api/auth/setup",
json={
"username": "admin",
"password": "testpass123",
"display_name": "Test Admin",
},
)
data = resp.json()
return client, data["token"], data["user"]
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12be95d345

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +108 to +113
@router.post("/auth/login")
def login(
request: Request,
body: LoginRequest,
db: Session = Depends(get_db),
):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the documented login rate limit

For shared-mode instances reachable on a LAN or reverse proxy, this login handler is the only password gate, but it has no @limiter.limit (unlike the vault unlock endpoint) or other throttling despite the docstring saying it is rate-limited. An attacker can make unlimited bcrypt guesses against /api/auth/login; add the same SlowAPI protection used elsewhere or remove the misleading assumption.

Useful? React with 👍 / 👎.


class SetupRequest(BaseModel):
username: str = Field(..., min_length=1, max_length=150)
password: str = Field(..., min_length=8, max_length=128)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject or prehash passwords beyond bcrypt's limit

This schema accepts passwords up to 128 characters while hash_password() sends the raw UTF-8 bytes directly to bcrypt, which ignores bytes after its 72-byte input limit. If a user chooses a long admin or join password, different passwords that share the first 72 bytes will verify as the same password, so either cap by byte length or prehash before bcrypt.

Useful? React with 👍 / 👎.

@Kishalll Kishalll force-pushed the backend-owned-auth branch from 12be95d to 99371b5 Compare May 29, 2026 05:27
Comment thread backend/src/find_api/routers/auth.py
Comment thread backend/src/find_api/core/config.py Outdated


def _bcrypt_input(plain: str) -> bytes:
return hashlib.sha256(plain.encode("utf-8")).hexdigest().encode("ascii")
@Kishalll
Copy link
Copy Markdown
Contributor Author

The current code does not store plain SHA-256 as the password hash,it prehashes the password and then passes that digest into bcrypt to avoid bcrypt’s 72-byte input truncation
The secrets detected by GitGuardian is only the username and pass explicitly mentioned in the test file

@Abhash-Chakraborty Abhash-Chakraborty added gssoc26 Related to GirlScript Summer of Code 2026. level:critical GSSoC difficulty level: critical. Base contributor points: 80. backend FastAPI, database, storage, and API work api API contract, endpoint behavior, and response shape architecture High-level design decisions and technical direction privacy Data privacy, security boundaries, and user trust local-first Privacy-preserving local runtime and offline behavior type:feature Feature PR. GSSoC type bonus. under-review Maintainer needs to verify do-not-merge Blocks merging. Remove this label only when the PR is fully ready for production. labels May 29, 2026
Comment on lines 1 to +5
# Small-Team Authentication (Instance Sharing)

**Status:** Not started
**Last reviewed:** 2026-05-29
**Current implementation status:** No user model, authentication middleware, invite flow, instance-management UI, or upload ownership/deletion-request workflow is implemented in the current codebase.
**Status:** In progress — backend auth foundation implemented
**Last reviewed:** 2026-05-30
**Current implementation status:** Backend auth foundation implemented (user model, session tokens, invite flow, join requests, admin approval, upload ownership tracking). Frontend UI, deletion-request workflow, and audit log remain unimplemented.
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.

🟢 Low not-started/small-team-authentication.md:1

The status header was updated to "In progress — backend auth foundation implemented" but the file remains at docs/plans/not-started/small-team-authentication.md. The repository uses a directory-based status convention (not-started/, partial/, complete/) where file location indicates status. Leaving this file in not-started/ contradicts the stated status and will mislead contributors scanning the directory structure. Consider moving the file to docs/plans/partial/small-team-authentication.md to align with the new status.

-**Status:** In progress — backend auth foundation implemented  
+**Status:** In progress — backend auth foundation implemented  
+
+> Note: This file should be moved from `docs/plans/not-started/` to `docs/plans/partial/` to match the new status.
🤖 Copy this AI Prompt to have your agent fix this:
In file docs/plans/not-started/small-team-authentication.md around lines 1-5:

The status header was updated to `"In progress — backend auth foundation implemented"` but the file remains at `docs/plans/not-started/small-team-authentication.md`. The repository uses a directory-based status convention (`not-started/`, `partial/`, `complete/`) where file location indicates status. Leaving this file in `not-started/` contradicts the stated status and will mislead contributors scanning the directory structure. Consider moving the file to `docs/plans/partial/small-team-authentication.md` to align with the new status.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
backend/src/find_api/routers/auth.py (1)

280-305: 💤 Low value

Potential race window between username check and commit.

Between lines 281-293 (username availability check) and line 305 (commit), another concurrent request could create a user with the same username. If the User table has a unique constraint on username, an IntegrityError would bubble up as a 500 error instead of a clean 409.

Consider catching IntegrityError around the commit and returning a 409 for username conflicts, similar to the pattern used in setup_instance.

Proposed fix
     db.add(join_req)
-    db.commit()
+    try:
+        db.commit()
+    except IntegrityError:
+        db.rollback()
+        raise HTTPException(409, "Username conflict occurred")
     db.refresh(join_req)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/find_api/routers/auth.py` around lines 280 - 305, There is a race
between checking username availability and db.commit causing IntegrityError;
wrap the transaction around creating JoinRequest and marking invite.is_used (the
code that sets invite.is_used = True, creates JoinRequest, db.add(join_req) and
db.commit()) in a try/except that catches sqlalchemy.exc.IntegrityError, calls
db.rollback(), and raises HTTPException(409, "Username is already taken") (or a
similar conflict message) so username unique-constraint failures are translated
to 409 instead of 500.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/find_api/models/join_request.py`:
- Around line 36-37: Remove the username from the JoinRequest model's string
representation: update the JoinRequest.__repr__ implementation so it no longer
references self.username and instead returns a string containing only non-PII
fields (e.g., id and status). Edit the __repr__ method in join_request.py (class
JoinRequest, method __repr__) to exclude username and ensure the returned
f-string uses only self.id and self.status; update any tests that assert the
previous repr if present.

In `@backend/src/find_api/routers/upload.py`:
- Around line 215-216: The duplicate-upload path currently returns early and
never updates an existing Media row's uploader_user_id; modify the
duplicate-handling branches that check for an existing Media by hash (the early
return around the block referencing uploader_user_id) so that if an existing
Media is found and uploader_user_id is provided and
existing_media.uploader_user_id is null/None, you set
existing_media.uploader_user_id = uploader_user_id and persist/save/update the
Media before returning; apply the same backfill logic to the other duplicate
branch handling (the block around lines 256-268) so duplicates get attributed to
the authenticated uploader when appropriate.

In `@backend/tests/test_auth.py`:
- Around line 130-141: Both tests currently only assert status_code == 401,
which can miss user-enumeration leaks in the response body; update
test_login_rejects_bad_password and test_login_rejects_unknown_user to capture
the response payloads returned by _login (e.g., resp.json() or resp.get_data())
for both scenarios using WRONG_PASSWORD and username="ghost" and assert that the
two payloads are exactly equal (and optionally assert they contain only a
generic error field) so that the error bodies for bad password and unknown user
cannot be distinguished.
- Around line 114-127: Add a regression check in test_login_returns_token to
assert User.last_login is updated and persisted: before calling _login (or
during _setup_admin) read the admin user's current last_login (via the User
model or your user lookup helper), perform the login with _login, then reload
the user from the database (e.g., fresh query on User by username) and assert
the new User.last_login is not None and is newer than the previous value (or
within a recent time window) to ensure the field was updated and saved.
- Around line 341-359: The test test_reject_marks_rejected currently only checks
the response message but not persistence; update it to verify the join request
record is actually set to "rejected" after calling the handler by retrieving the
stored join request (either via the API GET endpoint for join requests or
directly via the join request model/DB access used in tests) and asserting its
status field equals "rejected" for the id returned by _join (join_request_id);
keep the existing calls to _setup_admin, _create_invite, _join and the POST to
/api/auth/join-requests/{req_id}/reject but add a follow-up retrieval/assertion
on the persisted join request record.

---

Nitpick comments:
In `@backend/src/find_api/routers/auth.py`:
- Around line 280-305: There is a race between checking username availability
and db.commit causing IntegrityError; wrap the transaction around creating
JoinRequest and marking invite.is_used (the code that sets invite.is_used =
True, creates JoinRequest, db.add(join_req) and db.commit()) in a try/except
that catches sqlalchemy.exc.IntegrityError, calls db.rollback(), and raises
HTTPException(409, "Username is already taken") (or a similar conflict message)
so username unique-constraint failures are translated to 409 instead of 500.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24d57dfe-aa4f-459f-8e03-b593895acb8c

📥 Commits

Reviewing files that changed from the base of the PR and between 44a7d3a and e9fc85a.

📒 Files selected for processing (17)
  • .env.example
  • backend/src/find_api/core/auth.py
  • backend/src/find_api/core/config.py
  • backend/src/find_api/core/database.py
  • backend/src/find_api/core/dependencies.py
  • backend/src/find_api/main.py
  • backend/src/find_api/models/__init__.py
  • backend/src/find_api/models/invite.py
  • backend/src/find_api/models/join_request.py
  • backend/src/find_api/models/media.py
  • backend/src/find_api/models/session.py
  • backend/src/find_api/models/user.py
  • backend/src/find_api/routers/auth.py
  • backend/src/find_api/routers/upload.py
  • backend/tests/conftest.py
  • backend/tests/test_auth.py
  • docs/plans/not-started/small-team-authentication.md

Comment on lines +36 to +37
def __repr__(self):
return f"<JoinRequest(id={self.id}, username={self.username}, status={self.status})>"
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the username from JoinRequest.__repr__.

Line 37 includes a user identifier in the model repr. These objects often end up in debug logs and exception traces, so this adds an avoidable PII leak path in the auth flow.

Proposed fix
     def __repr__(self):
-        return f"<JoinRequest(id={self.id}, username={self.username}, status={self.status})>"
+        return f"<JoinRequest(id={self.id}, status={self.status})>"
📝 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
def __repr__(self):
return f"<JoinRequest(id={self.id}, username={self.username}, status={self.status})>"
def __repr__(self):
return f"<JoinRequest(id={self.id}, status={self.status})>"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/find_api/models/join_request.py` around lines 36 - 37, Remove the
username from the JoinRequest model's string representation: update the
JoinRequest.__repr__ implementation so it no longer references self.username and
instead returns a string containing only non-PII fields (e.g., id and status).
Edit the __repr__ method in join_request.py (class JoinRequest, method __repr__)
to exclude username and ensure the returned f-string uses only self.id and
self.status; update any tests that assert the previous repr if present.

Comment on lines +215 to 216
uploader_user_id: Optional[int] = None,
) -> dict:
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Backfill uploader_user_id on duplicate uploads.

This only sets uploader_user_id when a new Media row is created. If an authenticated user re-uploads a file whose hash already exists, the early return on Line 245 skips the new attribution entirely, so media imported before shared mode—or any existing row with a null uploader—stays permanently unattributed.

Suggested fix
     existing = db.query(Media).filter(Media.file_hash == file_hash).first()
     if existing:
+        if uploader_user_id is not None and existing.uploader_user_id is None:
+            existing.uploader_user_id = uploader_user_id
+            db.commit()
         logger.info(f"File {filename} already exists (hash: {file_hash})")
         return {"filename": filename, "status": "duplicate", "media_id": existing.id}

Also applies to: 256-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/find_api/routers/upload.py` around lines 215 - 216, The
duplicate-upload path currently returns early and never updates an existing
Media row's uploader_user_id; modify the duplicate-handling branches that check
for an existing Media by hash (the early return around the block referencing
uploader_user_id) so that if an existing Media is found and uploader_user_id is
provided and existing_media.uploader_user_id is null/None, you set
existing_media.uploader_user_id = uploader_user_id and persist/save/update the
Media before returning; apply the same backfill logic to the other duplicate
branch handling (the block around lines 256-268) so duplicates get attributed to
the authenticated uploader when appropriate.

Comment on lines +114 to +127
def test_login_returns_token(client):
"""After setup, the admin should be able to log in and use the token."""
_setup_admin(client)
resp = _login(client)
assert resp.status_code == 200

data = resp.json()
token = data["token"]
assert data["user"]["username"] == "admin"

# The token should work against /me
me_resp = client.get("/api/auth/me", headers=_auth_header(token))
assert me_resp.status_code == 200
assert me_resp.json()["user"]["username"] == "admin"
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a regression check for last_login persistence.

A successful login is part of the new storage behavior, but this test only verifies token issuance and /me. If User.last_login stops updating, the suite still passes.

Possible test addition
 def test_login_returns_token(client):
     """After setup, the admin should be able to log in and use the token."""
     _setup_admin(client)
     resp = _login(client)
     assert resp.status_code == 200

     data = resp.json()
     token = data["token"]
     assert data["user"]["username"] == "admin"

     # The token should work against /me
     me_resp = client.get("/api/auth/me", headers=_auth_header(token))
     assert me_resp.status_code == 200
     assert me_resp.json()["user"]["username"] == "admin"
+
+    admin = client.app.dependency_overrides  # replace with the existing DB fixture/session
+    # Re-query the user here and assert last_login was populated.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_auth.py` around lines 114 - 127, Add a regression check in
test_login_returns_token to assert User.last_login is updated and persisted:
before calling _login (or during _setup_admin) read the admin user's current
last_login (via the User model or your user lookup helper), perform the login
with _login, then reload the user from the database (e.g., fresh query on User
by username) and assert the new User.last_login is not None and is newer than
the previous value (or within a recent time window) to ensure the field was
updated and saved.

Comment on lines +130 to +141
def test_login_rejects_bad_password(client):
"""Wrong password → 401, nothing else leaked."""
_setup_admin(client)
resp = _login(client, password=WRONG_PASSWORD)
assert resp.status_code == 401


def test_login_rejects_unknown_user(client):
"""Non-existent username → same 401 as bad password (no user enumeration)."""
_setup_admin(client)
resp = _login(client, username="ghost", password=WRONG_PASSWORD)
assert resp.status_code == 401
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert identical failure payloads for both invalid-login paths.

These tests only check 401, so an enumeration leak in the response body would still slip through. Compare the error payloads from “bad password” and “unknown user” explicitly.

🧰 Tools
🪛 GitHub Check: GitGuardian Security Checks

[error] 136-136: GitGuardian detected a hardcoded secret of type "Username Password" (GitGuardian id: Generics/UsernamePassword). Commit ec92d8b. Remediate by removing the credential from code, rotating/revoking it, and optionally rewriting git history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_auth.py` around lines 130 - 141, Both tests currently only
assert status_code == 401, which can miss user-enumeration leaks in the response
body; update test_login_rejects_bad_password and test_login_rejects_unknown_user
to capture the response payloads returned by _login (e.g., resp.json() or
resp.get_data()) for both scenarios using WRONG_PASSWORD and username="ghost"
and assert that the two payloads are exactly equal (and optionally assert they
contain only a generic error field) so that the error bodies for bad password
and unknown user cannot be distinguished.

Comment on lines +341 to +359
def test_reject_marks_rejected(client):
"""Rejecting a join request should set its status to 'rejected'."""
setup = _setup_admin(client)
admin_token = setup.json()["token"]

invite_resp = _create_invite(client, admin_token)
assert invite_resp.status_code == 200
inv_token = invite_resp.json()["invite_token"]

join_resp = _join(client, inv_token, username="eve")
assert join_resp.status_code == 200
req_id = join_resp.json()["join_request_id"]

resp = client.post(
f"/api/auth/join-requests/{req_id}/reject",
headers=_auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["message"] == "Join request rejected"
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Verify the join request is actually persisted as rejected.

This test name/docstring says it checks status mutation, but it only asserts the response message. A handler that returns the message without updating storage would still pass.

Possible test addition
+from find_api.models.join_request import JoinRequest
+
 def test_reject_marks_rejected(client):
     """Rejecting a join request should set its status to 'rejected'."""
     setup = _setup_admin(client)
     admin_token = setup.json()["token"]
@@
     resp = client.post(
         f"/api/auth/join-requests/{req_id}/reject",
         headers=_auth_header(admin_token),
     )
     assert resp.status_code == 200
     assert resp.json()["message"] == "Join request rejected"
+
+    rejected = db.query(JoinRequest).filter(JoinRequest.id == req_id).one()
+    assert rejected.status == "rejected"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_auth.py` around lines 341 - 359, The test
test_reject_marks_rejected currently only checks the response message but not
persistence; update it to verify the join request record is actually set to
"rejected" after calling the handler by retrieving the stored join request
(either via the API GET endpoint for join requests or directly via the join
request model/DB access used in tests) and asserting its status field equals
"rejected" for the id returned by _join (join_request_id); keep the existing
calls to _setup_admin, _create_invite, _join and the POST to
/api/auth/join-requests/{req_id}/reject but add a follow-up retrieval/assertion
on the persisted join request record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api API contract, endpoint behavior, and response shape architecture High-level design decisions and technical direction backend FastAPI, database, storage, and API work do-not-merge Blocks merging. Remove this label only when the PR is fully ready for production. gssoc26 Related to GirlScript Summer of Code 2026. level:critical GSSoC difficulty level: critical. Base contributor points: 80. local-first Privacy-preserving local runtime and offline behavior privacy Data privacy, security boundaries, and user trust type:feature Feature PR. GSSoC type bonus. under-review Maintainer needs to verify

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add backend-owned authentication for small-team instance sharing

4 participants