Add comprehensive backend features and mobile UI improvements
Backend: - Add 2FA authentication with TOTP support - Add API keys management system - Add audit logging for security events - Add file upload/management system - Add notifications system with preferences - Add session management - Add webhooks integration - Add analytics endpoints - Add export functionality - Add password policy enforcement - Add new database migrations for core tables Frontend: - Add module position system (top/bottom sidebar sections) - Add search and notifications module configuration tabs - Add mobile logo replacing hamburger menu - Center page title absolutely when no tabs present - Align sidebar footer toggles with navigation items - Add lighter icon color in dark theme for mobile - Add API keys management page - Add notifications page with context - Add admin analytics and audit logs pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
240
backend/app/core/password_policy.py
Normal file
240
backend/app/core/password_policy.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Password policy validation and enforcement."""
|
||||
|
||||
import re
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud
|
||||
|
||||
|
||||
@dataclass
|
||||
class PasswordPolicy:
|
||||
"""Password policy configuration."""
|
||||
min_length: int = 8
|
||||
max_length: int = 128
|
||||
require_uppercase: bool = True
|
||||
require_lowercase: bool = True
|
||||
require_digit: bool = True
|
||||
require_special: bool = False
|
||||
special_characters: str = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
disallow_username: bool = True
|
||||
disallow_email: bool = True
|
||||
disallow_common: bool = True
|
||||
|
||||
|
||||
# Common passwords to disallow
|
||||
COMMON_PASSWORDS = {
|
||||
"password", "123456", "12345678", "qwerty", "abc123",
|
||||
"monkey", "1234567", "letmein", "trustno1", "dragon",
|
||||
"baseball", "iloveyou", "master", "sunshine", "ashley",
|
||||
"bailey", "passw0rd", "shadow", "123123", "654321",
|
||||
"superman", "qazwsx", "michael", "football", "password1",
|
||||
"password123", "welcome", "jesus", "ninja", "mustang",
|
||||
"admin", "admin123", "root", "toor", "administrator"
|
||||
}
|
||||
|
||||
|
||||
def get_password_policy(db: Optional[Session] = None) -> PasswordPolicy:
|
||||
"""Get password policy from settings or use defaults."""
|
||||
policy = PasswordPolicy()
|
||||
|
||||
if db:
|
||||
try:
|
||||
# Load settings from database
|
||||
min_length = crud.settings.get_setting_value(db, "password_min_length")
|
||||
if min_length is not None:
|
||||
policy.min_length = int(min_length)
|
||||
|
||||
require_uppercase = crud.settings.get_setting_value(db, "password_require_uppercase")
|
||||
if require_uppercase is not None:
|
||||
policy.require_uppercase = str(require_uppercase).lower() in ("true", "1")
|
||||
|
||||
require_lowercase = crud.settings.get_setting_value(db, "password_require_lowercase")
|
||||
if require_lowercase is not None:
|
||||
policy.require_lowercase = str(require_lowercase).lower() in ("true", "1")
|
||||
|
||||
require_digit = crud.settings.get_setting_value(db, "password_require_digit")
|
||||
if require_digit is not None:
|
||||
policy.require_digit = str(require_digit).lower() in ("true", "1")
|
||||
|
||||
require_special = crud.settings.get_setting_value(db, "password_require_special")
|
||||
if require_special is not None:
|
||||
policy.require_special = str(require_special).lower() in ("true", "1")
|
||||
|
||||
except Exception:
|
||||
pass # Use defaults on error
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def validate_password(
|
||||
password: str,
|
||||
policy: Optional[PasswordPolicy] = None,
|
||||
username: Optional[str] = None,
|
||||
email: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
Validate password against policy.
|
||||
Returns list of validation errors (empty list if valid).
|
||||
"""
|
||||
if policy is None:
|
||||
policy = PasswordPolicy()
|
||||
|
||||
errors = []
|
||||
|
||||
# Length checks
|
||||
if len(password) < policy.min_length:
|
||||
errors.append(f"Password must be at least {policy.min_length} characters long")
|
||||
|
||||
if len(password) > policy.max_length:
|
||||
errors.append(f"Password must not exceed {policy.max_length} characters")
|
||||
|
||||
# Character requirements
|
||||
if policy.require_uppercase and not re.search(r"[A-Z]", password):
|
||||
errors.append("Password must contain at least one uppercase letter")
|
||||
|
||||
if policy.require_lowercase and not re.search(r"[a-z]", password):
|
||||
errors.append("Password must contain at least one lowercase letter")
|
||||
|
||||
if policy.require_digit and not re.search(r"\d", password):
|
||||
errors.append("Password must contain at least one digit")
|
||||
|
||||
if policy.require_special:
|
||||
special_pattern = f"[{re.escape(policy.special_characters)}]"
|
||||
if not re.search(special_pattern, password):
|
||||
errors.append("Password must contain at least one special character")
|
||||
|
||||
# Disallow username/email in password
|
||||
password_lower = password.lower()
|
||||
|
||||
if policy.disallow_username and username:
|
||||
if username.lower() in password_lower:
|
||||
errors.append("Password must not contain your username")
|
||||
|
||||
if policy.disallow_email and email:
|
||||
email_parts = email.lower().split("@")
|
||||
if email_parts[0] in password_lower:
|
||||
errors.append("Password must not contain your email address")
|
||||
|
||||
# Disallow common passwords
|
||||
if policy.disallow_common:
|
||||
if password_lower in COMMON_PASSWORDS:
|
||||
errors.append("Password is too common. Please choose a stronger password")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def get_password_requirements(policy: Optional[PasswordPolicy] = None) -> dict:
|
||||
"""Get password requirements as a dictionary (for frontend display)."""
|
||||
if policy is None:
|
||||
policy = PasswordPolicy()
|
||||
|
||||
requirements = {
|
||||
"min_length": policy.min_length,
|
||||
"max_length": policy.max_length,
|
||||
"require_uppercase": policy.require_uppercase,
|
||||
"require_lowercase": policy.require_lowercase,
|
||||
"require_digit": policy.require_digit,
|
||||
"require_special": policy.require_special,
|
||||
}
|
||||
|
||||
# Generate human-readable description
|
||||
rules = [f"At least {policy.min_length} characters"]
|
||||
|
||||
if policy.require_uppercase:
|
||||
rules.append("At least one uppercase letter (A-Z)")
|
||||
if policy.require_lowercase:
|
||||
rules.append("At least one lowercase letter (a-z)")
|
||||
if policy.require_digit:
|
||||
rules.append("At least one digit (0-9)")
|
||||
if policy.require_special:
|
||||
rules.append(f"At least one special character ({policy.special_characters[:10]}...)")
|
||||
|
||||
requirements["rules"] = rules
|
||||
|
||||
return requirements
|
||||
|
||||
|
||||
def check_password_strength(password: str) -> dict:
|
||||
"""
|
||||
Check password strength and return a score.
|
||||
Returns dict with score (0-100), level (weak/medium/strong/very_strong), and feedback.
|
||||
"""
|
||||
score = 0
|
||||
feedback = []
|
||||
|
||||
# Length scoring
|
||||
length = len(password)
|
||||
if length >= 8:
|
||||
score += 20
|
||||
if length >= 12:
|
||||
score += 10
|
||||
if length >= 16:
|
||||
score += 10
|
||||
|
||||
# Character variety scoring
|
||||
has_upper = bool(re.search(r"[A-Z]", password))
|
||||
has_lower = bool(re.search(r"[a-z]", password))
|
||||
has_digit = bool(re.search(r"\d", password))
|
||||
has_special = bool(re.search(r"[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]", password))
|
||||
|
||||
if has_upper:
|
||||
score += 15
|
||||
if has_lower:
|
||||
score += 15
|
||||
if has_digit:
|
||||
score += 15
|
||||
if has_special:
|
||||
score += 15
|
||||
|
||||
# Bonus for mixing character types
|
||||
variety = sum([has_upper, has_lower, has_digit, has_special])
|
||||
if variety >= 3:
|
||||
score += 10
|
||||
if variety == 4:
|
||||
score += 10
|
||||
|
||||
# Penalties
|
||||
if password.lower() in COMMON_PASSWORDS:
|
||||
score = min(score, 20)
|
||||
feedback.append("This is a commonly used password")
|
||||
|
||||
if re.search(r"(.)\1{2,}", password):
|
||||
score -= 10
|
||||
feedback.append("Avoid repeated characters")
|
||||
|
||||
if re.search(r"(012|123|234|345|456|567|678|789|890|abc|bcd|cde|def)", password.lower()):
|
||||
score -= 10
|
||||
feedback.append("Avoid sequential characters")
|
||||
|
||||
# Ensure score is in valid range
|
||||
score = max(0, min(100, score))
|
||||
|
||||
# Determine strength level
|
||||
if score >= 80:
|
||||
level = "very_strong"
|
||||
elif score >= 60:
|
||||
level = "strong"
|
||||
elif score >= 40:
|
||||
level = "medium"
|
||||
else:
|
||||
level = "weak"
|
||||
|
||||
# Add suggestions
|
||||
if not has_upper:
|
||||
feedback.append("Add uppercase letters")
|
||||
if not has_lower:
|
||||
feedback.append("Add lowercase letters")
|
||||
if not has_digit:
|
||||
feedback.append("Add numbers")
|
||||
if not has_special:
|
||||
feedback.append("Add special characters")
|
||||
if length < 12:
|
||||
feedback.append("Use a longer password")
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"level": level,
|
||||
"feedback": feedback[:3] # Limit to top 3 suggestions
|
||||
}
|
||||
@@ -310,6 +310,57 @@ register_setting(SettingDefinition(
|
||||
category="auth"
|
||||
))
|
||||
|
||||
# Password policy settings
|
||||
register_setting(SettingDefinition(
|
||||
key="password_min_length",
|
||||
type=SettingType.INTEGER,
|
||||
scope=SettingScope.GLOBAL,
|
||||
storage=SettingStorage.DATABASE,
|
||||
default=8,
|
||||
description="Minimum password length",
|
||||
category="security"
|
||||
))
|
||||
|
||||
register_setting(SettingDefinition(
|
||||
key="password_require_uppercase",
|
||||
type=SettingType.BOOLEAN,
|
||||
scope=SettingScope.GLOBAL,
|
||||
storage=SettingStorage.DATABASE,
|
||||
default=True,
|
||||
description="Require uppercase letters in passwords",
|
||||
category="security"
|
||||
))
|
||||
|
||||
register_setting(SettingDefinition(
|
||||
key="password_require_lowercase",
|
||||
type=SettingType.BOOLEAN,
|
||||
scope=SettingScope.GLOBAL,
|
||||
storage=SettingStorage.DATABASE,
|
||||
default=True,
|
||||
description="Require lowercase letters in passwords",
|
||||
category="security"
|
||||
))
|
||||
|
||||
register_setting(SettingDefinition(
|
||||
key="password_require_digit",
|
||||
type=SettingType.BOOLEAN,
|
||||
scope=SettingScope.GLOBAL,
|
||||
storage=SettingStorage.DATABASE,
|
||||
default=True,
|
||||
description="Require digits in passwords",
|
||||
category="security"
|
||||
))
|
||||
|
||||
register_setting(SettingDefinition(
|
||||
key="password_require_special",
|
||||
type=SettingType.BOOLEAN,
|
||||
scope=SettingScope.GLOBAL,
|
||||
storage=SettingStorage.DATABASE,
|
||||
default=False,
|
||||
description="Require special characters in passwords",
|
||||
category="security"
|
||||
))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UI/LAYOUT SETTINGS (Global, Database)
|
||||
|
||||
Reference in New Issue
Block a user