kkt-2002 commited on
Commit
c08bf91
·
1 Parent(s): a6e8527

Critical security fixes: prevent cross-user attendance + stable face detection

Browse files
Files changed (1) hide show
  1. app.py +187 -586
app.py CHANGED
@@ -5,7 +5,7 @@ import gc
5
  import logging
6
  import time
7
  import uuid
8
- import secrets # Added for token generation
9
  import pymongo
10
  from pymongo import MongoClient
11
  from bson.binary import Binary
@@ -51,15 +51,15 @@ app.config.update({
51
  'PERMANENT_SESSION_LIFETIME': timedelta(hours=2),
52
  'SESSION_COOKIE_NAME': 'face_app_session',
53
  'SESSION_COOKIE_HTTPONLY': True,
54
- 'SESSION_COOKIE_SECURE': False, # Hugging Face uses HTTP internally
55
- 'SESSION_COOKIE_SAMESITE': None, # Critical for Hugging Face iframe
56
- 'SESSION_REFRESH_EACH_REQUEST': False, # Prevent session reset
57
  'SESSION_COOKIE_DOMAIN': None,
58
  'SESSION_COOKIE_PATH': '/',
59
- 'SEND_FILE_MAX_AGE_DEFAULT': 0 # Disable caching
60
  })
61
 
62
- # CRITICAL FIX: Add ProxyFix middleware for Hugging Face reverse proxy
63
  app.wsgi_app = ProxyFix(
64
  app.wsgi_app,
65
  x_for=1,
@@ -99,7 +99,7 @@ try:
99
  teachers_collection = db['teachers']
100
  attendance_collection = db['attendance']
101
  metrics_events = db['metrics_events']
102
- sessions_collection = db['user_sessions'] # Added for token-based sessions
103
 
104
  # Create indexes for better performance
105
  students_collection.create_index([("student_id", pymongo.ASCENDING)], unique=True)
@@ -118,9 +118,7 @@ try:
118
  except Exception as e:
119
  print(f"MongoDB connection error: {e}")
120
 
121
- # ---------------- Memory-Optimized Face Detection ----------------
122
-
123
- # Initialize Haar Cascade Face Detector (lightweight and reliable)
124
  face_detector = None
125
  try:
126
  face_detector = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
@@ -128,7 +126,6 @@ try:
128
  except Exception as e:
129
  print(f"Error initializing face detector: {e}")
130
 
131
- # Initialize eye cascade for liveness detection
132
  eye_cascade = None
133
  try:
134
  eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
@@ -160,7 +157,6 @@ def detect_faces_haar(image):
160
  "score": 0.9
161
  })
162
 
163
- # Clean up memory
164
  del gray
165
  gc.collect()
166
  return detections
@@ -173,7 +169,6 @@ def detect_faces_yunet(image):
173
  if face_detector is not None:
174
  return detect_faces_haar(image)
175
 
176
- # Final fallback
177
  try:
178
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
179
  face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
@@ -194,7 +189,7 @@ def detect_faces_yunet(image):
194
  return []
195
 
196
  def recognize_face_deepface(image, user_id, user_type='student'):
197
- """Enhanced face recognition with better detector and alignment"""
198
  global total_attempts, correct_recognitions, unauthorized_attempts, inference_times
199
 
200
  temp_files = []
@@ -204,10 +199,27 @@ def recognize_face_deepface(image, user_id, user_type='student'):
204
 
205
  start_time = time.time()
206
 
207
- # Save current image temporarily
 
 
 
 
 
 
 
 
 
208
  temp_img_path = get_unique_temp_path(f"current_{user_id}")
209
  temp_files.append(temp_img_path)
210
- cv2.imwrite(temp_img_path, image)
 
 
 
 
 
 
 
 
211
 
212
  # Get user's reference image
213
  if user_type == 'student':
@@ -219,39 +231,49 @@ def recognize_face_deepface(image, user_id, user_type='student'):
219
  unauthorized_attempts += 1
220
  return False, f"No reference face found for {user_type} ID {user_id}"
221
 
222
- # Save reference image temporarily
223
  ref_image_bytes = user['face_image']
224
  ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
225
  ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
 
 
 
 
226
  temp_ref_path = get_unique_temp_path(f"ref_{user_id}")
227
  temp_files.append(temp_ref_path)
228
- cv2.imwrite(temp_ref_path, ref_image)
 
 
 
 
 
 
 
229
 
230
  # Clean up arrays immediately
231
  del ref_image_array, ref_image
232
 
233
  try:
234
- # CRITICAL FIX: Use better detector and alignment for accuracy
235
  result = DeepFace.verify(
236
  img1_path=temp_img_path,
237
  img2_path=temp_ref_path,
238
  model_name="Facenet",
239
- detector_backend="retinaface", # More robust than default opencv
240
- enforce_detection=False, # Allow processing even if detection is uncertain
241
- align=True, # Enable face alignment for better matching
242
- distance_metric="cosine" # Often works better than euclidean
243
  )
244
 
245
  is_verified = result["verified"]
246
  distance = result["distance"]
247
- threshold = result["threshold"]
248
 
249
  inference_time = time.time() - start_time
250
  inference_times.append(inference_time)
251
  total_attempts += 1
252
 
253
- # Lower threshold for more lenient matching (adjust as needed)
254
- custom_threshold = 0.45 # Default is usually 0.4, try 0.45-0.5 for more lenient
255
  is_verified_custom = distance < custom_threshold
256
 
257
  if is_verified_custom:
@@ -262,33 +284,11 @@ def recognize_face_deepface(image, user_id, user_type='student'):
262
  return False, f"Face not recognized (distance={distance:.3f}, required < {custom_threshold})"
263
 
264
  except Exception as e:
265
- # Fallback: try with opencv detector if retinaface fails
266
- try:
267
- print(f"RetinaFace failed, trying OpenCV detector: {e}")
268
- result = DeepFace.verify(
269
- img1_path=temp_img_path,
270
- img2_path=temp_ref_path,
271
- model_name="Facenet",
272
- detector_backend="opencv",
273
- enforce_detection=False,
274
- align=True
275
- )
276
-
277
- distance = result["distance"]
278
- custom_threshold = 0.5 # More lenient for fallback
279
- is_verified_custom = distance < custom_threshold
280
-
281
- if is_verified_custom:
282
- correct_recognitions += 1
283
- return True, f"Face recognized with fallback (distance={distance:.3f})"
284
- else:
285
- unauthorized_attempts += 1
286
- return False, f"Face not recognized with fallback (distance={distance:.3f})"
287
-
288
- except Exception as e2:
289
- return False, f"DeepFace verification error: {str(e2)}"
290
 
291
  except Exception as e:
 
292
  return False, f"Error in face recognition: {str(e)}"
293
 
294
  finally:
@@ -322,29 +322,26 @@ def simple_liveness_check(image):
322
  liveness_score += 0.1 # No eyes detected but still some base score
323
 
324
  # Factor 2: Image quality assessment (30% weight)
325
- # Check for blur and contrast
326
  laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
