""" 密碼服務模組 提供: - 密碼複雜度驗證 - 密碼雜湊與驗證 - 密碼過期檢查 """ import re from datetime import datetime, timedelta from werkzeug.security import generate_password_hash, check_password_hash from config import ( PASSWORD_MIN_LENGTH, PASSWORD_REQUIRE_UPPERCASE, PASSWORD_REQUIRE_LOWERCASE, PASSWORD_REQUIRE_DIGIT, PASSWORD_REQUIRE_SPECIAL, PASSWORD_SPECIAL_CHARS, PASSWORD_EXPIRY_DAYS, ) def validate_password_complexity(password): """ 驗證密碼複雜度 根據 config.py 中的設定檢查密碼是否符合要求: - 最小長度 - 大寫字母 - 小寫字母 - 數字 - 特殊符號 Args: password: 要驗證的密碼 Returns: tuple: (is_valid, error_message) - is_valid: 是否符合要求 - error_message: 錯誤訊息(符合時為 None) """ if not password: return False, "密碼不能為空" errors = [] # 檢查長度 if len(password) < PASSWORD_MIN_LENGTH: errors.append(f"密碼長度至少需要 {PASSWORD_MIN_LENGTH} 個字元") # 檢查大寫字母 if PASSWORD_REQUIRE_UPPERCASE and not re.search(r'[A-Z]', password): errors.append("密碼必須包含至少一個大寫字母") # 檢查小寫字母 if PASSWORD_REQUIRE_LOWERCASE and not re.search(r'[a-z]', password): errors.append("密碼必須包含至少一個小寫字母") # 檢查數字 if PASSWORD_REQUIRE_DIGIT and not re.search(r'\d', password): errors.append("密碼必須包含至少一個數字") # 檢查特殊符號 if PASSWORD_REQUIRE_SPECIAL: # 轉義特殊字元用於正則表達式 escaped_chars = re.escape(PASSWORD_SPECIAL_CHARS) if not re.search(f'[{escaped_chars}]', password): errors.append(f"密碼必須包含至少一個特殊符號 ({PASSWORD_SPECIAL_CHARS})") if errors: return False, "、".join(errors) return True, None def get_password_requirements(): """ 取得密碼複雜度要求說明 Returns: list: 密碼要求的說明列表 """ requirements = [f"至少 {PASSWORD_MIN_LENGTH} 個字元"] if PASSWORD_REQUIRE_UPPERCASE: requirements.append("至少一個大寫字母 (A-Z)") if PASSWORD_REQUIRE_LOWERCASE: requirements.append("至少一個小寫字母 (a-z)") if PASSWORD_REQUIRE_DIGIT: requirements.append("至少一個數字 (0-9)") if PASSWORD_REQUIRE_SPECIAL: requirements.append(f"至少一個特殊符號 ({PASSWORD_SPECIAL_CHARS})") return requirements def hash_password(password): """ 將密碼進行雜湊處理 Args: password: 明文密碼 Returns: str: 雜湊後的密碼 """ return generate_password_hash(password, method='pbkdf2:sha256') def verify_password(password_hash, password): """ 驗證密碼是否正確 Args: password_hash: 資料庫中儲存的雜湊密碼 password: 用戶輸入的明文密碼 Returns: bool: 密碼是否正確 """ return check_password_hash(password_hash, password) def is_password_expired(password_changed_at): """ 檢查密碼是否已過期 Args: password_changed_at: 密碼上次變更時間 (datetime 或 None) Returns: tuple: (is_expired, days_until_expiry) - is_expired: 是否已過期 - days_until_expiry: 距離過期還有幾天(已過期時為負數) """ # 如果沒有設定過期天數(0 或 None),則永不過期 if not PASSWORD_EXPIRY_DAYS: return False, None # 如果沒有密碼變更時間,視為需要立即變更 if not password_changed_at: return True, 0 expiry_date = password_changed_at + timedelta(days=PASSWORD_EXPIRY_DAYS) now = datetime.now() days_until_expiry = (expiry_date - now).days if days_until_expiry < 0: return True, days_until_expiry else: return False, days_until_expiry def get_password_expiry_warning(password_changed_at, warning_days=14): """ 取得密碼過期警告訊息 Args: password_changed_at: 密碼上次變更時間 warning_days: 幾天前開始警告 Returns: str or None: 警告訊息,若不需警告則為 None """ is_expired, days_until = is_password_expired(password_changed_at) if is_expired: if days_until is None: return None # 永不過期 return f"您的密碼已過期,請立即變更密碼。" if days_until is not None and days_until <= warning_days: return f"您的密碼將在 {days_until} 天後過期,請及早變更密碼。" return None print("✅ Password service 已載入")