Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ class SharedSnippet(Base):
__tablename__ = "shares"

id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), index=True, nullable=True
)
token: Mapped[str] = mapped_column(String(64), unique=True, index=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
Expand Down
41 changes: 40 additions & 1 deletion backend/app/routers/share.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlalchemy.orm import Session

from ..database import get_db
from ..models import SharedSnippet, User
from ..models import AuditLog, SharedSnippet, User
from ..schemas import ShareCreateRequest, ShareRecord
from ..security import get_current_user

Expand Down Expand Up @@ -145,3 +145,42 @@ def get_share(token: str, db: Session = Depends(get_db)):
result=json.loads(record.result_json),
created_at=created_at.isoformat(),
)


@router.delete("/{token}", status_code=status.HTTP_204_NO_CONTENT)
def delete_share(
token: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
record = db.execute(
select(SharedSnippet).where(SharedSnippet.token == token)
).scalar_one_or_none()

if record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Shared result not found"
)

# Enforce authorization: only admins or the snippet owner can delete
if not current_user.is_admin and record.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this share",
)

# Record deletion in audit log
audit_log = AuditLog(
actor_id=current_user.id,
actor_email=current_user.email,
action="delete_share",
target_type="SharedSnippet",
target_id=str(record.id),
details=f"Deleted shared snippet with token: {token}",
)
db.add(audit_log)

db.delete(record)
db.commit()

return
46 changes: 45 additions & 1 deletion backend/tests/test_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from datetime import UTC, datetime, timedelta

import pytest
from app import database
from app.database import Base, get_db
from app.main import app
from app.models import AuditLog, SharedSnippet, User
from app.security import get_current_user
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
Expand Down Expand Up @@ -106,7 +109,6 @@ def test_share_accessible_after_owner_logout(client):

def test_expired_share_returns_404(client):
db = TEST_SESSION_LOCAL()
from app.models import SharedSnippet

record = SharedSnippet(
token="expired123",
Expand All @@ -121,3 +123,45 @@ def test_expired_share_returns_404(client):
resp = client.get("/share/expired123")
assert resp.status_code == 404
assert "expired" in resp.json()["detail"].lower()


def test_delete_share_authorization(client):
db = TEST_SESSION_LOCAL()

# 1. Create our pretend users in the database
owner = User(email="owner@test.com", password_hash="fake_pass", is_admin=False)
admin = User(email="admin@test.com", password_hash="fake_pass", is_admin=True)
stranger = User(
email="stranger@test.com", password_hash="fake_pass", is_admin=False
)
db.add_all([owner, admin, stranger])
db.commit()

# 2. Create pretend shares owned by the 'owner'
share1 = SharedSnippet(
token="token1", code="print('1')", result_json="{}", user_id=owner.id
)
share2 = SharedSnippet(
token="token2", code="print('2')", result_json="{}", user_id=owner.id
)
db.add_all([share1, share2])
db.commit()

# 3. Test Scenario A: Stranger tries to delete (Should Fail - 403)
app.dependency_overrides[get_current_user] = lambda: stranger
resp_stranger = client.delete("/share/token1")
assert resp_stranger.status_code == 403

# 4. Test Scenario B: Owner tries to delete (Should Succeed - 204)
app.dependency_overrides[get_current_user] = lambda: owner
resp_owner = client.delete("/share/token1")
assert resp_owner.status_code == 204

# 5. Test Scenario C: Admin tries to delete (Should Succeed - 204)
app.dependency_overrides[get_current_user] = lambda: admin
resp_admin = client.delete("/share/token2")
assert resp_admin.status_code == 204

# Cleanup our overrides and close database
app.dependency_overrides.clear()
db.close()
Loading