327
- if laplacian_var > 100: # Good sharpness
328
  liveness_score += 0.3
329
- elif laplacian_var > 50: # Moderate sharpness
330
  liveness_score += 0.2
331
  else:
332
- liveness_score += 0.1 # Poor sharpness but not zero
333
 
334
  # Factor 3: Face size and position (30% weight)
335
- # Larger faces are more likely to be real (closer to camera)
336
  face_area = image.shape[0] * image.shape[1]
337
- if face_area > 10000: # Decent face size
338
  liveness_score += 0.3
339
- elif face_area > 5000: # Smaller but acceptable
340
  liveness_score += 0.2
341
  else:
342
- liveness_score += 0.1 # Very small face
343
 
344
  # Ensure score is between 0 and 1
345
  liveness_score = min(1.0, max(0.0, liveness_score))
346
 
347
- # Clean up memory
348
  del gray
349
  gc.collect()
350
  return liveness_score
@@ -392,20 +389,10 @@ def decode_image(base64_image):
392
  np_array = np.frombuffer(image_bytes, np.uint8)
393
  image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
394
 
395
- # Clean up memory
396
  del image_bytes, np_array
397
  gc.collect()
398
  return image
399
 
400
- # Legacy function for backward compatibility
401
- def get_face_features(image):
402
- """Legacy wrapper - now uses DeepFace internally"""
403
- return None
404
-
405
- def recognize_face(image, user_id, user_type='student'):
406
- """Legacy wrapper for the new DeepFace recognition"""
407
- return recognize_face_deepface(image, user_id, user_type)
408
-
409
  # Token-based session helpers
410
  def validate_session_token(token):
411
  """Validate session token and return session data"""
@@ -445,26 +432,24 @@ def create_session_token(user_id, user_type):
445
  sessions_collection.insert_one(session_data)
446
  return token
447
 
448
- # ---------------------- Metrics helpers ----------------------
 
 
 
 
 
 
 
449
  def log_metrics_event(event: dict):
450
  try:
451
  metrics_events.insert_one(event)
452
  except Exception as e:
453
  print("Failed to log metrics event:", e)
454
 
455
- def log_metrics_event_normalized(
456
- *,
457
- event: str,
458
- attempt_type: str,
459
- claimed_id: Optional[str],
460
- recognized_id: Optional[str],
461
- liveness_pass: bool,
462
- distance: Optional[float],
463
- live_prob: Optional[float],
464
- latency_ms: Optional[float],
465
- client_ip: Optional[str],
466
- reason: Optional[str] = None
467
- ):
468
  if not liveness_pass:
469
  decision = "spoof_blocked"
470
  else:
@@ -700,6 +685,11 @@ def face_login():
700
  return redirect(url_for('login_page'))
701
 
702
  image = decode_image(face_image)
 
 
 
 
 
703
 
704
  if face_role == 'student':
705
  collection = students_collection
@@ -718,28 +708,43 @@ def face_login():
718
  best_match = None
719
  best_distance = float('inf')
720
 
721
- # Use DeepFace for face matching with improved temp file handling
722
  temp_login_path = get_unique_temp_path("login_image")
723
- cv2.imwrite(temp_login_path, image)
 
 
 
 
 
 
 
 
 
724
 
725
  try:
726
  from deepface import DeepFace
727
 
728
  for user in users:
729
- ref_image_bytes = user['face_image']
730
- ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
731
- ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
732
-
733
- temp_ref_path = get_unique_temp_path(f"ref_{user[id_field]}")
734
- cv2.imwrite(temp_ref_path, ref_image)
735
-
736
  try:
737
- # Use improved recognition settings
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  result = DeepFace.verify(
739
  img1_path=temp_login_path,
740
  img2_path=temp_ref_path,
741
  model_name="Facenet",
742
- detector_backend="retinaface",
743
  enforce_detection=False,
744
  align=True
745
  )
@@ -752,7 +757,7 @@ def face_login():
752
  best_match = user
753
 
754
  # More lenient threshold for face login
755
- if distance < 0.5: # Increased from default 0.4
756
  # Create session token
757
  token = create_session_token(user[id_field], face_role)
758
 
@@ -760,9 +765,10 @@ def face_login():
760
  flash('Face login successful!', 'success')
761
 
762
  # Cleanup
763
- for temp_file in [temp_ref_path, temp_login_path]:
764
- if os.path.exists(temp_file):
765
- os.remove(temp_file)
 
766
  gc.collect()
767
 
768
  return redirect(url_for(dashboard_route, token=token))
@@ -772,8 +778,6 @@ def face_login():
772
 
773
  except Exception as e:
774
  print(f"Face verification error: {e}")
775
- if os.path.exists(temp_ref_path):
776
- os.remove(temp_ref_path)
777
  continue
778
 
779
  if os.path.exists(temp_login_path):
@@ -800,108 +804,6 @@ def face_login():
800
  flash(f'Face login failed: {str(e)}', 'danger')
801
  return redirect(url_for('login_page'))
802
 
803
- @app.route('/auto-face-login', methods=['POST'])
804
- def auto_face_login():
805
- """Enhanced auto face login with role support"""
806
- try:
807
- data = request.json
808
- face_image = data.get('face_image')
809
- face_role = data.get('face_role', 'student')
810
-
811
- print(f"Auto face login attempt for role: {face_role}")
812
-
813
- if not face_image:
814
- return jsonify({'success': False, 'message': 'No image received'})
815
-
816
- image = decode_image(face_image)
817
-
818
- if face_role == 'teacher':
819
- collection = teachers_collection
820
- id_field = 'teacher_id'
821
- else:
822
- collection = students_collection
823
- id_field = 'student_id'
824
-
825
- # Use DeepFace for recognition with improved temp file handling
826
- temp_auto_path = get_unique_temp_path("auto_login")
827
- cv2.imwrite(temp_auto_path, image)
828
-
829
- try:
830
- from deepface import DeepFace
831
-
832
- users = collection.find({'face_image': {'$exists': True, '$ne': None}})
833
- for user in users:
834
- try:
835
- ref_image_array = np.frombuffer(user['face_image'], np.uint8)
836
- ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
837
-
838
- temp_ref_path = get_unique_temp_path(f"auto_ref_{user[id_field]}")
839
- cv2.imwrite(temp_ref_path, ref_image)
840
-
841
- result = DeepFace.verify(
842
- img1_path=temp_auto_path,
843
- img2_path=temp_ref_path,
844
- model_name="Facenet",
845
- detector_backend="retinaface",
846
- enforce_detection=False,
847
- align=True
848
- )
849
-
850
- if result["distance"] < 0.5: # More lenient threshold
851
- # Create session token
852
- token = create_session_token(user[id_field], face_role)
853
-
854
- print(f"Auto face login successful for {user.get('name')}, Token: {token[:10]}...")
855
-
856
- # Cleanup
857
- for temp_file in [temp_ref_path, temp_auto_path]:
858
- if os.path.exists(temp_file):
859
- os.remove(temp_file)
860
-
861
- gc.collect()
862
-
863
- dashboard_route = '/dashboard' if face_role == 'student' else '/teacher_dashboard'
864
-
865
- return jsonify({
866
- 'success': True,
867
- 'message': f'Welcome {user["name"]}! Redirecting...',
868
- 'redirect_url': f'{dashboard_route}?token={token}',
869
- 'face_role': face_role
870
- })
871
-
872
- if os.path.exists(temp_ref_path):
873
- os.remove(temp_ref_path)
874
- except Exception as e:
875
- continue
876
-
877
- if os.path.exists(temp_auto_path):
878
- os.remove(temp_auto_path)
879
-
880
- except Exception as e:
881
- if os.path.exists(temp_auto_path):
882
- os.remove(temp_auto_path)
883
- finally:
884
- gc.collect()
885
-
886
- print(f"Auto face login failed - face not recognized in {face_role} database")
887
- return jsonify({'success': False, 'message': f'Face not recognized in {face_role} database'})
888
- except Exception as e:
889
- print(f"Auto face login error: {e}")
890
- return jsonify({'success': False, 'message': 'Login failed due to server error'})
891
-
892
- @app.route('/attendance.html')
893
- def attendance_page():
894
- token = request.args.get('token')
895
- session_data = validate_session_token(token)
896
-
897
- if not session_data or session_data.get('user_type') != 'student':
898
- print("Token validation failed for attendance page")
899
- return redirect(url_for('login_page'))
900
-
901
- student_id = session_data.get('student_id')
902
- student = students_collection.find_one({'student_id': student_id})
903
- return render_template('attendance.html', student=student, session_token=token)
904
-
905
  @app.route('/dashboard')
