Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 15 additions & 8 deletions backend/init_db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
import os
from pathlib import Path
from sqlalchemy import text
import logging

# Add project root to path
current_file = Path(__file__).resolve()
Expand All @@ -11,19 +13,13 @@
from backend.database import engine, Base
from backend.models import *

logger = logging.getLogger(__name__)

def init_db():
print("Creating tables...")
Base.metadata.create_all(bind=engine)
print("Tables created.")

if __name__ == "__main__":
init_db()
from sqlalchemy import text
from backend.database import engine
import logging

logger = logging.getLogger(__name__)

def migrate_db():
"""
Perform database migrations.
Expand Down Expand Up @@ -124,6 +120,13 @@ def migrate_db():
except Exception:
pass

# Add previous_integrity_hash column for O(1) blockchain verification
try:
conn.execute(text("ALTER TABLE issues ADD COLUMN previous_integrity_hash VARCHAR"))
print("Migrated database: Added previous_integrity_hash column.")
except Exception:
pass

# Add index on user_email
try:
conn.execute(text("CREATE INDEX ix_issues_user_email ON issues (user_email)"))
Expand Down Expand Up @@ -212,3 +215,7 @@ def migrate_db():
logger.info("Database migration check completed.")
except Exception as e:
logger.error(f"Database migration error: {e}")

if __name__ == "__main__":
init_db()
migrate_db()
3 changes: 2 additions & 1 deletion backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ class Issue(Base):
longitude = Column(Float, nullable=True, index=True)
location = Column(String, nullable=True)
action_plan = Column(JSONEncodedDict, nullable=True)
integrity_hash = Column(String, nullable=True) # Blockchain integrity seal
integrity_hash = Column(String, nullable=True) # Current report hash
previous_integrity_hash = Column(String, nullable=True) # Link to predecessor for O(1) verification

class PushSubscription(Base):
__tablename__ = "push_subscriptions"
Expand Down
6 changes: 4 additions & 2 deletions backend/requirements-render.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ Pillow
firebase-functions
firebase-admin
a2wsgi
python-jose[cryptography]
passlib[bcrypt]
python-jose
cryptography
passlib
bcrypt
Comment on lines +16 to +19
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

passlib bcrypt compatibility issues breaking changes 2024 2025

💡 Result:

Key Passlib ↔ bcrypt compatibility / breaking-change issues (2024–2025)

  1. bcrypt 4.1.x “version metadata” / detection changes → Passlib warnings/errors
  • bcrypt 4.1.0 was yanked explicitly due to “Incompatibility with assumptions made by passlib”. [1]
  • bcrypt 4.1.1 notes it fixed incompatibility with passlib package detection assumptions. [2]
  • In practice, many projects saw Passlib log/raise around reading bcrypt’s version (commonly AttributeError: module 'bcrypt' has no attribute '__about__'). [3]
  1. bcrypt 5.0.0 changes long-password behavior (real breaking change)
  • bcrypt.hashpw() with a password longer than 72 bytes now raises ValueError; previously it was silently truncated. This can break applications/tests that relied on truncation (and some Passlib code paths). [2]
  1. Python 3.12 packaging change causing older Passlib import/install failures
  • Some ecosystems reported that with Python ≥ 3.12 (where setuptools may not be present by default), “old passlib” can fail to import unless setuptools is added (leading some projects to switch to forks like libpass). [4]

Practical mitigations people used

  • Pin bcrypt to a known-working range with your Passlib version (many pinned to 4.0.1 to avoid the __about__/version-reading issue) until upgrading off Passlib or to a maintained fork. [3]
  • If adopting bcrypt 5.0.0+, ensure your code never passes >72 bytes to bcrypt (e.g., pre-hash with SHA-256/Blake2 then base64) to avoid ValueError. [2]

Sources: bcrypt PyPI release history (yanked 4.1.0) [1], bcrypt 5.0.0 changelog (includes 4.1.1 note + 72-byte ValueError) [2], example failure report showing __about__ error surfaced via Passlib [3], Flask-Security note on Python 3.12 / passlib / setuptools and libpass [4].


🏁 Script executed:

# Check the full requirements-render.txt file
cat -n backend/requirements-render.txt

Repository: RohanExploit/VishwaGuru

Length of output: 429


🏁 Script executed:

# Look for other requirements files to understand the versioning strategy
find backend -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" -o -name "setup.cfg" | head -20

Repository: RohanExploit/VishwaGuru

Length of output: 123


🏁 Script executed:

# Search for usage of these packages in the codebase to understand their purpose
rg "python_jose|cryptography|passlib|bcrypt" --type py -A 2 | head -50

Repository: RohanExploit/VishwaGuru

Length of output: 385


🏁 Script executed:

# Check the other requirements file for comparison
cat -n backend/requirements.txt

Repository: RohanExploit/VishwaGuru

Length of output: 684


🏁 Script executed:

# Look for more usage of these packages, especially JWT and cryptography
rg "python_jose|jwt|from_config" --type py | head -30

Repository: RohanExploit/VishwaGuru

Length of output: 434


🏁 Script executed:

# Check if there are any version specifications elsewhere (setup.py, pyproject.toml, etc.)
find . -name "setup.py" -o -name "pyproject.toml" -o -name "setup.cfg" | xargs cat 2>/dev/null || echo "No setup files found"

Repository: RohanExploit/VishwaGuru

Length of output: 49


🌐 Web query:

python-jose cryptography passlib bcrypt current versions 2025

💡 Result:

Latest PyPI releases during 2025 (i.e., “current” by end of 2025):

  • python-jose: 3.5.0 (May 28, 2025) [1]
  • cryptography: 46.0.3 (Oct 15, 2025) [2]
  • passlib: 1.7.4 (Oct 8, 2020 — no 2025 release) [3]
  • bcrypt: 5.0.0 (Sep 25, 2025) [4]

Sources: [1] [2] [3] [4]


Unpinned security-sensitive dependencies and inconsistent dependency specification risk breaking production builds.

The four packages lack version pins and are split into separate entries, unlike the main requirements.txt which uses extras syntax (python-jose[cryptography], passlib[bcrypt]). Splitting these into separate entries means pip doesn't enforce the link between passlib and its bcrypt backend — reducing robustness.

Additionally, uncontrolled versions are risky for production. bcrypt 5.0.0 (Sep 2025) introduced a breaking change: hashpw() now raises ValueError for passwords longer than 72 bytes (previously silently truncated). An unpinned deployment could pull bcrypt 5.0.0 and break authentication if the application or passlib passes untruncated passwords.

Consider reverting to the extras syntax from requirements.txt and adding version pins to guard against breaking changes:

Suggested pins
-python-jose
-cryptography
-passlib
-bcrypt
+python-jose[cryptography]>=3.3,<5
+cryptography>=42.0,<50
+passlib[bcrypt]>=1.7.4,<2
+bcrypt>=4.1.1,<5

The bcrypt>=4.1.1,<5 pin avoids both the yanked 4.1.0 (which broke passlib version detection) and the 5.0.0 breaking change with long passwords.

📝 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
python-jose
cryptography
passlib
bcrypt
python-jose[cryptography]>=3.3,<5
cryptography>=42.0,<50
passlib[bcrypt]>=1.7.4,<2
bcrypt>=4.1.1,<5
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/requirements-render.txt` around lines 16 - 19, The
requirements-render.txt entries for python-jose, cryptography, passlib, and
bcrypt should be consolidated and version-pinned to mirror the main requirements
extras and avoid incompatible transitive installs: replace the four separate
lines with the extras form python-jose[cryptography]==<PIN>,
passlib[bcrypt]==<PIN>, and bcrypt>=4.1.1,<5 (or a concrete upper-bounded range)
so pip installs the linked extras and prevents bcrypt 5.0.0; adjust pins for
python-jose and cryptography to match your tested versions and ensure passlib
and bcrypt are compatible.

102 changes: 82 additions & 20 deletions backend/routers/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from backend.models import Issue, PushSubscription
from backend.schemas import (
IssueCreateWithDeduplicationResponse, IssueCategory, NearbyIssueResponse,
DeduplicationCheckResponse, IssueSummaryResponse, VoteResponse,
DeduplicationCheckResponse, IssueSummaryResponse, IssueResponse, VoteResponse,
IssueStatusUpdateRequest, IssueStatusUpdateResponse, PushSubscriptionRequest,
PushSubscriptionResponse, BlockchainVerificationResponse
)
Expand Down Expand Up @@ -168,15 +168,22 @@ async def create_issue(
try:
# Save to DB only if no nearby issues found or deduplication failed
if deduplication_info is None or not deduplication_info.has_nearby_issues:
# Blockchain feature: calculate integrity hash for the report
# Optimization: Fetch only the last hash to maintain the chain with minimal overhead
# Robust Blockchain Implementation
# 1. Fetch only the last hash to maintain the chain with minimal overhead
prev_issue = await run_in_threadpool(
lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).first()
)
Comment on lines +171 to 175
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

Computing prev_hash and then inserting the new issue is not atomic. Under concurrent issue creation, multiple issues can end up with the same previous_integrity_hash, which breaks the “previous report” linear-chain assumption. If strict linear chaining by insertion order is required, wrap this in a transaction and use a lock (e.g., SELECT ... FOR UPDATE) or another serialization strategy.

Copilot uses AI. Check for mistakes.
prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else ""
Comment on lines 173 to 176
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

prev_hash is derived from the most recent Issue.integrity_hash regardless of whether it’s NULL. Since some issues (e.g., Telegram-sourced) are created without an integrity_hash, this can cause new web issues to start chaining from an empty string even when there are earlier valid hashes. Consider querying the most recent non-null/non-empty integrity_hash (and ideally a deterministic genesis value for the first sealed issue).

Copilot uses AI. Check for mistakes.

# Simple but effective SHA-256 chaining
hash_content = f"{description}|{category}|{prev_hash}"
# 2. Generate secure reference ID
reference_id = str(uuid.uuid4())

# 3. Calculate robust integrity hash incorporating multiple fields
# Chaining logic: hash(ref_id|desc|cat|lat|lon|email|prev_hash)
# Use fixed float formatting to ensure consistent hashing across environments
lat_str = f"{latitude:.7f}" if latitude is not None else "None"
lon_str = f"{longitude:.7f}" if longitude is not None else "None"
hash_content = f"{reference_id}|{description}|{category}|{lat_str}|{lon_str}|{user_email}|{prev_hash}"
integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()

# RAG Retrieval (New)
Expand All @@ -186,7 +193,7 @@ async def create_issue(
initial_action_plan = {"relevant_government_rule": relevant_rule}

new_issue = Issue(
reference_id=str(uuid.uuid4()),
reference_id=reference_id,
description=description,
category=category,
image_path=image_path,
Expand All @@ -196,7 +203,8 @@ async def create_issue(
longitude=longitude,
location=location,
action_plan=initial_action_plan,
integrity_hash=integrity_hash
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
)

# Offload blocking DB operations to threadpool
Expand Down Expand Up @@ -615,30 +623,44 @@ def get_user_issues(
async def verify_blockchain_integrity(issue_id: int, db: Session = Depends(get_db)):
"""
Verify the cryptographic integrity of a report using the blockchain-style chaining.
Optimized: Uses column projection to fetch only needed data.
Optimized: Uses stored previous_integrity_hash for O(1) chain verification.
"""
# Fetch current issue data
# Fetch current issue data (Performance Boost: Fetch only needed columns)
current_issue = await run_in_threadpool(
lambda: db.query(
Issue.id, Issue.description, Issue.category, Issue.integrity_hash
Issue.id,
Issue.reference_id,
Issue.description,
Issue.category,
Issue.latitude,
Issue.longitude,
Issue.user_email,
Issue.integrity_hash,
Issue.previous_integrity_hash
).filter(Issue.id == issue_id).first()
)

if not current_issue:
raise HTTPException(status_code=404, detail="Issue not found")

# Fetch previous issue's integrity hash to verify the chain
prev_issue_hash = await run_in_threadpool(
lambda: db.query(Issue.integrity_hash).filter(Issue.id < issue_id).order_by(Issue.id.desc()).first()
)

prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else ""
# Chaining logic depends on when the issue was created (legacy fallback)
# Optimized: Use stored previous hash if available, otherwise fallback to subquery (O(N) search)
if current_issue.previous_integrity_hash is not None:
prev_hash = current_issue.previous_integrity_hash
# New robust hash formula with fixed float formatting
lat_str = f"{current_issue.latitude:.7f}" if current_issue.latitude is not None else "None"
lon_str = f"{current_issue.longitude:.7f}" if current_issue.longitude is not None else "None"
hash_content = f"{current_issue.reference_id}|{current_issue.description}|{current_issue.category}|{lat_str}|{lon_str}|{current_issue.user_email}|{prev_hash}"
else:
# Legacy fallback: Fetch previous issue's hash via subquery
prev_issue_hash = await run_in_threadpool(
lambda: db.query(Issue.integrity_hash).filter(Issue.id < issue_id).order_by(Issue.id.desc()).first()
)
prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else ""
# Legacy hash formula
hash_content = f"{current_issue.description}|{current_issue.category}|{prev_hash}"

# Recompute hash based on current data and previous hash
# Chaining logic: hash(description|category|prev_hash)
hash_content = f"{current_issue.description}|{current_issue.category}|{prev_hash}"
computed_hash = hashlib.sha256(hash_content.encode()).hexdigest()

is_valid = (computed_hash == current_issue.integrity_hash)

if is_valid:
Expand Down Expand Up @@ -702,3 +724,43 @@ def get_recent_issues(
# Thread-safe cache update
recent_issues_cache.set(data, cache_key)
return data

@router.get("/api/issues/{issue_id}", response_model=IssueResponse)
async def get_issue_by_id(issue_id: int, db: Session = Depends(get_db)):
"""
Get a single issue by its ID.
Optimized: Uses column projection for efficient retrieval.
"""
# Performance Boost: Use column projection instead of loading full model
issue = await run_in_threadpool(
lambda: db.query(
Issue.id,
Issue.category,
Issue.description,
Issue.created_at,
Issue.image_path,
Issue.status,
Issue.upvotes,
Issue.location,
Issue.latitude,
Issue.longitude,
Issue.action_plan
).filter(Issue.id == issue_id).first()
)

if not issue:
raise HTTPException(status_code=404, detail="Issue not found")

return {
"id": issue.id,
"category": issue.category,
"description": issue.description,
"created_at": issue.created_at,
"image_path": issue.image_path,
"status": issue.status,
"upvotes": issue.upvotes or 0,
"location": issue.location,
"latitude": issue.latitude,
"longitude": issue.longitude,
"action_plan": issue.action_plan
}
8 changes: 8 additions & 0 deletions frontend/src/api/issues.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,13 @@ export const issuesApi = {

vote: async (id) => {
return await apiClient.post(`/api/issues/${id}/vote`, {}); // The backend endpoint might not require a body for upvote
},

getById: async (id) => {
return await apiClient.get(`/api/issues/${id}`);
},

verifyBlockchain: async (id) => {
return await apiClient.get(`/api/issues/${id}/blockchain-verify`);
}
Comment on lines +25 to 31
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

New API methods getById and verifyBlockchain were added but aren’t covered by the existing Jest tests for issuesApi (see frontend/src/api/__tests__/issues.test.js). Add tests asserting the correct endpoints are called and that errors propagate/are handled consistently with the other methods.

Copilot uses AI. Check for mistakes.
};
73 changes: 67 additions & 6 deletions frontend/src/views/VerifyView.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Camera, Upload, CheckCircle, XCircle, AlertTriangle, ArrowLeft } from 'lucide-react';
import { Camera, Upload, CheckCircle, XCircle, AlertTriangle, ArrowLeft, ShieldCheck } from 'lucide-react';
import { issuesApi } from '../api/issues';

const API_URL = import.meta.env.VITE_API_URL || '';
Expand All @@ -14,16 +14,18 @@ const VerifyView = () => {
const [image, setImage] = useState(null);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [blockchainLoading, setBlockchainLoading] = useState(false);
const [blockchainResult, setBlockchainResult] = useState(null);

useEffect(() => {
const fetchIssue = async () => {
try {
const issues = await issuesApi.getRecent();
const found = issues.find(i => i.id === parseInt(id));
if (found) {
setIssue(found);
// Optimization: Fetch issue by ID directly (O(1)) instead of searching recent list (O(N))
const data = await issuesApi.getById(id);
if (data) {
setIssue(data);
} else {
setError("Issue not found in recent list.");
setError("Issue not found.");
}
} catch (err) {
console.error("Load failed", err);
Expand All @@ -42,6 +44,23 @@ const VerifyView = () => {
}
};

const handleBlockchainVerify = async () => {
setBlockchainLoading(true);
setBlockchainResult(null);
try {
const data = await issuesApi.verifyBlockchain(id);
setBlockchainResult(data);
} catch (err) {
console.error("Blockchain verification failed", err);
setBlockchainResult({
is_valid: false,
message: "Failed to connect to the blockchain verification service."
});
} finally {
setBlockchainLoading(false);
}
};

const handleVerify = async () => {
if (!image) return;
setVerifying(true);
Expand Down Expand Up @@ -141,6 +160,48 @@ const VerifyView = () => {
)}
</div>
</div>

{/* Blockchain Integrity Seal Section */}
<div className="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-xl shadow-sm border border-indigo-100 p-6 mb-6">
<div className="flex items-center gap-3 mb-4">
<ShieldCheck className="text-indigo-600" size={28} />
<h2 className="text-xl font-bold text-indigo-900">Blockchain Integrity Seal</h2>
</div>

<p className="text-indigo-800 text-sm mb-4">
Every report in our system is cryptographically sealed and linked to the previous report, creating an immutable chain of records. Verify that this report hasn't been tampered with.
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The UI copy claims “Every report in our system is cryptographically sealed…”, but some records can exist without an integrity seal (e.g., legacy issues or Telegram-sourced issues that don’t set integrity_hash). Please soften/clarify this text so it doesn’t make a guarantee the backend can’t uphold for all issues.

Suggested change
Every report in our system is cryptographically sealed and linked to the previous report, creating an immutable chain of records. Verify that this report hasn't been tampered with.
Reports in our system can be cryptographically sealed and linked together to create an immutable chain of records. When a cryptographic seal is available, you can verify that this report hasn't been tampered with.

Copilot uses AI. Check for mistakes.
</p>

<button
onClick={handleBlockchainVerify}
disabled={blockchainLoading}
className="w-full bg-indigo-600 text-white py-2 rounded-lg font-semibold hover:bg-indigo-700 transition disabled:opacity-50"
>
{blockchainLoading ? 'Verifying Integrity Hash...' : 'Verify Cryptographic Seal'}
</button>

{blockchainResult && (
<div className={`mt-4 p-3 rounded border text-sm ${blockchainResult.is_valid ? 'bg-white border-green-200 text-green-800' : 'bg-red-50 border-red-200 text-red-800'}`}>
<div className="flex items-start gap-2">
{blockchainResult.is_valid ? (
<CheckCircle size={18} className="text-green-600 mt-0.5" />
) : (
<XCircle size={18} className="text-red-600 mt-0.5" />
)}
<div>
<p className="font-bold">{blockchainResult.is_valid ? 'Integrity Verified' : 'Integrity Compromised'}</p>
<p className="opacity-90">{blockchainResult.message}</p>
{blockchainResult.current_hash && (
<div className="mt-2 pt-2 border-t border-gray-100 font-mono text-[10px] break-all">
<p className="font-semibold text-gray-500 mb-1 uppercase tracking-tighter">Current Hash:</p>
{blockchainResult.current_hash}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ services:
name: vishwaguru-backend
property: port
- key: PYTHONPATH
value: backend
value: .
# Required API Keys (must be set in Render dashboard)
- key: GEMINI_API_KEY
sync: false
Expand Down