Attender / app.py
chualinwei3's picture
Update app.py
4669b4f verified
Raw
History Blame Contribute Delete
34.8 kB
"""
Attendr - Smart Attendance System
Main Flask Application
"""
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash, send_from_directory
from werkzeug.middleware.proxy_fix import ProxyFix
from flask_sqlalchemy import SQLAlchemy
from config import Config
from models import db, User, Student, Classroom, AttendanceSession, AttendanceRecord
from face_recognition_module import face_recognition_module
from geolocation_module import geolocation_module
from attendance_code_module import attendance_code_module
from utils import base64_to_image, image_to_numpy, format_error_response, format_success_response
from auth import login_required, role_required, get_current_user
from datetime import datetime
import logging
# Initialize Flask app
app = Flask(__name__, template_folder='.')
app.config.from_object(Config)
Config.init_app(app)
# Trust proxy headers (needed for Hugging Face Spaces)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
# Initialize database
db.init_app(app)
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def migrate_database():
"""Manually add missing columns to the database if they don't exist"""
try:
from sqlalchemy import text
with db.engine.connect() as conn:
# Check if status column exists in users table
result = conn.execute(text("PRAGMA table_info(users)"))
columns = [row[1] for row in result]
if 'status' not in columns:
logger.info("Adding 'status' column to users table...")
conn.execute(text("ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'pending'"))
conn.commit()
logger.info("Column 'status' added successfully.")
except Exception as e:
logger.error(f"Migration error: {str(e)}")
# Ensure database tables are created (important for Hugging Face deployment)
with app.app_context():
db.create_all()
migrate_database()
# Auto-initialize Admin user
try:
admin = User.query.filter_by(role='admin').first()
if not admin:
admin = User(
matric_no='ADMIN',
name='System Admin',
email='admin@attendr.com',
role='admin',
is_active=True,
status='approved'
)
admin.set_password('admin123') # Default password
db.session.add(admin)
db.session.commit()
logger.info("Default Admin user created (ADMIN / admin123)")
except Exception as init_err:
logger.error(f"Admin initialization error: {str(init_err)}")
logger.info("Database initialized")
# Force fix any users who are pending but accidentally active
try:
inconsistent_users = User.query.filter_by(status='pending', is_active=True).all()
if inconsistent_users:
for u in inconsistent_users:
u.is_active = False
db.session.commit()
logger.info(f"Fixed {len(inconsistent_users)} inconsistent pending accounts.")
except Exception as e:
logger.error(f"Cleanup error: {str(e)}")
@app.after_request
def add_header(response):
"""
Add headers to prevent caching of dynamic pages.
This ensures that when a user logs out, they cannot click 'Back'
to see protected content from the browser cache.
"""
if 'Cache-Control' not in response.headers:
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response
# ==================== ROUTES ====================
@app.route('/style.css')
def style():
return send_from_directory('.', 'style.css')
@app.route('/')
def index():
"""Landing page"""
current_user = get_current_user()
return render_template('index.html', current_user=current_user)
@app.route('/student')
@login_required
@role_required('student')
def student_page():
"""Student attendance interface - requires student login"""
return render_template('student.html', current_user=get_current_user())
@app.route('/student/history')
@login_required
@role_required('student')
def student_history():
"""Student attendance history page"""
return render_template('student_history.html', current_user=get_current_user())
@app.route('/lecturer')
@login_required
@role_required('lecturer')
def lecturer_page():
"""Lecturer dashboard - requires lecturer login"""
return render_template('lecturer.html', current_user=get_current_user())
@app.route('/lecturer/history')
@login_required
@role_required('lecturer')
def lecturer_history():
"""Lecturer session history page"""
return render_template('lecturer_history.html', current_user=get_current_user())
@app.route('/admin')
@login_required
@role_required('admin')
def admin_page():
"""Admin dashboard - approve/reject users"""
return render_template('admin.html', current_user=get_current_user())
@app.route('/register')
def register_page():
"""User registration page - redirect if already logged in"""
current_user = get_current_user()
if current_user:
role = current_user.role
if role == 'admin': return redirect(url_for('admin_page'))
return redirect(url_for('student_page' if role == 'student' else 'lecturer_page'))
# Clear stale session if user doesn't exist
if 'user_id' in session:
session.clear()
return render_template('register.html')
@app.route('/login')
def login_page():
"""Login page - redirect if already logged in"""
current_user = get_current_user()
if current_user:
role = current_user.role
if role == 'admin': return redirect(url_for('admin_page'))
return redirect(url_for('student_page' if role == 'student' else 'lecturer_page'))
# Clear stale session if user doesn't exist
if 'user_id' in session:
session.clear()
return render_template('login.html')
# ==================== AUTHENTICATION API ====================
@app.route('/api/auth/register', methods=['POST'])
def register_user():
"""
Register new user (student or lecturer)
Validates matric number format based on role
"""
try:
data = request.get_json()
required_fields = ['matric_no', 'name', 'password', 'role']
if not all(k in data for k in required_fields):
return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
matric_no = data['matric_no'].upper().strip()
name = data['name']
password = data['password']
role = data['role'].lower()
email = data.get('email')
# Validate role
if role not in ['student', 'lecturer']:
return format_error_response("Role must be 'student' or 'lecturer'")
# Validate matric number format
is_valid, error_msg = User.validate_matric_no(matric_no, role)
if not is_valid:
return format_error_response(error_msg, 400)
# Check if user already exists
existing_user = User.query.filter_by(matric_no=matric_no).first()
if existing_user:
return format_error_response("Matric number already registered", 409)
if email:
existing_email = User.query.filter_by(email=email).first()
if existing_email:
return format_error_response("Email already registered", 409)
# Create user - explicitly set status and activity for Admin approval workflow
user = User(
matric_no=matric_no,
name=name,
email=email,
role=role,
is_active=False,
status='pending'
)
user.set_password(password)
db.session.add(user)
db.session.commit()
logger.info(f"User registered: {matric_no} ({role})")
return format_success_response(
data=user.to_dict(),
message=f"{role.capitalize()} account created successfully! Please wait for Admin approval before logging in."
)
except Exception as e:
db.session.rollback()
logger.error(f"User registration error: {str(e)}")
return format_error_response(f"Registration failed: {str(e)}", 500)
@app.route('/api/auth/login', methods=['POST'])
def login():
"""Login user and create session"""
try:
data = request.get_json()
if not all(k in data for k in ['matric_no', 'password']):
return format_error_response("Missing required fields: matric_no, password")
matric_no = data['matric_no'].upper().strip()
password = data['password']
# Find user
user = User.query.filter_by(matric_no=matric_no).first()
if not user or not user.check_password(password):
return format_error_response("Invalid matric number or password", 401)
if not user.is_active:
if user.status == 'pending':
return format_error_response("Your account is pending approval by an Admin.", 403)
elif user.status == 'rejected':
return format_error_response("Your account registration has been rejected.", 403)
else:
return format_error_response("Account is inactive. Please contact admin.", 403)
# Create session
session['user_id'] = user.id
session['matric_no'] = user.matric_no
session['role'] = user.role
session['name'] = user.name
logger.info(f"User logged in: {matric_no} ({user.role})")
return format_success_response(
data={
'user': user.to_dict(),
'redirect': '/admin' if user.role == 'admin' else ('/student' if user.role == 'student' else '/lecturer')
},
message=f"Welcome back, {user.name}!"
)
except Exception as e:
logger.error(f"Login error: {str(e)}")
return format_error_response(f"Login failed: {str(e)}", 500)
@app.route('/api/auth/logout', methods=['POST'])
@login_required
def logout():
"""Logout user and destroy session"""
try:
user_name = session.get('name', 'User')
matric_no = session.get('matric_no', 'Unknown')
# Clear all session data
session.clear()
logger.info(f"User logged out: {matric_no}")
return format_success_response(
message=f"Goodbye, {user_name}! You have been logged out successfully."
)
except Exception as e:
logger.error(f"Logout error: {str(e)}")
return format_error_response(f"Logout failed: {str(e)}", 500)
@app.route('/api/auth/current_user', methods=['GET'])
def get_current_user_api():
"""Get current logged in user"""
user = get_current_user()
if user:
return format_success_response(data=user.to_dict())
return format_error_response("Not logged in", 401)
# ==================== API ENDPOINTS ====================
@app.route('/api/register_face', methods=['POST'])
def register_face():
"""
Register student face encoding
Implements: PB-1 (Register Student Face Data)
"""
try:
data = request.get_json()
# Validate input
if not all(k in data for k in ['student_id', 'name', 'image']):
return format_error_response("Missing required fields: student_id, name, image")
student_id = data['student_id']
name = data['name']
email = data.get('email')
image_base64 = data['image']
# Check if student already exists
existing_student = Student.query.filter_by(student_id=student_id).first()
if existing_student:
return format_error_response("Student ID already registered", 409)
# Get user account (if exists)
user = User.query.filter_by(matric_no=student_id).first()
# Convert base64 to image
try:
image = base64_to_image(image_base64)
image_array = image_to_numpy(image)
except Exception as e:
return format_error_response(f"Invalid image data: {str(e)}")
# Generate face encoding
success, result, face_location = face_recognition_module.register_face(image_array)
if not success:
return format_error_response(result)
# Create student record
student = Student(
student_id=student_id,
name=name,
email=email,
user_id=user.id if user else None # Link to user account if exists
)
student.set_face_encoding(result)
db.session.add(student)
db.session.commit()
logger.info(f"Student registered: {student_id} - {name}")
return format_success_response(
data=student.to_dict(),
message="Face registered successfully!"
)
except Exception as e:
db.session.rollback()
logger.error(f"Registration error: {str(e)}")
return format_error_response(f"Registration failed: {str(e)}", 500)
@app.route('/api/verify_face', methods=['POST'])
def verify_face():
"""
Verify student face against stored encoding
Implements: PB-3 (Face Recognition Matching)
Implements KR Rule #1: ∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))
"""
try:
data = request.get_json()
if not all(k in data for k in ['student_id', 'image']):
return format_error_response("Missing required fields: student_id, image")
student_id = data['student_id']
image_base64 = data['image']
# Get student record
student = Student.query.filter_by(student_id=student_id).first()
if not student:
return format_error_response("Student not found. Please register first.", 404)
if not student.is_active:
return format_error_response("Student account is inactive", 403)
# Get stored encoding
stored_encoding = student.get_face_encoding()
if not stored_encoding:
return format_error_response("No face encoding found. Please register your face.", 404)
# Convert base64 to image
try:
image = base64_to_image(image_base64)
image_array = image_to_numpy(image)
except Exception as e:
return format_error_response(f"Invalid image data: {str(e)}")
# Generate encoding from captured image
success, result = face_recognition_module.generate_face_encoding(image_array)
if not success:
return format_error_response(result)
captured_encoding = result
# Verify face match
is_match, confidence = face_recognition_module.verify_face(captured_encoding, stored_encoding)
if is_match:
logger.info(f"Face verified for student {student_id}: confidence={confidence:.2f}%")
return format_success_response(
data={
'verified': True,
'confidence': round(confidence, 2),
'student': student.to_dict()
},
message=f"Face verified! Confidence: {confidence:.1f}%"
)
else:
logger.warning(f"Face verification failed for {student_id}: confidence={confidence:.2f}%")
return format_error_response(
f"Face verification failed. Confidence too low: {confidence:.1f}%",
401
)
except Exception as e:
logger.error(f"Face verification error: {str(e)}")
return format_error_response(f"Verification failed: {str(e)}", 500)
@app.route('/api/verify_location', methods=['POST'])
def verify_location():
"""
Verify student location against classroom boundaries
Implements: PB-5 (Geofencing Logic – Classroom Detection)
Implements KR Rule #2: ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]
"""
try:
data = request.get_json()
if not all(k in data for k in ['latitude', 'longitude', 'session_id']):
return format_error_response("Missing required fields: latitude, longitude, session_id")
latitude = data['latitude']
longitude = data['longitude']
session_id = data['session_id']
# Get session
session_obj = AttendanceSession.query.get(session_id)
if not session_obj:
return format_error_response("Session not found", 404)
if not session_obj.is_active:
return format_error_response("Session is not active", 403)
# Verify location
result = geolocation_module.verify_location(
latitude,
longitude,
session_obj.classroom_id
)
if result['verified']:
logger.info(f"Location verified: distance={result['distance']:.2f}m")
return format_success_response(
data={
'verified': True,
'distance': round(result['distance'], 2),
'classroom': session_obj.classroom.to_dict()
},
message=result['message']
)
else:
logger.warning(f"Location verification failed: {result['error']}")
return format_error_response(result['error'] or result['message'], 403)
except Exception as e:
logger.error(f"Location verification error: {str(e)}")
return format_error_response(f"Location verification failed: {str(e)}", 500)
@app.route('/api/mark_attendance', methods=['POST'])
def mark_attendance():
"""
Mark student attendance after all verifications
Implements: PB-12 (Mark Attendance in Database)
Implements KR Rule #4: ∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))
Implements KR Rule #5: ∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))
"""
try:
data = request.get_json()
required_fields = ['student_id', 'session_id', 'code', 'latitude', 'longitude']
if not all(k in data for k in required_fields):
return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
student_id = data['student_id']
session_id = data['session_id']
code = data['code']
latitude = data['latitude']
longitude = data['longitude']
# Get student
student = Student.query.filter_by(student_id=student_id).first()
if not student:
return format_error_response("Student not found", 404)
# Get session
session_obj = AttendanceSession.query.get(session_id)
if not session_obj:
return format_error_response("Session not found", 404)
if not session_obj.is_active:
return format_error_response("Session is not active", 403)
# Check if already marked
existing_record = AttendanceRecord.query.filter_by(
session_id=session_id,
student_id=student.id
).first()
if existing_record:
return format_error_response("Attendance already marked for this session", 409)
# Validate code
is_valid, message = attendance_code_module.is_code_valid(session_id, code)
if not is_valid:
return format_error_response(message, 401)
# Verify location one more time
location_result = geolocation_module.verify_location(
latitude,
longitude,
session_obj.classroom_id
)
if not location_result['verified']:
return format_error_response(location_result['error'] or "Location verification failed", 403)
# Create attendance record
attendance = AttendanceRecord(
session_id=session_id,
student_id=student.id,
face_verified=True, # Assumed verified before reaching this endpoint
location_verified=True,
code_verified=True,
student_latitude=float(latitude),
student_longitude=float(longitude),
distance_from_classroom=location_result['distance'],
status='present'
)
db.session.add(attendance)
db.session.commit()
logger.info(f"Attendance marked: {student.name} for session {session_id}")
return format_success_response(
data=attendance.to_dict(),
message="Attendance marked successfully! ✓"
)
except Exception as e:
db.session.rollback()
logger.error(f"Mark attendance error: {str(e)}")
return format_error_response(f"Failed to mark attendance: {str(e)}", 500)
@app.route('/api/create_session', methods=['POST'])
def create_session():
"""
Create new attendance session
Implements: US-06 (Generate Auto-Refresh Codes)
"""
try:
data = request.get_json()
if not all(k in data for k in ['course_name', 'classroom_id', 'lecturer_name']):
return format_error_response("Missing required fields: course_name, classroom_id, lecturer_name")
course_name = data['course_name']
classroom_id = data['classroom_id']
lecturer_name = data['lecturer_name']
# Verify classroom exists
classroom = Classroom.query.get(classroom_id)
if not classroom:
return format_error_response("Classroom not found", 404)
# Create session
session_obj = AttendanceSession(
course_name=course_name,
classroom_id=classroom_id,
lecturer_name=lecturer_name,
is_active=True
)
db.session.add(session_obj)
db.session.commit()
# Generate initial code
success, code = attendance_code_module.create_session_code(session_obj.id)
if not success:
return format_error_response(f"Session created but code generation failed: {code}")
logger.info(f"Session created: {course_name} by {lecturer_name}")
return format_success_response(
data=session_obj.to_dict(),
message="Session created successfully!"
)
except Exception as e:
db.session.rollback()
logger.error(f"Create session error: {str(e)}")
return format_error_response(f"Failed to create session: {str(e)}", 500)
@app.route('/api/get_session/<int:session_id>', methods=['GET'])
def get_session(session_id):
"""Get session details including current code"""
try:
session_obj = AttendanceSession.query.get(session_id)
if not session_obj:
return format_error_response("Session not found", 404)
# Get code status
code_status = attendance_code_module.get_code_status(session_id)
# Auto-refresh if needed
if code_status.get('needs_refresh'):
attendance_code_module.auto_refresh_code(session_id)
code_status = attendance_code_module.get_code_status(session_id)
session_data = session_obj.to_dict()
session_data['code_status'] = code_status
return format_success_response(data=session_data)
except Exception as e:
logger.error(f"Get session error: {str(e)}")
return format_error_response(f"Failed to get session: {str(e)}", 500)
@app.route('/api/get_active_sessions', methods=['GET'])
def get_active_sessions():
"""Get all active sessions"""
try:
sessions = AttendanceSession.query.filter_by(is_active=True).all()
return format_success_response(
data=[session.to_dict() for session in sessions]
)
except Exception as e:
logger.error(f"Get active sessions error: {str(e)}")
return format_error_response(f"Failed to get sessions: {str(e)}", 500)
@app.route('/api/get_attendance/<int:session_id>', methods=['GET'])
def get_attendance(session_id):
"""
Get attendance records for a session
Implements: US-07 (View Real-time Attendance)
"""
try:
session_obj = AttendanceSession.query.get(session_id)
if not session_obj:
return format_error_response("Session not found", 404)
records = AttendanceRecord.query.filter_by(session_id=session_id).all()
return format_success_response(
data={
'session': session_obj.to_dict(),
'records': [record.to_dict() for record in records],
'total_present': len(records)
}
)
except Exception as e:
logger.error(f"Get attendance error: {str(e)}")
return format_error_response(f"Failed to get attendance: {str(e)}", 500)
@app.route('/api/end_session/<int:session_id>', methods=['POST'])
def end_session(session_id):
"""End an attendance session"""
try:
session_obj = AttendanceSession.query.get(session_id)
if not session_obj:
return format_error_response("Session not found", 404)
session_obj.is_active = False
session_obj.end_time = datetime.utcnow()
db.session.commit()
logger.info(f"Session ended: {session_id}")
return format_success_response(message="Session ended successfully")
except Exception as e:
db.session.rollback()
logger.error(f"End session error: {str(e)}")
return format_error_response(f"Failed to end session: {str(e)}", 500)
@app.route('/api/student/history', methods=['GET'])
@login_required
@role_required('student')
def get_student_history():
"""Get attendance history for logged-in student"""
try:
# Get current user's student profile
current_user = get_current_user()
student = Student.query.filter_by(student_id=current_user.matric_no).first()
if not student:
return format_error_response("Student profile not found", 404)
# Get all attendance records for this student
records = AttendanceRecord.query.filter_by(student_id=student.id)\
.order_by(AttendanceRecord.marked_at.desc()).all()
# Format response with session details
history = []
for record in records:
session = record.session
history.append({
'id': record.id,
'course_name': session.course_name,
'lecturer_name': session.lecturer_name,
'classroom': session.classroom.name,
'building': session.classroom.building,
'status': record.status,
'marked_at': record.marked_at.isoformat() if record.marked_at else None,
'session_date': session.start_time.isoformat() if session.start_time else None,
'face_verified': record.face_verified,
'location_verified': record.location_verified,
'code_verified': record.code_verified,
'distance_from_classroom': round(record.distance_from_classroom, 2) if record.distance_from_classroom else None
})
return format_success_response(
data={
'student': student.to_dict(),
'total_records': len(history),
'history': history
}
)
except Exception as e:
logger.error(f"Get student history error: {str(e)}")
return format_error_response(f"Failed to get history: {str(e)}", 500)
@app.route('/api/lecturer/history', methods=['GET'])
@login_required
@role_required('lecturer')
def get_lecturer_history():
"""Get session history for logged-in lecturer"""
try:
current_user = get_current_user()
# Get all sessions created by this lecturer (case-insensitive match by name)
from sqlalchemy import func
sessions = AttendanceSession.query.filter(
func.lower(AttendanceSession.lecturer_name) == func.lower(current_user.name)
).order_by(AttendanceSession.start_time.desc()).all()
logger.info(f"Fetched {len(sessions)} sessions for lecturer: {current_user.name}")
# Format response with attendance statistics
history = []
for session in sessions:
# Count attendance records
total_attendance = len(session.attendance_records)
present_count = sum(1 for r in session.attendance_records if r.status == 'present')
# Calculate session duration
if session.end_time and session.start_time:
duration_minutes = int((session.end_time - session.start_time).total_seconds() / 60)
elif session.is_active and session.start_time:
duration_minutes = int((datetime.utcnow() - session.start_time).total_seconds() / 60)
else:
duration_minutes = 0
history.append({
'id': session.id,
'course_name': session.course_name,
'lecturer_name': session.lecturer_name,
'classroom': session.classroom.name,
'building': session.classroom.building,
'start_time': session.start_time.isoformat() if session.start_time else None,
'end_time': session.end_time.isoformat() if session.end_time else None,
'is_active': session.is_active,
'duration_minutes': duration_minutes,
'total_attendance': total_attendance,
'present_count': present_count,
'attendance_records': [
{
'student_id': r.student.student_id if r.student else 'Unknown',
'student_name': r.student.name if r.student else 'Unknown',
'status': r.status,
'marked_at': r.marked_at.isoformat() if r.marked_at else None,
'face_verified': r.face_verified,
'location_verified': r.location_verified,
'code_verified': r.code_verified
} for r in session.attendance_records
]
})
return format_success_response(
data={
'lecturer': {
'name': current_user.name,
'matric_no': current_user.matric_no
},
'total_sessions': len(history),
'history': history
}
)
except Exception as e:
logger.error(f"Get lecturer history error: {str(e)}")
return format_error_response(f"Failed to get history: {str(e)}", 500)
@app.route('/api/classrooms', methods=['GET'])
def get_classrooms():
"""Get all classrooms"""
try:
classrooms = geolocation_module.get_all_classrooms()
return format_success_response(data=classrooms)
except Exception as e:
logger.error(f"Get classrooms error: {str(e)}")
return format_error_response(f"Failed to get classrooms: {str(e)}", 500)
@app.route('/api/admin/users', methods=['GET'])
@login_required
@role_required('admin')
def admin_get_users():
"""Get list of users for admin management"""
try:
status = request.args.get('status', 'pending')
query = User.query
if status != 'all':
query = query.filter_by(status=status)
users = query.order_by(User.created_at.desc()).all()
return format_success_response(data=[u.to_dict() for u in users])
except Exception as e:
logger.error(f"Admin get users error: {str(e)}")
return format_error_response(f"Failed to get users: {str(e)}", 500)
@app.route('/api/admin/users/<int:user_id>/approve', methods=['POST'])
@login_required
@role_required('admin')
def admin_approve_user(user_id):
"""Approve a user registration"""
try:
user = User.query.get(user_id)
if not user:
return format_error_response("User not found", 404)
user.status = 'approved'
user.is_active = True
db.session.commit()
logger.info(f"Admin approved user: {user.matric_no}")
return format_success_response(message=f"User {user.matric_no} approved successfully.")
except Exception as e:
db.session.rollback()
logger.error(f"Admin approve user error: {str(e)}")
return format_error_response(f"Failed to approve user: {str(e)}", 500)
@app.route('/api/admin/users/<int:user_id>/reject', methods=['POST'])
@login_required
@role_required('admin')
def admin_reject_user(user_id):
"""Reject a user registration"""
try:
user = User.query.get(user_id)
if not user:
return format_error_response("User not found", 404)
user.status = 'rejected'
user.is_active = False
db.session.commit()
logger.info(f"Admin rejected user: {user.matric_no}")
return format_success_response(message=f"User {user.matric_no} rejected.")
except Exception as e:
db.session.rollback()
logger.error(f"Admin reject user error: {str(e)}")
return format_error_response(f"Failed to reject user: {str(e)}", 500)
# ==================== ERROR HANDLERS ====================
@app.errorhandler(404)
def not_found(error):
return format_error_response("Resource not found", 404)
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return format_error_response("Internal server error", 500)
# ==================== MAIN ====================
if __name__ == '__main__':
with app.app_context():
db.create_all()
logger.info("Database tables created")
app.run(debug=True, host='0.0.0.0', port=5000)