from flask import Flask, render_template, request, jsonify, session from flask_cors import CORS import json from datetime import datetime from data_manager import DataManager import os from functools import wraps app = Flask(__name__, static_folder='static', template_folder='templates') CORS(app) app.secret_key = os.environ.get('SECRET_KEY', 'rfid-detection-secret-key-change-in-production') # Create data directory if it doesn't exist os.makedirs('data', exist_ok=True) # Global variables data_manager = DataManager("data/participants.json", "data/scan_logs.json") last_scan_result = None # Admin credentials (change this to your desired password) ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'Pleterski') def require_admin(f): """Decorator to require admin authentication""" @wraps(f) def decorated_function(*args, **kwargs): if 'admin' not in session or not session['admin']: return jsonify({'success': False, 'message': 'Unauthorized'}), 401 return f(*args, **kwargs) return decorated_function def process_card(uid): """Process scanned card or key""" global last_scan_result participant = data_manager.get_participant_by_card(uid) # Get card and key UIDs from participant card_uid = participant.get('card_uid') if participant else None key_uid = participant.get('key_uid') if participant else None if participant and not data_manager.is_card_expired(participant): data_manager.log_scan(uid, "PASS", participant['name'], card_uid=card_uid, key_uid=key_uid) last_scan_result = { 'uid': uid, 'name': participant['name'], 'status': 'PASS', 'color': 'green', 'timestamp': datetime.now().isoformat() } else: # FAIL if participant not found or card is expired name = participant['name'] if participant else "Unknown" reason = "Expired" if participant and data_manager.is_card_expired(participant) else "Unknown" # For FAIL scans, log the scanned UID as card_uid data_manager.log_scan(uid, "FAIL", name, reason, card_uid=uid, key_uid=None) last_scan_result = { 'uid': uid, 'name': name if participant else 'Unknown', 'status': 'FAIL', 'color': 'red', 'timestamp': datetime.now().isoformat() } return last_scan_result # ===== AUTHENTICATION ROUTES ===== @app.route('/api/login', methods=['POST']) def login(): """Admin login""" try: data = request.json password = data.get('password', '').strip() if password == ADMIN_PASSWORD: session['admin'] = True return jsonify({'success': True, 'message': 'Logged in successfully'}) else: return jsonify({'success': False, 'message': 'Invalid password'}), 401 except Exception as e: return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/api/logout', methods=['POST']) def logout(): """Admin logout""" session.pop('admin', None) return jsonify({'success': True, 'message': 'Logged out successfully'}) @app.route('/api/check-admin', methods=['GET']) def check_admin(): """Check if user is logged in as admin""" is_admin = 'admin' in session and session['admin'] return jsonify({'is_admin': is_admin}) # ===== API Routes ===== @app.route('/') def index(): return render_template('index.html') @app.route('/api/scan', methods=['POST']) def receive_wifi_scan(): """Receive card scan from WiFi-enabled ESP8266""" try: data = request.json uid = data.get('uid', '').strip() if not uid: return jsonify({'success': False, 'message': 'No UID provided'}), 400 print(f"[WiFi Scan] Card scanned via WiFi: {uid}") result = process_card(uid) print(f"[WiFi Scan] Result: {result}") return jsonify({'success': True, 'result': result}) except Exception as e: print(f"[WiFi Scan Error] {str(e)}") return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/scan', methods=['POST']) def receive_wifi_scan_root(): """Fallback route for /scan (without /api prefix)""" try: data = request.json uid = data.get('uid', '').strip() if not uid: return jsonify({'success': False, 'message': 'No UID provided'}), 400 print(f"[WiFi Scan Root] Card scanned via WiFi: {uid}") result = process_card(uid) print(f"[WiFi Scan Root] Result: {result}") return jsonify({'success': True, 'result': result}) except Exception as e: print(f"[WiFi Scan Root Error] {str(e)}") return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/api/participants', methods=['GET']) def get_participants(): """Get all participants""" participants = data_manager.get_all_participants() return jsonify(participants) @app.route('/api/participants', methods=['POST']) @require_admin def add_participant(): """Add a new participant (admin only)""" try: data = request.json if not data: return jsonify({'success': False, 'message': 'Invalid JSON'}), 400 name = (data.get('name') or '').strip() card_uid = (data.get('card_uid') or '').strip() or None key_uid = (data.get('key_uid') or '').strip() or None expiration_date = (data.get('expiration_date') or '').strip() or None if not name: return jsonify({'success': False, 'message': 'Name is required'}), 400 if not card_uid and not key_uid: return jsonify({'success': False, 'message': 'At least one of card UID or key UID is required'}), 400 if data_manager.add_participant(name, card_uid, expiration_date, key_uid): return jsonify({'success': True, 'message': 'Participant added'}) else: return jsonify({'success': False, 'message': 'Duplicate UID or error'}), 400 except Exception as e: print(f"Error adding participant: {e}") return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/api/participants/', methods=['PUT']) @require_admin def update_participant(participant_id): """Update a participant (admin only)""" try: data = request.json if not data: return jsonify({'success': False, 'message': 'Invalid JSON'}), 400 name = (data.get('name') or '').strip() card_uid = (data.get('card_uid') or '').strip() or None key_uid = (data.get('key_uid') or '').strip() or None expiration_date = (data.get('expiration_date') or '').strip() or None if not name: return jsonify({'success': False, 'message': 'Name is required'}), 400 if not card_uid and not key_uid: return jsonify({'success': False, 'message': 'At least one of card UID or key UID is required'}), 400 if data_manager.update_participant(participant_id, name, card_uid, expiration_date, key_uid): return jsonify({'success': True, 'message': 'Participant updated'}) else: return jsonify({'success': False, 'message': 'Update failed'}), 400 except Exception as e: print(f"Error updating participant: {e}") return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/api/participants/', methods=['DELETE']) @require_admin def delete_participant(participant_id): """Delete a participant (admin only)""" if data_manager.delete_participant(participant_id): return jsonify({'success': True, 'message': 'Participant deleted'}) else: return jsonify({'success': False, 'message': 'Delete failed'}), 400 @app.route('/api/logs', methods=['GET']) def get_logs(): """Get scan logs""" filter_type = request.args.get('filter', 'All') logs = data_manager.get_scan_history(1000) if filter_type == 'PASS': logs = [log for log in logs if log['scan_result'] == 'PASS'] elif filter_type == 'FAIL': logs = [log for log in logs if log['scan_result'] == 'FAIL'] return jsonify(logs) @app.route('/api/logs/unregistered', methods=['POST']) def add_unregistered_log(): """Add unregistered user to logs""" try: data = request.json if not data: return jsonify({'success': False, 'message': 'Invalid JSON'}), 400 name = (data.get('name') or '').strip() timestamp = (data.get('timestamp') or '').strip() or None if not name: return jsonify({'success': False, 'message': 'Name is required'}), 400 if data_manager.log_unregistered_user(name, timestamp): return jsonify({'success': True, 'message': 'Unregistered user logged'}) else: return jsonify({'success': False, 'message': 'Error logging user'}), 400 except Exception as e: print(f"Error adding unregistered log: {e}") return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/api/logs/', methods=['DELETE']) @require_admin def delete_log(log_id): """Delete a log entry (admin only)""" try: if data_manager.delete_log(log_id): return jsonify({'success': True, 'message': 'Log deleted'}) else: return jsonify({'success': False, 'message': 'Log not found'}), 404 except Exception as e: print(f"Error deleting log: {e}") return jsonify({'success': False, 'message': str(e)}), 400 @app.route('/api/export', methods=['GET']) def export_logs(): """Export logs to Excel - returns file data for client-side saving""" try: import openpyxl from openpyxl.styles import PatternFill, Font, Alignment import io import base64 from datetime import datetime as dt logs = data_manager.get_scan_history(10000) # Apply filters from query parameters filter_type = request.args.get('filter', 'All') participant_filter = request.args.get('participant', '') start_date = request.args.get('startDate', '') end_date = request.args.get('endDate', '') # Filter by result type if filter_type == 'PASS': logs = [log for log in logs if log['scan_result'] == 'PASS'] elif filter_type == 'FAIL': logs = [log for log in logs if log['scan_result'] == 'FAIL'] # Filter by participant if participant_filter: logs = [log for log in logs if log.get('name') == participant_filter] # Filter by date range if start_date: start_dt = dt.strptime(start_date, '%Y-%m-%d') logs = [log for log in logs if dt.fromisoformat(log['timestamp'].replace('Z', '+00:00')) >= start_dt] if end_date: end_dt = dt.strptime(end_date, '%Y-%m-%d') end_dt = end_dt.replace(hour=23, minute=59, second=59) logs = [log for log in logs if dt.fromisoformat(log['timestamp'].replace('Z', '+00:00')) <= end_dt] if not logs: return jsonify({'success': False, 'message': 'No logs to export'}), 400 wb = openpyxl.Workbook() ws = wb.active ws.title = "Scan Logs" headers = ["ID", "Name", "Card UID", "Key UID", "Result", "Date", "Time"] ws.append(headers) header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") header_font = Font(bold=True, color="FFFFFF") for cell in ws[1]: cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal="center", vertical="center") for log in logs: timestamp = log['timestamp'] date_part = timestamp[:10] time_part = timestamp[11:19] ws.append([ log['id'], log['name'], log['card_uid'], log.get('key_uid', ''), log['scan_result'], date_part, time_part ]) pass_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") pass_font = Font(color="006100") fail_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") fail_font = Font(color="9C0006") for idx, row in enumerate(ws.iter_rows(min_row=2, max_row=ws.max_row), start=2): result_cell = row[4] # Result is now column 5 (index 4) if result_cell.value == "PASS": result_cell.fill = pass_fill result_cell.font = pass_font else: result_cell.fill = fail_fill result_cell.font = fail_font ws.column_dimensions['A'].width = 8 ws.column_dimensions['B'].width = 20 ws.column_dimensions['C'].width = 20 ws.column_dimensions['D'].width = 20 ws.column_dimensions['E'].width = 10 ws.column_dimensions['F'].width = 12 ws.column_dimensions['G'].width = 10 # Generate filename timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"scan_logs_{timestamp}.xlsx" # Save to bytes buffer buffer = io.BytesIO() wb.save(buffer) buffer.seek(0) # Encode to base64 for transfer file_data = base64.b64encode(buffer.getvalue()).decode('utf-8') return jsonify({ 'success': True, 'filename': filename, 'data': file_data }) except Exception as e: return jsonify({'success': False, 'message': str(e)}), 500 @app.route('/api/stats', methods=['GET']) def get_stats(): """Get statistics""" stats = data_manager.get_statistics() return jsonify(stats) @app.route('/api/documents', methods=['GET']) def get_documents(): """List PDF files in static/PDF folder""" pdf_dir = os.path.join(app.static_folder, 'PDF') os.makedirs(pdf_dir, exist_ok=True) files = [f for f in os.listdir(pdf_dir) if f.lower().endswith('.pdf')] files.sort() return jsonify(files) @app.route('/api/last-scan', methods=['GET']) def get_last_scan(): """Get last scan result""" return jsonify(last_scan_result if last_scan_result else { 'uid': '-', 'name': '-', 'status': '-', 'color': 'gray' }) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)