399 lines
14 KiB
Python
399 lines
14 KiB
Python
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/<int:participant_id>', 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/<int:participant_id>', 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/<int:log_id>', 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)
|