906
  def dashboard():
907
  token = request.args.get('token')
@@ -977,13 +879,13 @@ def mark_attendance():
977
  if session_data.get('user_type') != 'student':
978
  return jsonify({'success': False, 'message': 'Invalid user type'})
979
 
980
- student_id = session_data.get('student_id')
981
  program = data.get('program')
982
  semester = data.get('semester')
983
  course = data.get('course')
984
  face_image = data.get('face_image')
985
 
986
- if not all([student_id, program, semester, course, face_image]):
987
  return jsonify({'success': False, 'message': 'Missing required data'})
988
 
989
  client_ip = request.remote_addr
@@ -1004,7 +906,7 @@ def mark_attendance():
1004
  log_metrics_event_normalized(
1005
  event="reject_true",
1006
  attempt_type="impostor",
1007
- claimed_id=student_id,
1008
  recognized_id=None,
1009
  liveness_pass=False,
1010
  distance=None,
@@ -1013,7 +915,7 @@ def mark_attendance():
1013
  client_ip=client_ip,
1014
  reason="no_face_detected"
1015
  )
1016
- return jsonify({'success': False, 'message': 'No face detected for liveness', 'overlay': overlay})
1017
 
1018
  # Pick highest-score detection
1019
  best = max(detections, key=lambda d: d["score"])
@@ -1025,7 +927,7 @@ def mark_attendance():
1025
  log_metrics_event_normalized(
1026
  event="reject_true",
1027
  attempt_type="impostor",
1028
- claimed_id=student_id,
1029
  recognized_id=None,
1030
  liveness_pass=False,
1031
  distance=None,
@@ -1034,13 +936,12 @@ def mark_attendance():
1034
  client_ip=client_ip,
1035
  reason="failed_crop"
1036
  )
1037
- return jsonify({'success': False, 'message': 'Failed to crop face for liveness', 'overlay': overlay})
1038
 
1039
- # 2) CRITICAL FIX: Enhanced liveness check with lower threshold
1040
  live_prob = simple_liveness_check(face_crop)
1041
 
1042
- # FIXED: Lower threshold to reduce false positives (was 0.7, now 0.5)
1043
- liveness_threshold = 0.5 # More lenient to avoid false spoof detection
1044
  is_live = live_prob >= liveness_threshold
1045
 
1046
  label = "LIVE" if is_live else "SPOOF"
@@ -1054,7 +955,7 @@ def mark_attendance():
1054
  log_metrics_event_normalized(
1055
  event="reject_true",
1056
  attempt_type="impostor",
1057
- claimed_id=student_id,
1058
  recognized_id=None,
1059
  liveness_pass=False,
1060
  distance=None,
@@ -1069,8 +970,8 @@ def mark_attendance():
1069
  'overlay': overlay_data
1070
  })
1071
 
1072
- # 3) Face recognition using DeepFace
1073
- success, message = recognize_face_deepface(image, student_id, user_type='student')
1074
  total_latency_ms = round((time.time() - t0) * 1000.0, 2)
1075
 
1076
  # Parse distance from message if available
@@ -1082,87 +983,80 @@ def mark_attendance():
1082
  except Exception:
1083
  pass
1084
 
1085
- # Derive reason string
1086
- reason = None
1087
  if not success:
1088
- if message.startswith("Unauthorized attempt"):
1089
- reason = "unauthorized_attempt"
1090
- elif message.startswith("No face detected"):
1091
- reason = "no_face_detected"
1092
- elif message.startswith("False reject"):
1093
- reason = "false_reject"
1094
- elif message.startswith("Error in face recognition") or message.startswith("DeepFace"):
1095
- reason = "recognition_error"
1096
- else:
1097
- reason = "not_recognized"
1098
-
1099
- # Log event
1100
- if success:
1101
  log_metrics_event_normalized(
1102
- event="accept_true",
1103
- attempt_type="genuine",
1104
- claimed_id=student_id,
1105
- recognized_id=student_id,
1106
  liveness_pass=True,
1107
  distance=distance_val,
1108
  live_prob=float(live_prob),
1109
  latency_ms=total_latency_ms,
1110
  client_ip=client_ip,
1111
- reason=None
1112
  )
1113
-
1114
- # Save attendance
1115
- attendance_data = {
1116
- 'student_id': student_id,
1117
- 'program': program,
1118
- 'semester': semester,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1119
  'subject': course,
1120
- 'date': datetime.now().date().isoformat(),
1121
- 'time': datetime.now().time().strftime('%H:%M:%S'),
1122
- 'status': 'present',
1123
- 'created_at': datetime.now()
1124
- }
1125
- try:
1126
- existing_attendance = attendance_collection.find_one({
1127
- 'student_id': student_id,
1128
- 'subject': course,
1129
- 'date': datetime.now().date().isoformat()
1130
  })
