feat: implement backend authentication foundation for small-team sharing#290
feat: implement backend authentication foundation for small-team sharing#290Kishalll wants to merge 7 commits into
Conversation
PR Context Summary
Suggested issue links
Use |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughImplements 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. ChangesSmall-Team Authentication System
Sequence Diagram(s)sequenceDiagram
participant ComponentA
participant ComponentB
ComponentA->>ComponentB: observable interaction
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ 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. Comment |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| - | - | Username Password | ec92d8b | backend/tests/test_auth.py | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- 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
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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.
ApprovabilityVerdict: 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 You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
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_idonMediarows. - 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. |
| @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. | ||
| """ |
| @router.post("/auth/invites") | ||
| def create_invite( | ||
| body: InviteCreateRequest = InviteCreateRequest(), | ||
| admin: User = Depends(get_admin_user), | ||
| db: Session = Depends(get_db), | ||
| ): |
| @router.get("/auth/me") | ||
| def get_me( | ||
| user: User = Depends(get_required_user), | ||
| ): |
| """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 |
| 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)" | ||
| ) | ||
| ) |
| join_resp = _join(client, inv_token, username="member1") | ||
| req_id = join_resp.json()["join_request_id"] | ||
|
|
||
| client.post( |
| # bcrypt hashes always start with $2b$ | ||
| assert admin.password_hash.startswith("$2b$") |
| @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"] |
There was a problem hiding this comment.
💡 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".
| @router.post("/auth/login") | ||
| def login( | ||
| request: Request, | ||
| body: LoginRequest, | ||
| db: Session = Depends(get_db), | ||
| ): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.
12be95d to
99371b5
Compare
|
|
||
|
|
||
| def _bcrypt_input(plain: str) -> bytes: | ||
| return hashlib.sha256(plain.encode("utf-8")).hexdigest().encode("ascii") |
|
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 |
| # 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. |
There was a problem hiding this comment.
🟢 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.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
backend/src/find_api/routers/auth.py (1)
280-305: 💤 Low valuePotential 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
IntegrityErroraround the commit and returning a 409 for username conflicts, similar to the pattern used insetup_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
📒 Files selected for processing (17)
.env.examplebackend/src/find_api/core/auth.pybackend/src/find_api/core/config.pybackend/src/find_api/core/database.pybackend/src/find_api/core/dependencies.pybackend/src/find_api/main.pybackend/src/find_api/models/__init__.pybackend/src/find_api/models/invite.pybackend/src/find_api/models/join_request.pybackend/src/find_api/models/media.pybackend/src/find_api/models/session.pybackend/src/find_api/models/user.pybackend/src/find_api/routers/auth.pybackend/src/find_api/routers/upload.pybackend/tests/conftest.pybackend/tests/test_auth.pydocs/plans/not-started/small-team-authentication.md
| def __repr__(self): | ||
| return f"<JoinRequest(id={self.id}, username={self.username}, status={self.status})>" |
There was a problem hiding this comment.
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.
| 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.
| uploader_user_id: Optional[int] = None, | ||
| ) -> dict: |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
🛠️ 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.
| 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 |
There was a problem hiding this comment.
🛠️ 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.
| 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" |
There was a problem hiding this comment.
🛠️ 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.
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/setupto create an admin account, the instance switches to "shared mode" and requires authentication for protected endpoints.It introduces the
User,AuthSession,InviteToken, andJoinRequestmodels alongside secure password hashing (bcrypt) and opaque session tokens.Mediauploads are also now tagged with theuploader_user_idwhen authenticated.Fixes #262
Type of change
What changed
User,AuthSession,InviteToken, andJoinRequestSQLAlchemy models. ModifiedMediato track the uploader.core/auth.py. Added FastAPI dependencies (get_optional_user,get_required_user,get_admin_user) for robust access control./api/authendpoints for setup, login, logout, generating invites, submitting join requests, and admin approvals. Wired the auth dependency intorouters/upload.py.tests/test_auth.pycovering 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)
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)
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
GSSoC'26 checklist
Summary by CodeRabbit
New Features
Tests
Documentation