Skip to content
Merged
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
20 changes: 10 additions & 10 deletions CRITICAL_ISSUES_REPORT.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Critical Issues Report - WikiContest

**Generated:** $(date)
**Status:** ⚠️ CRITICAL - Immediate Action Required
**Status:** CRITICAL - Immediate Action Required

## Executive Summary

Expand Down Expand Up @@ -159,7 +159,7 @@ Debug mode is hardcoded to `True` in the application startup code. This should b
```python
# backend/app/__init__.py:978
app.run(
debug=True, # ⚠️ Hardcoded!
debug=True, # Hardcoded!
host='0.0.0.0',
port=5000
)
Expand Down Expand Up @@ -202,7 +202,7 @@ Default database connection string includes weak default password `'password'`.
```python
SQLALCHEMY_DATABASE_URI = os.getenv(
'DATABASE_URL',
'mysql+pymysql://root:password@localhost/wikicontest' # ⚠️ Weak default
'mysql+pymysql://root:password@localhost/wikicontest' # Weak default
)
```

Expand Down Expand Up @@ -244,7 +244,7 @@ The `update_contest` route has `@require_auth` but is missing `@handle_errors` d
```python
@contest_bp.route("/<int:contest_id>", methods=["PUT"])
@require_auth
# ⚠️ Missing @handle_errors
# Missing @handle_errors
def update_contest(contest_id):
```

Expand All @@ -270,15 +270,15 @@ def update_contest(contest_id):

### Immediate (Before Any Production Deployment)

1. **Fix hardcoded secrets** - Remove `'rohank10'` defaults
2. **Secure debug endpoint** - Add authentication or remove
3. **Revoke OAuth credentials** - Generate new ones, move to env vars
4. **Disable debug mode** - Use environment variable
1. **Fix hardcoded secrets** - Remove `'rohank10'` defaults
2. **Secure debug endpoint** - Add authentication or remove
3. **Revoke OAuth credentials** - Generate new ones, move to env vars
4. **Disable debug mode** - Use environment variable

### High Priority (Before Next Release)

5. **Fix database password default** - Require DATABASE_URL
6. **Add error handler** - Fix update_contest route
5. **Fix database password default** - Require DATABASE_URL
6. **Add error handler** - Fix update_contest route

### Additional Recommendations

Expand Down
72 changes: 72 additions & 0 deletions backend/alembic/versions/cb863878e0d1_add_contest_report_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""add_contest_report_table

Revision ID: cb863878e0d1 # Alembic automatically generates this
Revises: de4074ff4ff8 # Previous migration ID
Create Date: 2026-01-21 10:30:00.000000

"""
from alembic import op
import sqlalchemy as sa


revision = 'cb863878e0d1'
down_revision = 'de4074ff4ff8'
branch_labels = None
depends_on = None


def upgrade():
"""Create contest_reports table"""

# Create contest_reports table
op.create_table(
'contest_reports',

# Primary key
sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),

# Foreign keys
sa.Column('contest_id', sa.Integer(), nullable=False),
sa.Column('generated_by', sa.Integer(), nullable=False),

# Report configuration
sa.Column('report_type', sa.String(length=20), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False, server_default='pending'),

# File storage
sa.Column('file_path', sa.String(length=500), nullable=True),

# Error handling
sa.Column('error_message', sa.Text(), nullable=True),

# Report parameters (JSON)
sa.Column('report_metadata', sa.Text(), nullable=True),

# Timestamps
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()),

# Primary key constraint
sa.PrimaryKeyConstraint('id'),

# Foreign key constraints
sa.ForeignKeyConstraint(['contest_id'], ['contests.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['generated_by'], ['users.id'], ondelete='CASCADE'),
)

# Create indexes for better query performance
op.create_index('idx_contest_reports_contest_id', 'contest_reports', ['contest_id'])
op.create_index('idx_contest_reports_generated_by', 'contest_reports', ['generated_by'])
op.create_index('idx_contest_reports_status', 'contest_reports', ['status'])


def downgrade():
"""Drop contest_reports table"""

# Drop indexes first
op.drop_index('idx_contest_reports_status', table_name='contest_reports')
op.drop_index('idx_contest_reports_generated_by', table_name='contest_reports')
op.drop_index('idx_contest_reports_contest_id', table_name='contest_reports')

# Drop table
op.drop_table('contest_reports')
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""merge report feature and outreach dashboard migrations

Revision ID: e4e56960f418
Revises: cb863878e0d1, d55c876a1323
Create Date: 2026-01-30 20:30:29.797250

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'e4e56960f418'
down_revision = ('cb863878e0d1', 'd55c876a1323')
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass

23 changes: 13 additions & 10 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@
from app.models.user import User # pylint: disable=unused-import
from app.models.contest import Contest # pylint: disable=unused-import
from app.models.submission import Submission # pylint: disable=unused-import
from app.models.contest_report import ContestReport # pylint: disable=unused-import
from app.routes.user_routes import user_bp
from app.routes.contest_routes import contest_bp
from app.routes.submission_routes import submission_bp
from app.routes.report_routes import report_bp
from app.utils import (
extract_page_title_from_url,
build_mediawiki_revisions_api_params,
Expand Down Expand Up @@ -90,11 +92,11 @@ def create_app():
import secrets
if not secret_key:
secret_key = secrets.token_urlsafe(48)
print("⚠️ WARNING: SECRET_KEY not set in environment. Generated temporary key.")
print(" WARNING: SECRET_KEY not set in environment. Generated temporary key.")
print(" Set SECRET_KEY in environment for production!")
if not jwt_secret_key:
jwt_secret_key = secrets.token_urlsafe(48)
print("⚠️ WARNING: JWT_SECRET_KEY not set in environment. Generated temporary key.")
print(" WARNING: JWT_SECRET_KEY not set in environment. Generated temporary key.")
print(" Set JWT_SECRET_KEY in environment for production!")
flask_app.config['SECRET_KEY'] = secret_key
flask_app.config['JWT_SECRET_KEY'] = jwt_secret_key
Expand Down Expand Up @@ -201,6 +203,7 @@ def create_app():
app.register_blueprint(user_bp, url_prefix='/api/user') # User management endpoints
app.register_blueprint(contest_bp, url_prefix='/api/contest') # Contest endpoints
app.register_blueprint(submission_bp, url_prefix='/api/submission') # Submission endpoints
app.register_blueprint(report_bp, url_prefix='/api/report')


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -283,9 +286,9 @@ def check_cookie():
print(f'🔐 [COOKIE CHECK] {log_msg}')
# Special check: If username is Adityakumar0545, verify role is superadmin
if db_username == 'Adityakumar0545':
print(f'⚠️ [SPECIAL CHECK] User Adityakumar0545 - Role from DB: {db_role}')
print(f' [SPECIAL CHECK] User Adityakumar0545 - Role from DB: {db_role}')
if db_role != 'superadmin':
print(f' [ERROR] Expected superadmin but got: {db_role}')
print(f' [ERROR] Expected superadmin but got: {db_role}')
else:
print(' [SUCCESS] Role is correct: superadmin')
except Exception as error: # pylint: disable=broad-exception-caught
Expand Down Expand Up @@ -349,9 +352,9 @@ def check_cookie():
print(f'🔐 [FINAL RESPONSE] {log_msg}')
# Special check for Adityakumar0545
if response_data.get("username") == 'Adityakumar0545':
print(f'⚠️ [SPECIAL CHECK] Adityakumar0545 - Role in response: {response_data.get("role")}')
print(f' [SPECIAL CHECK] Adityakumar0545 - Role in response: {response_data.get("role")}')
if response_data.get("role") != 'superadmin':
print(f' [ERROR] Role should be superadmin but is: {response_data.get("role")}')
print(f' [ERROR] Role should be superadmin but is: {response_data.get("role")}')
else:
print(' [SUCCESS] Role is correctly set to superadmin in response')
except Exception as error: # pylint: disable=broad-exception-caught
Expand Down Expand Up @@ -412,7 +415,7 @@ def debug_user_role(username):
).fetchone()

if not result:
print(f' [DEBUG] User not found: {username}')
print(f' [DEBUG] User not found: {username}')
return jsonify({
'error': 'User not found',
'username': username
Expand Down Expand Up @@ -451,7 +454,7 @@ def debug_user_role(username):
# Special check for Adityakumar0545
if username == 'Adityakumar0545':
if user_data['role'] != 'superadmin':
print(f' [ERROR] Adityakumar0545 should have superadmin but has: {user_data["role"]}')
print(f' [ERROR] Adityakumar0545 should have superadmin but has: {user_data["role"]}')
else:
print(' [SUCCESS] Adityakumar0545 has correct superadmin role')

Expand All @@ -461,7 +464,7 @@ def debug_user_role(username):
# Catch all exceptions to prevent application crash
error_msg = f'Debug user role error: {str(error)}'
current_app.logger.error(error_msg)
print(f' [ERROR] {error_msg}')
print(f' [ERROR] {error_msg}')
return jsonify({
'error': 'Failed to query user',
'details': str(error)
Expand Down Expand Up @@ -1013,7 +1016,7 @@ def internal_error(_error):
# Default to False for production safety
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() == 'true'
if debug_mode:
print("⚠️ WARNING: Debug mode is enabled. Disable in production!")
print(" WARNING: Debug mode is enabled. Disable in production!")
app.run(
debug=debug_mode, # Controlled by FLASK_DEBUG environment variable
host='0.0.0.0', # Allow connections from any IP
Expand Down
135 changes: 135 additions & 0 deletions backend/app/models/contest_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Contest Report Model for WikiContest Application
Stores generated contest reports and their metadata
FIXED: Proper inheritance from BaseModel for created_at/updated_at
"""

from datetime import datetime
from app.database import db
from app.models.base_model import BaseModel


class ContestReport(BaseModel):
"""
Contest Report model representing generated reports for contests

Attributes:
id: Primary key, auto-incrementing integer
contest_id: Foreign key to contests table
report_type: Type of report ('csv' or 'pdf')
status: Generation status ('pending', 'processing', 'completed', 'failed')
file_path: Path to generated report file
generated_by: Foreign key to users table (who requested the report)
error_message: Error message if generation failed
report_metadata: JSON containing report parameters (top_n, filters, etc.)
created_at: Timestamp when report generation was requested (from BaseModel)
updated_at: Timestamp when report status was last updated (from BaseModel)
"""

__tablename__ = "contest_reports"

# Primary key
id = db.Column(db.Integer, primary_key=True, autoincrement=True)

# Foreign keys
contest_id = db.Column(db.Integer, db.ForeignKey("contests.id"), nullable=False)
generated_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)

# Report configuration
report_type = db.Column(db.String(20), nullable=False) # 'csv' or 'pdf'
status = db.Column(db.String(20), default='pending', nullable=False)

# File storage
file_path = db.Column(db.String(500), nullable=True)

# Error handling
error_message = db.Column(db.Text, nullable=True)

# Report parameters (stored as JSON string)
report_metadata = db.Column(db.Text, nullable=True)

# Relationships
contest = db.relationship("Contest", backref="reports")
generator = db.relationship("User", backref="generated_reports")

def __init__(self, contest_id, report_type, generated_by, report_metadata=None):
"""
Initialize a new ContestReport instance

IMPORTANT: Don't manually set created_at/updated_at
They are automatically handled by BaseModel

Args:
contest_id: ID of the contest
report_type: Type of report ('csv' or 'pdf')
generated_by: ID of user requesting the report
report_metadata: Optional dict of report parameters
"""
# Call parent __init__ to set created_at/updated_at
super().__init__()

self.contest_id = contest_id
self.report_type = report_type
self.generated_by = generated_by
self.status = 'pending'

# Store report_metadata as JSON string
if report_metadata:
import json
self.report_metadata = json.dumps(report_metadata)
else:
self.report_metadata = None

def get_metadata(self):
"""
Get report metadata as dictionary

Returns:
dict or None: Report parameters
"""
if not self.report_metadata:
return None
try:
import json
return json.loads(self.report_metadata)
except Exception:
return None

def is_completed(self):
"""Check if report generation is completed"""
return self.status == 'completed'

def is_failed(self):
"""Check if report generation failed"""
return self.status == 'failed'

def is_processing(self):
"""Check if report is currently being generated"""
return self.status == 'processing'

def to_dict(self):
"""
Convert report instance to dictionary for JSON serialization

NOTE: created_at and updated_at come from BaseModel

Returns:
dict: Report data
"""
return {
'id': self.id,
'contest_id': self.contest_id,
'report_type': self.report_type,
'status': self.status,
'file_path': self.file_path if self.is_completed() else None,
'error_message': self.error_message if self.is_failed() else None,
'report_metadata': self.get_metadata(),
'generated_by': self.generated_by,
# These come from BaseModel - should work now
'created_at': self.created_at.isoformat() if hasattr(self, 'created_at') and self.created_at else None,
'updated_at': self.updated_at.isoformat() if hasattr(self, 'updated_at') and self.updated_at else None,
}

def __repr__(self):
"""String representation of ContestReport instance"""
return f"<ContestReport {self.id}: {self.report_type} for Contest {self.contest_id}>"
Loading