1131
- if existing_attendance:
1132
- return jsonify({'success': False, 'message': 'Attendance already marked for this course today', 'overlay': overlay_data})
1133
- attendance_collection.insert_one(attendance_data)
1134
- gc.collect() # Clean up memory after successful operation
1135
- return jsonify({'success': True, 'message': 'Attendance marked successfully', 'overlay': overlay_data})
1136
- except Exception as e:
1137
- return jsonify({'success': False, 'message': f'Database error: {str(e)}', 'overlay': overlay_data})
1138
- else:
1139
- if reason == "false_reject":
1140
- log_metrics_event_normalized(
1141
- event="reject_false",
1142
- attempt_type="genuine",
1143
- claimed_id=student_id,
1144
- recognized_id=student_id,
1145
- liveness_pass=True,
1146
- distance=distance_val,
1147
- live_prob=float(live_prob),
1148
- latency_ms=total_latency_ms,
1149
- client_ip=client_ip,
1150
- reason=reason
1151
- )
1152
- else:
1153
- log_metrics_event_normalized(
1154
- event="reject_true",
1155
- attempt_type="impostor",
1156
- claimed_id=student_id,
1157
- recognized_id=None,
1158
- liveness_pass=True,
1159
- distance=distance_val,
1160
- live_prob=float(live_prob),
1161
- latency_ms=total_latency_ms,
1162
- client_ip=client_ip,
1163
- reason=reason
1164
- )
1165
- return jsonify({'success': False, 'message': message, 'overlay': overlay_data})
1166
 
1167
  @app.route('/liveness-preview', methods=['POST'])
1168
  def liveness_preview():
@@ -1211,15 +1105,13 @@ def liveness_preview():
1211
  })
1212
 
1213
  live_prob = simple_liveness_check(face_crop)
1214
- # Use same threshold as attendance marking
1215
- threshold = 0.5
1216
  label = "LIVE" if live_prob >= threshold else "SPOOF"
1217
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
1218
 
1219
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
1220
  overlay_data = image_to_data_uri(vis)
1221
 
1222
- # Clean up memory
1223
  del image, vis, face_crop
1224
  gc.collect()
1225
 
@@ -1233,138 +1125,7 @@ def liveness_preview():
1233
  print("liveness_preview error:", e)
1234
  return jsonify({'success': False, 'message': 'Server error during preview'})
1235
 
1236
- # --------- TEACHER ROUTES ---------
1237
- @app.route('/teacher_register.html')
1238
- def teacher_register_page():
1239
- return render_template('teacher_register.html')
1240
-
1241
- @app.route('/teacher_login.html')
1242
- def teacher_login_page():
1243
- return render_template('teacher_login.html')
1244
-
1245
- @app.route('/teacher_register', methods=['POST'])
1246
- def teacher_register():
1247
- try:
1248
- teacher_data = {
1249
- 'teacher_id': request.form.get('teacher_id'),
1250
- 'name': request.form.get('name'),
1251
- 'email': request.form.get('email'),
1252
- 'department': request.form.get('department'),
1253
- 'designation': request.form.get('designation'),
1254
- 'mobile': request.form.get('mobile'),
1255
- 'dob': request.form.get('dob'),
1256
- 'gender': request.form.get('gender'),
1257
- 'password': request.form.get('password'),
1258
- 'created_at': datetime.now()
1259
- }
1260
- face_image = request.form.get('face_image')
1261
- if face_image and ',' in face_image:
1262
- image_data = face_image.split(',')[1]
1263
- teacher_data['face_image'] = Binary(base64.b64decode(image_data))
1264
- teacher_data['face_image_type'] = face_image.split(',')[0].split(':')[1].split(';')[0]
1265
- else:
1266
- flash('Face image is required for registration.', 'danger')
1267
- return redirect(url_for('teacher_register_page'))
1268
- result = teachers_collection.insert_one(teacher_data)
1269
- if result.inserted_id:
1270
- flash('Registration successful! You can now login.', 'success')
1271
- return redirect(url_for('teacher_login_page'))
1272
- else:
1273
- flash('Registration failed. Please try again.', 'danger')
1274
- return redirect(url_for('teacher_register_page'))
1275
- except pymongo.errors.DuplicateKeyError:
1276
- flash('Teacher ID already exists. Please use a different ID.', 'danger')
1277
- return redirect(url_for('teacher_register_page'))
1278
- except Exception as e:
1279
- flash(f'Registration failed: {str(e)}', 'danger')
1280
- return redirect(url_for('teacher_register_page'))
1281
-
1282
- @app.route('/teacher_login', methods=['POST'])
1283
- def teacher_login():
1284
- try:
1285
- teacher_id = request.form.get('teacher_id')
1286
- password = request.form.get('password')
1287
-
1288
- print(f"Teacher login attempt for teacher_id: {teacher_id}")
1289
-
1290
- if not teacher_id or not password:
1291
- flash('Teacher ID and password are required.', 'danger')
1292
- return redirect(url_for('teacher_login_page'))
1293
-
1294
- teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1295
- print(f"Teacher found: {bool(teacher)}")
1296
-
1297
- if teacher and teacher.get('password') == password:
1298
- # Create session token
1299
- token = create_session_token(teacher_id, 'teacher')
1300
-
1301
- print(f"Teacher token created: {token[:10]}...")
1302
- flash('Login successful!', 'success')
1303
- return redirect(url_for('teacher_dashboard', token=token))
1304
- else:
1305
- print("Invalid teacher credentials")
1306
- flash('Invalid credentials. Please try again.', 'danger')
1307
- return redirect(url_for('teacher_login_page'))
1308
-
1309
- except Exception as e:
1310
- print(f"Teacher login error: {e}")
1311
- flash(f'Login failed: {str(e)}', 'danger')
1312
- return redirect(url_for('teacher_login_page'))
1313
-
1314
- @app.route('/teacher_dashboard')
1315
- def teacher_dashboard():
1316
- token = request.args.get('token')
1317
-
1318
- print(f"Teacher dashboard access attempt. Token: {token[:10] if token else 'None'}...")
1319
-
1320
- if not token:
1321
- print("No token provided for teacher dashboard")
1322
- flash('Please log in to access the teacher dashboard.', 'info')
1323
- return redirect(url_for('teacher_login_page'))
1324
-
1325
- # Validate session token
1326
- session_data = validate_session_token(token)
1327
-
1328
- if not session_data or session_data.get('user_type') != 'teacher':
1329
- print("Teacher token validation failed - redirecting to login")
1330
- flash('Please log in to access the teacher dashboard.', 'info')
1331
- return redirect(url_for('teacher_login_page'))
1332
-
1333
- try:
1334
- teacher_id = session_data.get('teacher_id')
1335
- print(f"Loading teacher dashboard for teacher: {teacher_id}")
1336
-
1337
- teacher = teachers_collection.find_one({'teacher_id': teacher_id})
1338
- if not teacher:
1339
- print("Teacher not found in database")
1340
- sessions_collection.delete_one({'token': token})
1341
- flash('Teacher record not found. Please log in again.', 'danger')
1342
- return redirect(url_for('teacher_login_page'))
1343
-
1344
- # Process face image if exists
1345
- if teacher and 'face_image' in teacher and teacher['face_image']:
1346
- face_image_base64 = base64.b64encode(teacher['face_image']).decode('utf-8')
1347
- mime_type = teacher.get('face_image_type', 'image/jpeg')
1348
- teacher['face_image_url'] = f"data:{mime_type};base64,{face_image_base64}"
1349
-
1350
- print(f"Teacher dashboard loaded successfully for {teacher.get('name')}")
1351
- return render_template('teacher_dashboard.html', teacher=teacher, session_token=token)
1352
-
1353
- except Exception as e:
1354
- print(f"Teacher dashboard error: {e}")
1355
- flash(f'Error loading teacher dashboard: {str(e)}', 'danger')
1356
- return redirect(url_for('teacher_login_page'))
1357
-
1358
- @app.route('/teacher_logout')
1359
- def teacher_logout():
1360
- token = request.args.get('token')
1361
- if token:
1362
- sessions_collection.delete_one({'token': token})
1363
- print(f"Teacher token {token[:10]}... invalidated")
1364
- flash('You have been logged out', 'info')
1365
- return redirect(url_for('teacher_login_page'))
1366
-
1367
- # --------- COMMON LOGOUT ---------
1368
  @app.route('/logout')
