Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
180 lines
4.7 KiB
Python
180 lines
4.7 KiB
Python
"""
|
||
密碼服務模組
|
||
|
||
提供:
|
||
- 密碼複雜度驗證
|
||
- 密碼雜湊與驗證
|
||
- 密碼過期檢查
|
||
"""
|
||
|
||
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 已載入")
|