Spaces:
Sleeping
Sleeping
| # --- START OF FILE app.py --- | |
| import os | |
| import json | |
| import hashlib | |
| import time | |
| import re | |
| import base64 | |
| import traceback # For detailed error logging | |
| import secrets # Use secrets for cryptographic randomness | |
| import string # For character sets | |
| import random # For shuffling (secrets doesn't have shuffle) | |
| from datetime import timedelta # ** IMPORT timedelta ** | |
| from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session | |
| from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user | |
| from flask_bcrypt import Bcrypt | |
| from supabase import create_client, Client # Use v2 import style | |
| # Note: cryptography library parts are only needed for derive_key now | |
| from cryptography.hazmat.primitives import hashes | |
| from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
| from dotenv import load_dotenv | |
| import google.generativeai as genai | |
| from zxcvbn import zxcvbn | |
| load_dotenv() | |
| app = Flask(__name__) | |
| # Make sure FLASK_SECRET_KEY is set strong in your .env! | |
| app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', 'default_secret_key_please_change') | |
| app.config['ENV'] = os.environ.get('FLASK_ENV', 'production') | |
| app.config['DEBUG'] = app.config['ENV'] == 'development' | |
| # ** SET PERMANENT SESSION LIFETIME (e.g., 7 days) ** | |
| # This applies *only* when 'Remember Me' is checked. | |
| app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) | |
| # --- Supabase Configuration --- | |
| supabase_url = os.environ.get("SUPABASE_URL") | |
| supabase_key = os.environ.get("SUPABASE_KEY") # Anon key | |
| if not supabase_url or not supabase_key: | |
| raise ValueError("Supabase URL and Key must be set in .env file") | |
| supabase: Client = create_client(supabase_url, supabase_key) | |
| print("Supabase client initialized.") | |
| # --- Authentication Setup --- | |
| bcrypt = Bcrypt(app) | |
| login_manager = LoginManager() | |
| login_manager.init_app(app) | |
| login_manager.login_view = 'login' | |
| login_manager.login_message_category = 'info' | |
| print("Flask-Login initialized.") | |
| # --- User Model for Flask-Login --- | |
| class User(UserMixin): | |
| def __init__(self, id, email): | |
| self.id = id | |
| self.email = email | |
| def load_user(user_id): | |
| user_data = session.get('user_data') | |
| if user_data and user_data['id'] == user_id: | |
| return User(id=user_data['id'], email=user_data['email']) | |
| # print(f"User {user_id} not found in session.") # Reduced verbosity | |
| return None | |
| # --- Gemini LLM Configuration --- | |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") | |
| genai_available = False | |
| if GEMINI_API_KEY and GEMINI_API_KEY.startswith("AIza"): | |
| try: | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| genai_available = True | |
| print("Gemini API configured successfully.") | |
| except Exception as e: | |
| print(f"Warning: Failed to configure Gemini API: {e}") | |
| else: | |
| print("Warning: Gemini API key not found or looks invalid. LLM features will use mock/basic analysis.") | |
| # --- E2EE Helper Functions --- | |
| def derive_key(master_password: str, salt_or_email: str) -> bytes: | |
| """ | |
| Derives a 32-byte key suitable for AES using PBKDF2HMAC. | |
| Uses email (lowercase) as salt for simplicity - *replace with stored unique salt in production*. | |
| Returns the key BASE64 URL-SAFE ENCODED (suitable for session/storage). | |
| """ | |
| salt = salt_or_email.lower().encode('utf-8') | |
| iterations = 390000 # Match JS side | |
| kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=iterations ) | |
| key_bytes = kdf.derive(master_password.encode('utf-8')) # Raw 32 bytes | |
| # --- ADDED LOGGING --- | |
| # Log the standard Base64 representation for easier comparison with JS console log | |
| try: | |
| raw_key_standard_b64 = base64.b64encode(key_bytes).decode('utf-8') | |
| # print(f"DEBUG: PY Derived Key (Raw -> Standard Base64): {raw_key_standard_b64}") # Keep commented unless debugging | |
| except Exception as log_e: | |
| print(f"DEBUG: Error logging derived key: {log_e}") | |
| # --- END LOGGING --- | |
| # Return URL-safe Base64 encoded key for session storage | |
| key_b64url_encoded = base64.urlsafe_b64encode(key_bytes) | |
| return key_b64url_encoded | |
| # --- Password Analysis Functions --- | |
| # (get_character_composition, get_llm_password_insights, get_basic_password_analysis_from_chars, get_mock_llm_insights_from_chars) | |
| def get_character_composition(password): | |
| """Generates character composition dict.""" | |
| composition = { 'lowercase': 0, 'uppercase': 0, 'digits': 0, 'special': 0 } | |
| if not password: return composition # Handle None or empty string | |
| for char in password: | |
| if char.islower(): composition['lowercase'] += 1 | |
| elif char.isupper(): composition['uppercase'] += 1 | |
| elif char.isdigit(): composition['digits'] += 1 | |
| elif not char.isalnum() and not char.isspace(): composition['special'] += 1 # More specific special char check | |
| return composition | |
| def get_llm_password_insights(password_characteristics: dict): | |
| """ Get password analysis insights from Gemini API based on characteristics.""" | |
| global genai_available | |
| try: | |
| use_mock = app.config['DEBUG'] or not genai_available | |
| if use_mock: | |
| return get_mock_llm_insights_from_chars(password_characteristics) | |
| generation_config = { "temperature": 0.2, "top_p": 0.8, "top_k": 40, "max_output_tokens": 512 } | |
| safety_settings = [ | |
| {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, | |
| {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, | |
| {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, | |
| {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, | |
| ] | |
| model = genai.GenerativeModel('gemini-1.5-flash', generation_config=generation_config, safety_settings=safety_settings) | |
| prompt = f""" | |
| Analyze a password based *only* on the following characteristics. Provide a security assessment. | |
| DO NOT attempt to guess the password or ask for it. | |
| Return ONLY a valid JSON object with the exact structure below, no other text or explanations. | |
| {{ | |
| "strength_score": 1-5 (integer, 1=very weak, 5=very strong), | |
| "assessment": "Very Weak/Weak/Medium/Strong/Very Strong", | |
| "insights": ["Specific observations based on characteristics provided"], | |
| "suggestions": ["Actionable improvement tips based on characteristics"] | |
| }} | |
| Password Characteristics: | |
| Length: {password_characteristics.get('length', 0)} | |
| Contains Lowercase: {'Yes' if password_characteristics.get('composition', {}).get('lowercase', 0) > 0 else 'No'} | |
| Contains Uppercase: {'Yes' if password_characteristics.get('composition', {}).get('uppercase', 0) > 0 else 'No'} | |
| Contains Digits: {'Yes' if password_characteristics.get('composition', {}).get('digits', 0) > 0 else 'No'} | |
| Contains Special Chars: {'Yes' if password_characteristics.get('composition', {}).get('special', 0) > 0 else 'No'} | |
| """ | |
| response = model.generate_content(prompt) | |
| if not response.candidates or not hasattr(response.candidates[0], 'content') or not response.candidates[0].content.parts: | |
| print(f"Gemini response blocked or empty. Reason: {response.prompt_feedback}") | |
| return get_basic_password_analysis_from_chars(password_characteristics) | |
| response_text = response.text | |
| json_match = re.search(r'```(?:json)?\s*({.*?})\s*```', response_text, re.DOTALL | re.IGNORECASE) | |
| if not json_match: json_match = re.search(r'({.*})', response_text, re.DOTALL) | |
| if json_match: | |
| try: | |
| insights_json = json.loads(json_match.group(1)) | |
| if all(k in insights_json for k in ['strength_score', 'assessment', 'insights', 'suggestions']): | |
| return { 'strength': insights_json.get('strength_score', 1), 'assessment': insights_json.get('assessment', 'Weak'), | |
| 'insights': insights_json.get('insights', []), 'suggestions': insights_json.get('suggestions', []) } | |
| else: raise ValueError("Missing keys in JSON response") | |
| except (json.JSONDecodeError, ValueError) as json_err: | |
| print(f"LLM JSON processing Error: {json_err}. Falling back.") | |
| return get_basic_password_analysis_from_chars(password_characteristics) | |
| else: | |
| print("LLM JSON pattern not found. Falling back.") | |
| return get_basic_password_analysis_from_chars(password_characteristics) | |
| except Exception as e: | |
| print(f"Error getting Gemini insights: {type(e).__name__} - {e}") | |
| return get_basic_password_analysis_from_chars(password_characteristics) | |
| def get_basic_password_analysis_from_chars(characteristics: dict): | |
| """ Basic password analysis based on characteristics. Returns same structure as LLM.""" | |
| strength = 0; insights = []; suggestions = [] | |
| length = characteristics.get('length', 0) | |
| composition = characteristics.get('composition', {}) | |
| # Score Calculation (simple example) | |
| if length < 8: insights.append("Short (< 8 chars)"); suggestions.append("Use 12+ chars.") | |
| elif length < 12: strength += 1; insights.append("Okay length (8-11)") ; suggestions.append("Use 12+ chars.") | |
| else: strength += 2; insights.append("Good length (12+)") | |
| if composition.get('uppercase', 0) > 0: strength += 1 | |
| else: insights.append("No uppercase"); suggestions.append("Add A-Z.") | |
| if composition.get('lowercase', 0) > 0: strength += 1 | |
| else: insights.append("No lowercase"); suggestions.append("Add a-z.") | |
| if composition.get('digits', 0) > 0: strength += 1 | |
| else: insights.append("No numbers"); suggestions.append("Add 0-9.") | |
| if composition.get('special', 0) > 0: strength += 1 | |
| else: insights.append("No special chars"); suggestions.append("Add !@#$.") | |
| # Map score (0-6) to 1-5 rating | |
| score_map = {0: 1, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 5} | |
| final_score = score_map.get(strength, 1) | |
| assessment_map = {1: "Very Weak", 2: "Weak", 3: "Medium", 4: "Strong", 5: "Very Strong"} | |
| assessment = assessment_map.get(final_score, "Weak") | |
| if final_score == 5 and not insights: insights.append("Meets basic complexity.") | |
| return {'strength': final_score, 'assessment': assessment, 'insights': insights, 'suggestions': suggestions} | |
| def get_mock_llm_insights_from_chars(characteristics: dict): | |
| """ Generates mock LLM insights based on characteristics.""" | |
| basic_analysis = get_basic_password_analysis_from_chars(characteristics) | |
| if basic_analysis['strength'] < 4: basic_analysis['suggestions'].append("Consider passphrase.") | |
| if not basic_analysis['insights'] and basic_analysis['strength'] >= 4: basic_analysis['insights'].append("Good length/variety.") | |
| return basic_analysis | |
| # --- NEW: Password Generation Function --- | |
| def generate_secure_password(length=16, use_lowercase=True, use_uppercase=True, use_digits=True, use_symbols=True): | |
| """Generates a secure password ensuring character set inclusion.""" | |
| character_pool = [] | |
| required_chars = [] | |
| if use_lowercase: | |
| character_pool.extend(string.ascii_lowercase) | |
| required_chars.append(secrets.choice(string.ascii_lowercase)) | |
| if use_uppercase: | |
| character_pool.extend(string.ascii_uppercase) | |
| required_chars.append(secrets.choice(string.ascii_uppercase)) | |
| if use_digits: | |
| character_pool.extend(string.digits) | |
| required_chars.append(secrets.choice(string.digits)) | |
| if use_symbols: | |
| # Define a standard set of symbols, avoid potentially problematic ones like `'"\ | |
| symbols = '!@#$%^&*()_-+={}[]|:;<>,.?/~' | |
| character_pool.extend(symbols) | |
| required_chars.append(secrets.choice(symbols)) | |
| if not character_pool: | |
| raise ValueError("No character types selected for password generation.") | |
| if length < len(required_chars): | |
| raise ValueError(f"Length ({length}) is too short to include all required character types ({len(required_chars)}).") | |
| # Fill the rest of the password length | |
| remaining_length = length - len(required_chars) | |
| password_chars = required_chars + [secrets.choice(character_pool) for _ in range(remaining_length)] | |
| # Shuffle the list to ensure randomness | |
| random.SystemRandom().shuffle(password_chars) # Use system random for better shuffling | |
| return "".join(password_chars) | |
| # --- Routes --- | |
| def home(): | |
| if current_user.is_authenticated: return redirect(url_for('add_password_page')) | |
| else: return redirect(url_for('login')) | |
| def login(): | |
| if current_user.is_authenticated: return redirect(url_for('add_password_page')) | |
| if request.method == 'POST': | |
| email = request.form.get('email', '').strip(); password = request.form.get('password', '') | |
| # ** GET REMEMBER ME VALUE ** | |
| remember_me = 'remember' in request.form # True if checkbox was ticked | |
| if not email or not password: | |
| flash('Email and Master Password are required.', 'danger'); return render_template('login.html'), 400 | |
| try: | |
| response = supabase.table('users').select("id, email, password_hash").eq('email', email).maybe_single().execute() | |
| if hasattr(response, 'data') and response.data: | |
| user_data = response.data; stored_hash = user_data['password_hash'] | |
| if bcrypt.check_password_hash(stored_hash, password): | |
| user_obj = User(id=user_data['id'], email=user_data['email']) | |
| # ** PASS REMEMBER ME TO login_user ** | |
| login_user(user_obj, remember=remember_me) | |
| # Flask-Login sets session.permanent based on 'remember' flag | |
| session['user_data'] = {'id': user_data['id'], 'email': user_data['email']} | |
| # Derive key and store URL-safe Base64 version in session | |
| derived_key_b64url = derive_key(password, user_data['email']) | |
| session['encryption_key'] = derived_key_b64url.decode('utf-8') | |
| flash('Logged in successfully!', 'success') | |
| next_page = request.args.get('next') | |
| return redirect(next_page or url_for('add_password_page')) | |
| else: flash('Login failed. Invalid email or password.', 'danger') | |
| else: flash('Login failed. Invalid email or password.', 'danger') | |
| return render_template('login.html'), 401 | |
| except Exception as e: | |
| flash(f'An error occurred during login. Please try again.', 'danger') | |
| print(f"Login Exception for {email}: {type(e).__name__} - {e}"); traceback.print_exc() | |
| return render_template('login.html'), 500 | |
| return render_template('login.html') | |
| def register(): | |
| if current_user.is_authenticated: return redirect(url_for('add_password_page')) | |
| if request.method == 'POST': | |
| email = request.form.get('email', '').strip() | |
| password = request.form.get('password', ''); confirm_password = request.form.get('confirm_password', '') | |
| if not email or not password or not confirm_password: flash('All fields are required.', 'danger'); return render_template('register.html'), 400 | |
| if password != confirm_password: flash('Passwords do not match.', 'danger'); return render_template('register.html'), 400 | |
| try: | |
| check_response = supabase.table('users').select("id", count='exact').eq('email', email).execute() | |
| if hasattr(check_response, 'count') and check_response.count is not None and check_response.count > 0: | |
| flash('Email address is already registered. Please login.', 'warning'); return redirect(url_for('login')) | |
| elif not hasattr(check_response, 'count'): print(f"WARNING: Supabase check for {email} lacked 'count': {check_response}") | |
| hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') | |
| insert_response = supabase.table('users').insert({'email': email, 'password_hash': hashed_password}).execute() | |
| if hasattr(insert_response, 'data') and insert_response.data: | |
| flash('Registration successful! Please log in.', 'success'); return redirect(url_for('login')) | |
| else: | |
| print(f"ERROR: Registration failed for {email}. Response: {insert_response}") | |
| flash("Registration failed due to a server error.", 'danger') | |
| return render_template('register.html'), 500 | |
| except Exception as e: | |
| flash(f'An error occurred during registration. Please try again.', 'danger') | |
| print(f"Registration Exception for {email}: {type(e).__name__} - {e}"); traceback.print_exc() | |
| return render_template('register.html'), 500 | |
| return render_template('register.html') | |
| def logout(): | |
| session.pop('encryption_key', None); session.pop('user_data', None) | |
| logout_user() | |
| flash('You have been logged out successfully.', 'success') | |
| return redirect(url_for('login')) | |
| # --- Main Application Pages (Require Login) --- | |
| def add_password_page(): return render_template('index.html') | |
| def storage(): return render_template('storage.html') | |
| def analyse(): return render_template('analyse.html') | |
| # --- API Endpoints (Require Login) --- | |
| def get_credentials(): | |
| user_id = current_user.id | |
| try: | |
| response = supabase.table('credentials').select("id, encrypted_data, service_hint, created_at").eq('user_id', user_id).order('created_at', desc=True).execute() | |
| # Check response structure for Supabase v2 (data attribute is primary) | |
| if hasattr(response, 'data'): | |
| return jsonify(response.data) | |
| else: # Fallback or error logging if structure changes | |
| print(f"Supabase get credentials unexpected response for user {user_id}. Response: {response}") | |
| return jsonify({'error': 'Failed to retrieve credentials'}), 500 | |
| except Exception as e: | |
| print(f"Error in get_credentials for user {user_id}: {e}"); traceback.print_exc() | |
| return jsonify({'error': 'Server error retrieving credentials'}), 500 | |
| def add_credential(): | |
| data = request.json; user_id = current_user.id | |
| encrypted_data = data.get('encrypted_data'); service_hint = data.get('service_hint') | |
| if not encrypted_data: return jsonify({'success': False, 'message': 'Encrypted data payload is required.'}), 400 | |
| try: | |
| insert_response = supabase.table('credentials').insert({'user_id': user_id, 'encrypted_data': encrypted_data, 'service_hint': service_hint}).execute() | |
| # Check response structure for Supabase v2 | |
| if hasattr(insert_response, 'data') and insert_response.data: | |
| return jsonify({'success': True, 'message': 'Credential saved successfully.'}) | |
| else: | |
| print(f"Supabase insert credentials error for user {user_id}. Response: {insert_response}") | |
| # Attempt to get more specific error if available (structure might vary) | |
| error_msg = "Failed to save credential" | |
| if hasattr(insert_response, 'error') and insert_response.error and hasattr(insert_response.error, 'message'): | |
| error_msg += f": {insert_response.error.message}" | |
| return jsonify({'success': False, 'message': error_msg}), 500 | |
| except Exception as e: | |
| print(f"Error in add_credential for user {user_id}: {e}"); traceback.print_exc() | |
| return jsonify({'success': False, 'message': 'Server error saving credential'}), 500 | |
| def analyse_password_api(): | |
| data = request.json; characteristics = data.get('characteristics') | |
| if not characteristics or not isinstance(characteristics, dict): | |
| return jsonify({'error': 'Password characteristics payload is required.'}), 400 | |
| analysis = get_llm_password_insights(characteristics) | |
| feedback = [] | |
| if 'insights' in analysis: feedback.extend([f"Issue: {ins}" for ins in analysis['insights'] if ins]) | |
| if 'suggestions' in analysis: feedback.extend([f"Tip: {sug}" for sug in analysis['suggestions'] if sug]) | |
| if not feedback: | |
| if analysis.get('strength', 0) == 5: feedback.append("Tip: Looks good based on characteristics!") | |
| else: feedback.append("Issue: Review password based on assessment.") | |
| return jsonify({ 'strength': analysis.get('strength', 1), 'assessment': analysis.get('assessment', 'Weak'), 'feedback': feedback }) | |
| # --- NEW: Password Generation API Endpoint --- | |
| # Using POST for consistency | |
| def generate_password_api(): | |
| try: | |
| data = request.get_json(silent=True) | |
| if data is None: | |
| data = request.args.to_dict() | |
| if not data: | |
| data = {} | |
| # Safely get length | |
| try: | |
| length_str = data.get('length', '16') # Default to string '16' | |
| length = int(length_str) | |
| if not (4 <= length <= 128): | |
| return jsonify({'error': 'Length must be between 4 and 128.'}), 400 | |
| except (ValueError, TypeError): | |
| return jsonify({'error': f'Invalid length parameter: "{length_str}". Must be an integer.'}), 400 | |
| # Helper for robust boolean parsing from various request inputs | |
| def parse_bool(key, default_value): | |
| value = data.get(key, default_value) # Get value or default | |
| if isinstance(value, bool): return value | |
| if isinstance(value, str): | |
| low_val = value.lower() | |
| if low_val in ['true', '1', 'yes', 'y']: return True | |
| if low_val in ['false', '0', 'no', 'n']: return False | |
| # Check for integer 1/0 as well | |
| if isinstance(value, int) and value in [0, 1]: | |
| return bool(value) | |
| # If it's none of the above, return the original default | |
| # print(f"Warning: Unexpected type for boolean parse '{key}': {type(value)}, using default: {default_value}") | |
| return default_value | |
| use_lowercase = parse_bool('use_lowercase', True) | |
| use_uppercase = parse_bool('use_uppercase', True) | |
| use_digits = parse_bool('use_digits', True) | |
| use_symbols = parse_bool('use_symbols', True) | |
| if not any([use_lowercase, use_uppercase, use_digits, use_symbols]): | |
| return jsonify({'error': 'At least one character type must be selected.'}), 400 | |
| password = generate_secure_password(length, use_lowercase, use_uppercase, use_digits, use_symbols) | |
| return jsonify({'password': password}) | |
| except ValueError as ve: | |
| print(f"Password generation validation error: {ve}") | |
| return jsonify({'error': str(ve)}), 400 | |
| except Exception as e: | |
| print(f"Unexpected error in generate_password_api: {type(e).__name__} - {e}") | |
| traceback.print_exc() | |
| return jsonify({'error': 'Server error generating password.'}), 500 | |
| # --- Helper for zxcvbn analysis --- | |
| # (Keep the import from zxcvbn at the top) | |
| # from zxcvbn import zxcvbn # Already should be there | |
| def get_zxcvbn_feedback(password): | |
| """Analyzes password using zxcvbn and formats feedback.""" | |
| results = zxcvbn(password) | |
| score = results['score'] # Score 0-4 | |
| feedback_items = [] | |
| # Map score to assessment | |
| assessment_map = { | |
| 0: "Very Weak", 1: "Weak", 2: "Medium", 3: "Strong", 4: "Very Strong" | |
| } | |
| assessment = assessment_map.get(score, "Unknown") | |
| # Add warning if present | |
| if results['feedback']['warning']: | |
| feedback_items.append(f"Warning: {results['feedback']['warning']}") | |
| # Add suggestions | |
| if results['feedback']['suggestions']: | |
| feedback_items.extend([f"Suggestion: {s}" for s in results['feedback']['suggestions']]) | |
| # Add a generic suggestion if feedback is empty but score is low | |
| if not feedback_items and score < 3: | |
| feedback_items.append("Suggestion: Add more variety (uppercase, numbers, symbols) or increase length.") | |
| elif not feedback_items and score >= 3: | |
| feedback_items.append("Suggestion: Looks reasonably strong!") | |
| # Return without calc_time which is not JSON serializable | |
| return { | |
| 'score': score, # 0-4 | |
| 'assessment': assessment, | |
| 'feedback': feedback_items, | |
| 'guesses': results['guesses'], # Optional: include estimated guesses if needed | |
| } | |
| def strength_check_api(): | |
| """Receives a password and returns its zxcvbn strength analysis.""" | |
| data = request.get_json() | |
| if not data or 'password' not in data: | |
| return jsonify({'error': 'Password key missing in request body.'}), 400 | |
| password = data['password'] | |
| if not password: # Handle empty password case | |
| return jsonify({'score': 0, 'assessment': 'Very Weak', 'feedback': ['Suggestion: Please enter a password.']}) | |
| try: | |
| analysis = get_zxcvbn_feedback(password) | |
| return jsonify(analysis) | |
| except Exception as e: | |
| print(f"Error during zxcvbn strength check: {e}") | |
| traceback.print_exc() | |
| # Return a generic low score on error, rather than crashing | |
| return jsonify({'score': 0, 'assessment': 'Error', 'feedback': ['Error analyzing password strength.']}), 500 | |
| if __name__ == '__main__': | |
| port = int(os.environ.get('PORT', 5000)) | |
| app.run(host='0.0.0.0', port=port, debug=app.config['DEBUG']) | |
| # --- END OF FILE app.py --- |