1369
  def logout():
1370
  token = request.args.get('token')
@@ -1374,89 +1135,7 @@ def logout():
1374
  flash('You have been logged out', 'info')
1375
  return redirect(url_for('login_page'))
1376
 
1377
- # --------- METRICS JSON ENDPOINTS ---------
1378
- @app.route('/metrics-data', methods=['GET'])
1379
- def metrics_data():
1380
- data = compute_metrics()
1381
- try:
1382
- recent = list(metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(200))
1383
- normalized_recent = []
1384
- for r in recent:
1385
- if isinstance(r.get("ts"), datetime):
1386
- r["ts"] = r["ts"].isoformat()
1387
- event, attempt_type = classify_event(r)
1388
- if event and not r.get("event"):
1389
- r["event"] = event
1390
- if attempt_type and not r.get("attempt_type"):
1391
- r["attempt_type"] = attempt_type
1392
- if "liveness_pass" not in r:
1393
- if r.get("decision") == "spoof_blocked":
1394
- r["liveness_pass"] = False
1395
- elif isinstance(r.get("live_prob"), (int, float)):
1396
- r["liveness_pass"] = bool(r["live_prob"] >= 0.5) # Updated threshold
1397
- else:
1398
- r["liveness_pass"] = None
1399
- normalized_recent.append(r)
1400
-
1401
- data["recent"] = normalized_recent
1402
- except Exception as e:
1403
- print(f"Error getting recent metrics: {e}")
1404
- data["recent"] = []
1405
-
1406
- data["avg_latency_ms"] = compute_latency_avg()
1407
- return jsonify(data)
1408
-
1409
- @app.route('/metrics-json')
1410
- def metrics_json():
1411
- m = compute_metrics()
1412
- counts = m["counts"]
1413
- rates = m["rates"]
1414
- totals = m["totals"]
1415
- avg_latency = compute_latency_avg()
1416
- accuracy_pct = rates["accuracy"] * 100.0
1417
- far_pct = rates["FAR"] * 100.0
1418
- frr_pct = rates["FRR"] * 100.0
1419
-
1420
- return jsonify({
1421
- 'Accuracy': f"{accuracy_pct:.2f}%" if totals["totalAttempts"] > 0 else "N/A",
1422
- 'False Accepts (FAR)': f"{far_pct:.2f}%" if counts["impostorAttempts"] > 0 else "N/A",
1423
- 'False Rejects (FRR)': f"{frr_pct:.2f}%" if counts["genuineAttempts"] > 0 else "N/A",
1424
- 'Average Inference Time (s)': f"{(avg_latency/1000.0):.2f}" if isinstance(avg_latency, (int, float)) else "N/A",
1425
- 'Correct Recognitions': counts["trueAccepts"],
1426
- 'Total Attempts': totals["totalAttempts"],
1427
- 'Unauthorized Attempts': counts["unauthorizedRejected"],
1428
- 'enhanced': {
1429
- 'totals': {
1430
- 'attempts': totals["totalAttempts"],
1431
- 'trueAccepts': counts["trueAccepts"],
1432
- 'falseAccepts': counts["falseAccepts"],
1433
- 'trueRejects': counts["trueRejects"],
1434
- 'falseRejects': counts["falseRejects"],
1435
- 'genuineAttempts': counts["genuineAttempts"],
1436
- 'impostorAttempts': counts["impostorAttempts"],
1437
- 'unauthorizedRejected': counts["unauthorizedRejected"],
1438
- 'unauthorizedAccepted': counts["unauthorizedAccepted"],
1439
- },
1440
- 'accuracy_pct': round(accuracy_pct, 2),
1441
- 'avg_latency_ms': round(avg_latency, 2) if isinstance(avg_latency, (int, float)) else None
1442
- }
1443
- })
1444
-
1445
- @app.route('/metrics-events')
1446
- def metrics_events_api():
1447
- limit = int(request.args.get("limit", 200))
1448
- try:
1449
- cursor = metrics_events.find({}, {"_id": 0}).sort("ts", -1).limit(limit)
1450
- events = list(cursor)
1451
- for ev in events:
1452
- if isinstance(ev.get("ts"), datetime):
1453
- ev["ts"] = ev["ts"].isoformat()
1454
- return jsonify(events)
1455
- except Exception as e:
1456
- print(f"Error getting metrics events: {e}")
1457
- return jsonify([])
1458
-
1459
- # --------- DEBUG AND HEALTH CHECK ROUTES ---------
1460
  @app.route('/health-check')
1461
  @app.route('/health')
1462
  def health_check():
@@ -1465,7 +1144,8 @@ def health_check():
1465
  'platform': 'hugging_face',
1466
  'session_type': 'token_based',
1467
  'proxy_fix': 'enabled',
1468
- 'liveness_threshold': 0.5,
 
1469
  'timestamp': datetime.now().isoformat()
1470
  }), 200
1471
 
@@ -1483,85 +1163,6 @@ def debug_session():
1483
  'proxy_fix': 'enabled'
1484
  })
1485
 
