Skip to content

Commit 9655d54

Browse files
authored
Merge pull request #1 from Team334/2025
2025 SZN
2 parents c13ca9c + b807c4b commit 9655d54

40 files changed

+5350
-1854
lines changed

Diff for: .github/workflows/bandit.yml

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ name: Bandit Code Security
22

33
on: [push, pull_request]
44

5+
permissions:
6+
contents: read
7+
security-events: write
8+
59
jobs:
610
bandit:
711
runs-on: ubuntu-latest

Diff for: .gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,7 @@ cython_debug/
161161
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162162
#.idea/
163163
.vercel
164+
165+
# CodeQL
166+
output.sarif
167+
codeqldb/

Diff for: app/__main__.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from app.app import create_app
2-
from waitress import serve
3-
from dotenv import load_dotenv
41
import os
52

3+
from dotenv import load_dotenv
4+
from waitress import serve
5+
6+
from app.app import create_app
7+
68
load_dotenv()
79

810
app = create_app()
@@ -13,6 +15,7 @@
1315
port = int(os.getenv("PORT", 5000))
1416

1517
if debug_mode:
16-
app.run(debug=True, host=host, port=port)
18+
debug = os.getenv("DEBUG", "False").lower() == "true"
19+
app.run(debug=debug, host=host, port=port)
1720
else:
1821
serve(app, host=host, port=port)

Diff for: app/app.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from flask import Flask, make_response, render_template, send_from_directory
2-
from flask_login import LoginManager
3-
from flask_pymongo import PyMongo
41
import os
2+
53
from dotenv import load_dotenv
4+
from flask import (Flask, jsonify, make_response, render_template,
5+
send_from_directory)
6+
from flask_login import LoginManager
7+
from flask_pymongo import PyMongo
68
from flask_wtf.csrf import CSRFProtect
79

810
from app.auth.auth_utils import UserManager
@@ -26,6 +28,7 @@ def create_app():
2628
)
2729

2830
mongo.init_app(app)
31+
# csrf.init_app(app)
2932

3033
with app.app_context():
3134
if "team_data" not in mongo.db.list_collection_names():
@@ -72,6 +75,17 @@ def serve_service_worker():
7275
response.headers["Content-Type"] = "application/javascript"
7376
response.headers["Service-Worker-Allowed"] = "/"
7477
return response
78+
79+
@app.errorhandler(404)
80+
def not_found(e):
81+
return render_template("404.html")
82+
83+
@app.errorhandler(Exception)
84+
def handle_exception(e):
85+
app.logger.error(f"Unhandled exception: {str(e)}", exc_info=True)
86+
return jsonify({
87+
"error": "An unexpected error occurred"
88+
}), 500
7589

7690
return app
7791

Diff for: app/auth/auth_utils.py

+62-73
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,18 @@
1-
from pymongo import MongoClient
2-
from pymongo.errors import ServerSelectionTimeoutError, ConnectionFailure
3-
from werkzeug.security import generate_password_hash
4-
from datetime import datetime, timezone
5-
from app.models import User
1+
from __future__ import annotations
2+
63
import logging
7-
import time
8-
from functools import wraps
4+
from datetime import datetime, timezone
5+
6+
from flask_login import current_user
97
from gridfs import GridFS
8+
from werkzeug.security import generate_password_hash
9+
10+
from app.models import User
11+
from app.utils import DatabaseManager, allowed_file, with_mongodb_retry
1012

1113
logging.basicConfig(level=logging.INFO)
1214
logger = logging.getLogger(__name__)
1315

