Phase 1 foundation: - DB: users table extended (password_hash, is_active, login_attempts, locked_until) - DB: user_oauth_tokens table (user_id, provider, access_token, refresh_token) - password_utils.py: SHA-256+salt hashing, verify, rate-limit lock check - auth.py: POST /auth/register + POST /auth/local-login + /auth/login?provider=local - Login page: tabs Login/Register with Gitea OAuth button - settings.html: profile (name/password), forges, API tokens, sessions - ALTER TABLE migrations for existing DBs
36 lines
1.0 KiB
Python
36 lines
1.0 KiB
Python
"""Password hashing and login security utilities."""
|
|
|
|
import hashlib
|
|
import secrets
|
|
import time
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
"""Hash a password using SHA-256 + random salt (16 bytes).
|
|
Format: salt_hex:hash_hex (64 + 64 = 128 chars)
|
|
Fallback for bcrypt — we use SHA-256 for SQLite simplicity
|
|
but with proper salt per password."""
|
|
salt = secrets.token_hex(16)
|
|
h = hashlib.sha256(f"{salt}:{password}".encode()).hexdigest()
|
|
return f"{salt}:{h}"
|
|
|
|
|
|
def verify_password(password: str, stored: str) -> bool:
|
|
"""Verify a password against its stored hash."""
|
|
try:
|
|
salt, h = stored.split(":", 1)
|
|
expected = hashlib.sha256(f"{salt}:{password}".encode()).hexdigest()
|
|
return h == expected
|
|
except (ValueError, AttributeError):
|
|
return False
|
|
|
|
|
|
def is_locked(locked_until: str | None) -> bool:
|
|
"""Check if account is temporarily locked."""
|
|
if not locked_until:
|
|
return False
|
|
try:
|
|
return float(locked_until) > time.time()
|
|
except (ValueError, TypeError):
|
|
return False
|