1486
- @app.route('/debug-session-detailed')
1487
- def debug_session_detailed():
1488
- token = request.args.get('token')
1489
- session_data = validate_session_token(token) if token else None
1490
- return jsonify({
1491
- 'token_provided': bool(token),
1492
- 'token_valid': bool(session_data),
1493
- 'session_data': session_data,
1494
- 'cookies_received': dict(request.cookies),
1495
- 'headers': dict(request.headers),
1496
- 'user_agent': request.headers.get('User-Agent'),
1497
- 'remote_addr': request.remote_addr,
1498
- 'flask_secret_key_length': len(app.secret_key),
1499
- 'session_interface_type': str(type(app.session_interface)),
1500
- 'liveness_threshold': 0.5,
1501
- 'timestamp': datetime.now().isoformat()
1502
- })
1503
-
1504
- @app.route('/test-session')
1505
- def test_session():
1506
- """Test session functionality"""
1507
- token = secrets.token_urlsafe(16)
1508
- test_session = {
1509
- 'token': token,
1510
- 'test_data': 'working',
1511
- 'timestamp': datetime.now(),
1512
- 'expires_at': datetime.now() + timedelta(minutes=5)
1513
- }
1514
- sessions_collection.insert_one(test_session)
1515
-
1516
- return jsonify({
1517
- 'message': 'Token-based session test completed',
1518
- 'test_token': token,
1519
- 'test_successful': True
1520
- })
1521
-
1522
- @app.route('/debug-liveness', methods=['POST'])
1523
- def debug_liveness():
1524
- """Debug route to test liveness detection settings"""
1525
- data = request.json or {}
1526
- token = data.get('session_token')
1527
-
1528
- if not token or not validate_session_token(token):
1529
- return jsonify({'success': False, 'message': 'Not authenticated'})
1530
-
1531
- try:
1532
- face_image = data.get('face_image')
1533
- if not face_image:
1534
- return jsonify({'success': False, 'message': 'No image received'})
1535
-
1536
- image = decode_image(face_image)
1537
- if image is None:
1538
- return jsonify({'success': False, 'message': 'Invalid image data'})
1539
-
1540
- detections = detect_faces_yunet(image)
1541
- if not detections:
1542
- return jsonify({'success': False, 'message': 'No face detected'})
1543
-
1544
- best = max(detections, key=lambda d: d["score"])
1545
- x1, y1, x2, y2 = [int(v) for v in best["bbox"]]
1546
- x1e, y1e, x2e, y2e = expand_and_clip_box((x1, y1, x2, y2), scale=1.2, w=image.shape[1], h=image.shape[0])
1547
- face_crop = image[y1e:y2e, x1e:x2e]
1548
-
1549
- if face_crop.size == 0:
1550
- return jsonify({'success': False, 'message': 'Failed to crop face'})
1551
-
1552
- live_prob = simple_liveness_check(face_crop)
1553
-
1554
- return jsonify({
1555
- 'success': True,
1556
- 'liveness_score': float(live_prob),
1557
- 'threshold': 0.5,
1558
- 'would_pass': live_prob >= 0.5,
1559
- 'face_area': int(face_crop.shape[0] * face_crop.shape[1]),
1560
- 'debug_info': f"Score: {live_prob:.3f}, Threshold: 0.5, Result: {'PASS' if live_prob >= 0.5 else 'FAIL'}"
1561
- })
1562
- except Exception as e:
1563
- return jsonify({'success': False, 'message': f'Debug error: {str(e)}'})
1564
-
1565
  @app.route('/cleanup', methods=['POST'])
1566
  def manual_cleanup():
1567
  """Manual cleanup endpoint for memory management"""
@@ -1573,6 +1174,6 @@ def manual_cleanup():
1573
 
1574
  # MAIN APPLICATION ENTRY POINT
1575
  if __name__ == '__main__':
1576
- port = int(os.environ.get('PORT', 7860)) # Hugging Face uses port 7860
1577
- print(f"Starting Flask app on port {port} with token-based authentication and improved liveness detection")
1578
  app.run(host='0.0.0.0', port=port, debug=False)
 
5
  import logging
6
  import time
7
  import uuid
8
+ import secrets
9
  import pymongo
10
  from pymongo import MongoClient
11
  from bson.binary import Binary
 
51
  'PERMANENT_SESSION_LIFETIME': timedelta(hours=2),
52
  'SESSION_COOKIE_NAME': 'face_app_session',
53
  'SESSION_COOKIE_HTTPONLY': True,
54
+ 'SESSION_COOKIE_SECURE': False,
55
+ 'SESSION_COOKIE_SAMESITE': None,
56
+ 'SESSION_REFRESH_EACH_REQUEST': False,
57
  'SESSION_COOKIE_DOMAIN': None,
58
  'SESSION_COOKIE_PATH': '/',
59
+ 'SEND_FILE_MAX_AGE_DEFAULT': 0
60
  })
61
 
62
+ # Add ProxyFix middleware for Hugging Face reverse proxy
63
  app.wsgi_app = ProxyFix(
64
  app.wsgi_app,
65
  x_for=1,
 
99
  teachers_collection = db['teachers']
100
  attendance_collection = db['attendance']
101
  metrics_events = db['metrics_events']
102
+ sessions_collection = db['user_sessions']
103
 
104
  # Create indexes for better performance
105
  students_collection.create_index([("student_id", pymongo.ASCENDING)], unique=True)
 
118
  except Exception as e:
119
  print(f"MongoDB connection error: {e}")
120
 
121
+ # Initialize face detection cascades
 
 
122
  face_detector = None
123
  try:
124
  face_detector = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
 
126
  except Exception as e:
127
  print(f"Error initializing face detector: {e}")
128
 
 
129
  eye_cascade = None
130
  try:
131
  eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
 
157
  "score": 0.9
158
  })
159
 
 
160
  del gray
161
  gc.collect()
162
  return detections
 
169
  if face_detector is not None:
170
  return detect_faces_haar(image)
171
 
 
172
  try:
173
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
174
  face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
 
189
  return []
190
 
191
  def recognize_face_deepface(image, user_id, user_type='student'):
192
+ """FIXED: Enhanced face recognition with robust error handling"""
193
  global total_attempts, correct_recognitions, unauthorized_attempts, inference_times
194
 
195
  temp_files = []
 
199
 
200
  start_time = time.time()
201
 
202
+ # CRITICAL FIX: Ensure image is valid numpy array
203
+ if not isinstance(image, np.ndarray):
204
+ print("Invalid image type provided to recognition")
205
+ return False, "Invalid image format"
206
+
207
+ if image.size == 0:
208
+ print("Empty image provided to recognition")
209
+ return False, "Empty image provided"
210
+
211
+ # Save current image temporarily with error handling
212
  temp_img_path = get_unique_temp_path(f"current_{user_id}")
213
  temp_files.append(temp_img_path)
214
+
215
+ # FIXED: Add image validation before saving
216
+ try:
217
+ success = cv2.imwrite(temp_img_path, image)
218
+ if not success:
219
+ return False, "Failed to save input image"
220
+ except Exception as e:
221
+ print(f"Error saving input image: {e}")
222
+ return False, f"Image save error: {str(e)}"
223
 
224
  # Get user's reference image
225
  if user_type == 'student':
 
231
  unauthorized_attempts += 1
232
  return False, f"No reference face found for {user_type} ID {user_id}"
233
 
234
+ # Save reference image temporarily with validation
235
  ref_image_bytes = user['face_image']
236
  ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
237
  ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
238
+
239
+ if ref_image is None:
240
+ return False, "Failed to decode reference image"
241
+
242
  temp_ref_path = get_unique_temp_path(f"ref_{user_id}")
243
  temp_files.append(temp_ref_path)
244
+
245
+ try:
246
+ success = cv2.imwrite(temp_ref_path, ref_image)
247
+ if not success:
248
+ return False, "Failed to save reference image"
249
+ except Exception as e:
250
+ print(f"Error saving reference image: {e}")
251
+ return False, f"Reference image save error: {str(e)}"
252
 
253
  # Clean up arrays immediately
254
  del ref_image_array, ref_image
255
 
256
  try:
257
+ # FIXED: More robust DeepFace call with better error handling
258
  result = DeepFace.verify(
259
  img1_path=temp_img_path,
260
  img2_path=temp_ref_path,
261
  model_name="Facenet",
262
+ detector_backend="opencv", # Use opencv instead of retinaface for stability
263
+ enforce_detection=False,
264
+ align=True,
265
+ distance_metric="cosine"
266
  )
267
 
268
  is_verified = result["verified"]
269
  distance = result["distance"]
 
270
 
271
  inference_time = time.time() - start_time
272
  inference_times.append(inference_time)
273
  total_attempts += 1
274
 
275
+ # More lenient threshold for better usability
276
+ custom_threshold = 0.55 # Increased threshold for better matching
277
  is_verified_custom = distance < custom_threshold
278
 
279
  if is_verified_custom:
 
284
  return False, f"Face not recognized (distance={distance:.3f}, required < {custom_threshold})"
285
 
286
  except Exception as e:
287
+ print(f"DeepFace verification error: {e}")
288
+ return False, f"DeepFace verification error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  except Exception as e:
291
+ print(f"Error in face recognition: {e}")
292
  return False, f"Error in face recognition: {str(e)}"
293
 
294
  finally:
 
322
  liveness_score += 0.1 # No eyes detected but still some base score
323
 
324
  # Factor 2: Image quality assessment (30% weight)
 
325
  laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
326
+ if laplacian_var > 100:
327
  liveness_score += 0.3
328
+ elif laplacian_var > 50:
329
  liveness_score += 0.2
330
  else:
331
+ liveness_score += 0.1
332
 
333
  # Factor 3: Face size and position (30% weight)
 
334
  face_area = image.shape[0] * image.shape[1]
335
+ if face_area > 10000:
336
  liveness_score += 0.3
337
+ elif face_area > 5000:
338
  liveness_score += 0.2
339
  else:
340
+ liveness_score += 0.1
341
 
342
  # Ensure score is between 0 and 1
343
  liveness_score = min(1.0, max(0.0, liveness_score))
344
 
 
345
  del gray
346
  gc.collect()
347
  return liveness_score
 
389
  np_array = np.frombuffer(image_bytes, np.uint8)
390
  image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
391
 
 
392
  del image_bytes, np_array
393
  gc.collect()
394
  return image
395
 
 
 
 
 
 
 
 
 
 
396
  # Token-based session helpers
397
  def validate_session_token(token):
398
  """Validate session token and return session data"""
 
432
  sessions_collection.insert_one(session_data)
433
  return token
434
 
435
+ # Legacy function for backward compatibility
436
+ def get_face_features(image):
437
+ return None
438
+
439
+ def recognize_face(image, user_id, user_type='student'):
440
+ return recognize_face_deepface(image, user_id, user_type)
441
+
442
+ # Metrics helpers (keeping existing implementation)
443
  def log_metrics_event(event: dict):
444
  try:
445
  metrics_events.insert_one(event)
446
  except Exception as e:
447
  print("Failed to log metrics event:", e)
448
 
449
+ def log_metrics_event_normalized(*, event: str, attempt_type: str, claimed_id: Optional[str],
450
+ recognized_id: Optional[str], liveness_pass: bool, distance: Optional[float],
451
+ live_prob: Optional[float], latency_ms: Optional[float], client_ip: Optional[str],
452
+ reason: Optional[str] = None):
 
 
 
 
 
 
 
 
 
453
  if not liveness_pass:
454
  decision = "spoof_blocked"
455
  else:
 
685
  return redirect(url_for('login_page'))
686
 
687
  image = decode_image(face_image)
688
+
689
+ # FIXED: Add image validation
690
+ if image is None:
691
+ flash('Invalid image format received.', 'danger')
692
+ return redirect(url_for('login_page'))
693
 
694
  if face_role == 'student':
695
  collection = students_collection
 
708
  best_match = None
709
  best_distance = float('inf')
710
 
711
+ # Use robust face matching
712
  temp_login_path = get_unique_temp_path("login_image")
713
+
714
+ try:
715
+ success = cv2.imwrite(temp_login_path, image)
716
+ if not success:
717
+ flash('Failed to process face image. Please try again.', 'danger')
718
+ return redirect(url_for('login_page'))
719
+ except Exception as e:
720
+ print(f"Error saving login image: {e}")
721
+ flash('Error processing face image. Please try again.', 'danger')
722
+ return redirect(url_for('login_page'))
723
 
724
  try:
725
  from deepface import DeepFace
726
 
727
  for user in users:
 
 
 
 
 
 
 
728
  try:
729
+ ref_image_bytes = user['face_image']
730
+ ref_image_array = np.frombuffer(ref_image_bytes, np.uint8)
731
+ ref_image = cv2.imdecode(ref_image_array, cv2.IMREAD_COLOR)
732
+
733
+ if ref_image is None:
734
+ continue
735
+
736
+ temp_ref_path = get_unique_temp_path(f"ref_{user[id_field]}")
737
+
738
+ success = cv2.imwrite(temp_ref_path, ref_image)
739
+ if not success:
740
+ continue
741
+
742
+ # FIXED: Use stable opencv detector
743
  result = DeepFace.verify(
744
  img1_path=temp_login_path,
745
  img2_path=temp_ref_path,
746
  model_name="Facenet",
747
+ detector_backend="opencv", # Use opencv instead of retinaface
748
  enforce_detection=False,
749
  align=True
750
  )
 
757
  best_match = user
758
 
759
  # More lenient threshold for face login
760
+ if distance < 0.6: # Increased threshold for better matching
761
  # Create session token
762
  token = create_session_token(user[id_field], face_role)
763
 
 
765
  flash('Face login successful!', 'success')
766
 
767
  # Cleanup
768
+ if os.path.exists(temp_ref_path):
769
+ os.remove(temp_ref_path)
770
+ if os.path.exists(temp_login_path):
771
+ os.remove(temp_login_path)
772
  gc.collect()
773
 
774
  return redirect(url_for(dashboard_route, token=token))
 
778
 
779
  except Exception as e:
780
  print(f"Face verification error: {e}")
 
 
781
  continue
782
 
783
  if os.path.exists(temp_login_path):
 
804
  flash(f'Face login failed: {str(e)}', 'danger')
805
  return redirect(url_for('login_page'))
806
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
  @app.route('/dashboard')
808
  def dashboard():
809
  token = request.args.get('token')
 
879
  if session_data.get('user_type') != 'student':
880
  return jsonify({'success': False, 'message': 'Invalid user type'})
881
 
882
+ logged_in_student_id = session_data.get('student_id')
883
  program = data.get('program')
884
  semester = data.get('semester')
885
  course = data.get('course')
886
  face_image = data.get('face_image')
887
 
888
+ if not all([logged_in_student_id, program, semester, course, face_image]):
889
  return jsonify({'success': False, 'message': 'Missing required data'})
890
 
891
  client_ip = request.remote_addr
 