14-
15-
def with_mongodb_retry(retries=3, delay=2):
16-
def decorator(f):
17-
@wraps(f)
18-
async def wrapper(*args, **kwargs):
19-
last_error = None
20-
for attempt in range(retries):
21-
try:
22-
return await f(*args, **kwargs)
23-
except (ServerSelectionTimeoutError, ConnectionFailure) as e:
24-
last_error = e
25-
if attempt < retries - 1: # don't sleep on last attempt
26-
logger.warning(f"Attempt {attempt + 1} failed: {str(e)}.")
27-
time.sleep(delay)
28-
else:
29-
logger.error(f"All {retries} attempts failed: {str(e)}")
30-
raise last_error
31-
32-
return wrapper
33-
34-
return decorator
35-
36-
3716
async def check_password_strength(password):
3817
"""
3918
Check if password meets minimum requirements:
@@ -44,42 +23,16 @@ async def check_password_strength(password):
4423
return True, "Password meets all requirements"
4524

4625

47-
class UserManager:
26+
class UserManager(DatabaseManager):
4827
def __init__(self, mongo_uri):
49-
self.mongo_uri = mongo_uri
50-
self.client = None
51-
self.db = None
52-
self.connect()
28+
super().__init__(mongo_uri)
29+
self._ensure_collections()
5330

54-
def connect(self):
55-
"""Establish connection to MongoDB with basic error handling"""
56-
try:
57-
if self.client is None:
58-
self.client = MongoClient(self.mongo_uri, serverSelectionTimeoutMS=5000)
59-
# Test the connection
60-
self.client.server_info()
61-
self.db = self.client.get_default_database()
62-
logger.info("Successfully connected to MongoDB")
63-
64-
# Ensure users collection exists
65-
if "users" not in self.db.list_collection_names():
66-
self.db.create_collection("users")
67-
logger.info("Created users collection")
68-
except Exception as e:
69-
logger.error(f"Failed to connect to MongoDB: {str(e)}")
70-
raise
71-
72-
def ensure_connected(self):
73-
"""Ensure we have a valid connection, reconnect if necessary"""
74-
try:
75-
if self.client is None:
76-
self.connect()
77-
else:
78-
# Test if connection is still alive
79-
self.client.server_info()
80-
except Exception:
81-
logger.warning("Lost connection to MongoDB, attempting to reconnect...")
82-
self.connect()
31+
def _ensure_collections(self):
32+
"""Ensure required collections exist"""
33+
if "users" not in self.db.list_collection_names():
34+
self.db.create_collection("users")
35+
logger.info("Created users collection")
8336

8437
@with_mongodb_retry(retries=3, delay=2)
8538
async def create_user(
@@ -123,7 +76,7 @@ async def create_user(
12376

12477
except Exception as e:
12578
logger.error(f"Error creating user: {str(e)}")
126-
return False, f"Error creating user: {str(e)}"
79+
return False, "An internal error has occurred."
12780

12881
@with_mongodb_retry(retries=3, delay=2)
12982
async def authenticate_user(self, login, password):
@@ -191,7 +144,7 @@ async def update_user_profile(self, user_id, updates):
191144

192145
except Exception as e:
193146
logger.error(f"Error updating profile: {str(e)}")
194-
return False, f"Error updating profile: {str(e)}"
147+
return False, "An internal error has occurred."
195148

196149
def get_user_profile(self, username):
197150
"""Get user profile by username"""
@@ -210,7 +163,7 @@ async def update_profile_picture(self, user_id, file_id):
210163
try:
211164
from bson.objectid import ObjectId
212165
from gridfs import GridFS
213-
166+
214167
# Get the old profile picture ID first
215168
user_data = self.db.users.find_one({"_id": ObjectId(user_id)})
216169
old_picture_id = user_data.get('profile_picture_id') if user_data else None
@@ -235,7 +188,7 @@ async def update_profile_picture(self, user_id, file_id):
235188

236189
except Exception as e:
237190
logger.error(f"Error updating profile picture: {str(e)}")
238-
return False, f"Error updating profile picture: {str(e)}"
191+
return False, "An internal error has occurred."
239192

240193
def get_profile_picture(self, user_id):
241194
"""Get user's profile picture ID"""
@@ -277,9 +230,45 @@ async def delete_user(self, user_id):
277230

278231
except Exception as e:
279232
logger.error(f"Error deleting user: {str(e)}")
280-
return False, f"Error deleting account: {str(e)}"
233+
return False, "An internal error has occurred."
281234

282-
def __del__(self):
283-
"""Cleanup MongoDB connection"""
284-
if self.client:
285-
self.client.close()
235+
@with_mongodb_retry(retries=3, delay=2)
236+
async def update_user_settings(self, user_id, form_data, profile_picture=None):
237+
"""Update user settings including profile picture"""
238+
self.ensure_connected()
239+
try:
240+
updates = {}
241+
242+
# Handle username update if provided
243+
if new_username := form_data.get('username'):
244+
if new_username != current_user.username:
245+
# Check if username is taken
246+
if self.db.users.find_one({"username": new_username}):
247+
return False
248+
updates['username'] = new_username
249+
250+
# Handle description update
251+
if description := form_data.get('description'):
252+
updates['description'] = description
253+
254+
# Handle profile picture
255+
if profile_picture:
256+
from werkzeug.utils import secure_filename
257+
if profile_picture and allowed_file(profile_picture.filename):
258+
fs = GridFS(self.db)
259+
filename = secure_filename(profile_picture.filename)
260+
file_id = fs.put(
261+
profile_picture.stream.read(),
262+
filename=filename,
263+
content_type=profile_picture.content_type
264+
)
265+
updates['profile_picture_id'] = file_id
266+
267+
if updates:
268+
success, message = await self.update_user_profile(user_id, updates)
269+
return success
270+
271+
return True, "Profile updated successfully"
272+
except Exception as e:
273+
logger.error(f"Error updating user settings: {str(e)}")
274+
return False, "An internal error has occurred."

0 commit comments

Comments
 (0)