906
  log_metrics_event_normalized(
907
  event="reject_true",
908
  attempt_type="impostor",
909
+ claimed_id=logged_in_student_id,
910
  recognized_id=None,
911
  liveness_pass=False,
912
  distance=None,
 
915
  client_ip=client_ip,
916
  reason="no_face_detected"
917
  )
918
+ return jsonify({'success': False, 'message': 'No face detected', 'overlay': overlay})
919
 
920
  # Pick highest-score detection
921
  best = max(detections, key=lambda d: d["score"])
 
927
  log_metrics_event_normalized(
928
  event="reject_true",
929
  attempt_type="impostor",
930
+ claimed_id=logged_in_student_id,
931
  recognized_id=None,
932
  liveness_pass=False,
933
  distance=None,
 
936
  client_ip=client_ip,
937
  reason="failed_crop"
938
  )
939
+ return jsonify({'success': False, 'message': 'Failed to crop face', 'overlay': overlay})
940
 
941
+ # 2) Liveness check with lower threshold
942
  live_prob = simple_liveness_check(face_crop)
943
 
944
+ liveness_threshold = 0.4 # More lenient threshold
 
945
  is_live = live_prob >= liveness_threshold
946
 
947
  label = "LIVE" if is_live else "SPOOF"
 
955
  log_metrics_event_normalized(
956
  event="reject_true",
957
  attempt_type="impostor",
958
+ claimed_id=logged_in_student_id,
959
  recognized_id=None,
960
  liveness_pass=False,
961
  distance=None,
 
970
  'overlay': overlay_data
971
  })
972
 
973
+ # 3) CRITICAL SECURITY FIX: Verify face belongs to logged-in user
974
+ success, message = recognize_face_deepface(image, logged_in_student_id, user_type='student')
975
  total_latency_ms = round((time.time() - t0) * 1000.0, 2)
976
 
977
  # Parse distance from message if available
 
983
  except Exception:
984
  pass
985
 
986
+ # CRITICAL SECURITY CHECK: Only allow attendance if recognized face matches logged-in user
 
987
  if not success:
988
+ reason = "face_mismatch_with_logged_user"
 
 
 
 
 
 
 
 
 
 
 
 
989
  log_metrics_event_normalized(
990
+ event="reject_true",
991
+ attempt_type="impostor",
992
+ claimed_id=logged_in_student_id,
993
+ recognized_id=None,
994
  liveness_pass=True,
995
  distance=distance_val,
996
  live_prob=float(live_prob),
997
  latency_ms=total_latency_ms,
998
  client_ip=client_ip,
999
+ reason=reason
1000
  )
1001
+ return jsonify({
1002
+ 'success': False,
1003
+ 'message': f'SECURITY ALERT: Face does not match logged-in student {logged_in_student_id}. Please ensure you are the correct person marking attendance.',
1004
+ 'overlay': overlay_data
1005
+ })
1006
+
1007
+ # Log successful verification
1008
+ log_metrics_event_normalized(
1009
+ event="accept_true",
1010
+ attempt_type="genuine",
1011
+ claimed_id=logged_in_student_id,
1012
+ recognized_id=logged_in_student_id,
1013
+ liveness_pass=True,
1014
+ distance=distance_val,
1015
+ live_prob=float(live_prob),
1016
+ latency_ms=total_latency_ms,
1017
+ client_ip=client_ip,
1018
+ reason=None
1019
+ )
1020
+
1021
+ # Save attendance for the LOGGED-IN user (not whoever's face was recognized)
1022
+ attendance_data = {
1023
+ 'student_id': logged_in_student_id, # FIXED: Use logged-in user ID
1024
+ 'program': program,
1025
+ 'semester': semester,
1026
+ 'subject': course,
1027
+ 'date': datetime.now().date().isoformat(),
1028
+ 'time': datetime.now().time().strftime('%H:%M:%S'),
1029
+ 'status': 'present',
1030
+ 'created_at': datetime.now()
1031
+ }
1032
+
1033
+ try:
1034
+ existing_attendance = attendance_collection.find_one({
1035
+ 'student_id': logged_in_student_id,
1036
  'subject': course,
1037
+ 'date': datetime.now().date().isoformat()
1038
+ })
1039
+ if existing_attendance:
1040
+ return jsonify({
1041
+ 'success': False,
1042
+ 'message': 'Attendance already marked for this course today',
1043
+ 'overlay': overlay_data
 
 
 
1044
  })
1045
+
1046
+ attendance_collection.insert_one(attendance_data)
1047
+ gc.collect()
1048
+
1049
+ return jsonify({
1050
+ 'success': True,
1051
+ 'message': f'Attendance marked successfully for {logged_in_student_id}',
1052
+ 'overlay': overlay_data
1053
+ })
1054
+ except Exception as e:
1055
+ return jsonify({
1056
+ 'success': False,
1057
+ 'message': f'Database error: {str(e)}',
1058
+ 'overlay': overlay_data
1059
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1060
 
1061
  @app.route('/liveness-preview', methods=['POST'])
1062
  def liveness_preview():
 
1105
  })
1106
 
1107
  live_prob = simple_liveness_check(face_crop)
1108
+ threshold = 0.4 # Match attendance marking threshold
 
1109
  label = "LIVE" if live_prob >= threshold else "SPOOF"
1110
  color = (0, 200, 0) if label == "LIVE" else (0, 0, 255)
1111
 
1112
  draw_live_overlay(vis, (x1e, y1e, x2e, y2e), label, live_prob, color)
1113
  overlay_data = image_to_data_uri(vis)
1114
 
 
1115
  del image, vis, face_crop
1116
  gc.collect()
1117
 
 
1125
  print("liveness_preview error:", e)
1126
  return jsonify({'success': False, 'message': 'Server error during preview'})
1127
 
1128
+ # --------- LOGOUT ROUTES ---------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1129
  @app.route('/logout')
1130
  def logout():
1131
  token = request.args.get('token')
 
1135
  flash('You have been logged out', 'info')
1136
  return redirect(url_for('login_page'))
1137
 
1138
+ # --------- DEBUG ROUTES ---------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  @app.route('/health-check')
1140
  @app.route('/health')
1141
  def health_check():
 
1144
  'platform': 'hugging_face',
1145
  'session_type': 'token_based',
1146
  'proxy_fix': 'enabled',
1147
+ 'liveness_threshold': 0.4,
1148
+ 'face_detector': 'opencv_stable',
1149
  'timestamp': datetime.now().isoformat()
1150
  }), 200
1151
 
 
1163
  'proxy_fix': 'enabled'
1164
  })
1165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1166
  @app.route('/cleanup', methods=['POST'])
1167
  def manual_cleanup():
1168
  """Manual cleanup endpoint for memory management"""
 
1174
 
1175
  # MAIN APPLICATION ENTRY POINT
1176
  if __name__ == '__main__':
1177
+ port = int(os.environ.get('PORT', 7860))
1178
+ print(f"Starting Flask app on port {port} with secure token-based authentication")
1179
  app.run(host='0.0.0.0', port=port, debug=False)