Add dynamic features: database models, authentication, and admin dashboard
This commit is contained in:
10
utils/__init__.py
Normal file
10
utils/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Utilities Package
|
||||
from .auth import login_required, admin_required, get_current_user
|
||||
from .helpers import flash_message, generate_slug, sanitize_html
|
||||
from .validators import validate_email, validate_password
|
||||
|
||||
__all__ = [
|
||||
'login_required', 'admin_required', 'get_current_user',
|
||||
'flash_message', 'generate_slug', 'sanitize_html',
|
||||
'validate_email', 'validate_password'
|
||||
]
|
||||
63
utils/auth.py
Normal file
63
utils/auth.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authentication Utilities
|
||||
|
||||
from functools import wraps
|
||||
from quart import session, request, redirect, url_for, jsonify
|
||||
from typing import Optional
|
||||
from models.user import User
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require login for a route"""
|
||||
@wraps(f)
|
||||
async def decorated_function(*args, **kwargs):
|
||||
if 'user_id' not in session:
|
||||
if request.is_json:
|
||||
return jsonify({'error': 'Login required'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
return await f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def admin_required(f):
|
||||
"""Decorator to require admin role for a route"""
|
||||
@wraps(f)
|
||||
async def decorated_function(*args, **kwargs):
|
||||
if 'user_id' not in session:
|
||||
if request.is_json:
|
||||
return jsonify({'error': 'Login required'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
user = await get_current_user()
|
||||
if not user or not user.is_admin:
|
||||
if request.is_json:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
return redirect(url_for('home.index'))
|
||||
|
||||
return await f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
async def get_current_user() -> Optional[User]:
|
||||
"""Get the currently logged-in user"""
|
||||
if 'user_id' not in session:
|
||||
return None
|
||||
|
||||
try:
|
||||
user_id = session['user_id']
|
||||
return await User.find_by_id(user_id)
|
||||
except:
|
||||
# Clear invalid session
|
||||
session.pop('user_id', None)
|
||||
return None
|
||||
|
||||
def login_user(user: User):
|
||||
"""Log in a user (set session)"""
|
||||
session['user_id'] = user.id
|
||||
session['username'] = user.username
|
||||
session['is_admin'] = user.is_admin
|
||||
|
||||
def logout_user():
|
||||
"""Log out the current user (clear session)"""
|
||||
session.pop('user_id', None)
|
||||
session.pop('username', None)
|
||||
session.pop('is_admin', None)
|
||||
74
utils/helpers.py
Normal file
74
utils/helpers.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Helper Utilities
|
||||
|
||||
import re
|
||||
import html
|
||||
from quart import session
|
||||
from typing import List
|
||||
|
||||
def flash_message(message: str, category: str = 'info'):
|
||||
"""Add a flash message to session"""
|
||||
if 'flash_messages' not in session:
|
||||
session['flash_messages'] = []
|
||||
session['flash_messages'].append({'message': message, 'category': category})
|
||||
|
||||
def get_flash_messages() -> List[dict]:
|
||||
"""Get and clear flash messages from session"""
|
||||
messages = session.pop('flash_messages', [])
|
||||
return messages
|
||||
|
||||
def generate_slug(text: str) -> str:
|
||||
"""Generate URL-friendly slug from text"""
|
||||
# Remove HTML tags
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
# Convert to lowercase and replace spaces/special chars with hyphens
|
||||
slug = re.sub(r'[^\w\s-]', '', text).strip().lower()
|
||||
slug = re.sub(r'[\s_-]+', '-', slug)
|
||||
# Remove leading/trailing hyphens
|
||||
slug = slug.strip('-')
|
||||
return slug
|
||||
|
||||
def sanitize_html(text: str) -> str:
|
||||
"""Basic HTML sanitization"""
|
||||
if not text:
|
||||
return ''
|
||||
# Escape HTML entities
|
||||
return html.escape(text)
|
||||
|
||||
def truncate_text(text: str, max_length: int = 150, suffix: str = '...') -> str:
|
||||
"""Truncate text to specified length"""
|
||||
if not text or len(text) <= max_length:
|
||||
return text
|
||||
return text[:max_length].rsplit(' ', 1)[0] + suffix
|
||||
|
||||
def format_date(date_obj, format_str: str = '%d/%m/%Y') -> str:
|
||||
"""Format datetime object to string"""
|
||||
if not date_obj:
|
||||
return ''
|
||||
return date_obj.strftime(format_str)
|
||||
|
||||
def paginate_query_params(page: int = 1, per_page: int = 10) -> tuple:
|
||||
"""Generate LIMIT and OFFSET for pagination"""
|
||||
if page < 1:
|
||||
page = 1
|
||||
offset = (page - 1) * per_page
|
||||
return per_page, offset
|
||||
|
||||
def calculate_pagination(total_items: int, page: int, per_page: int) -> dict:
|
||||
"""Calculate pagination information"""
|
||||
total_pages = (total_items + per_page - 1) // per_page
|
||||
has_prev = page > 1
|
||||
has_next = page < total_pages
|
||||
|
||||
return {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total_pages': total_pages,
|
||||
'total_items': total_items,
|
||||
'has_prev': has_prev,
|
||||
'has_next': has_next,
|
||||
'prev_page': page - 1 if has_prev else None,
|
||||
'next_page': page + 1 if has_next else None
|
||||
}
|
||||
113
utils/validators.py
Normal file
113
utils/validators.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Validation Utilities
|
||||
|
||||
import re
|
||||
from typing import Tuple, Optional
|
||||
|
||||
def validate_email(email: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Validate email format"""
|
||||
if not email:
|
||||
return False, "Email è richiesta"
|
||||
|
||||
# Basic email regex pattern
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
|
||||
if not re.match(pattern, email):
|
||||
return False, "Formato email non valido"
|
||||
|
||||
if len(email) > 100:
|
||||
return False, "Email troppo lunga (max 100 caratteri)"
|
||||
|
||||
return True, None
|
||||
|
||||
def validate_password(password: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Validate password strength"""
|
||||
if not password:
|
||||
return False, "Password è richiesta"
|
||||
|
||||
if len(password) < 8:
|
||||
return False, "Password deve essere almeno 8 caratteri"
|
||||
|
||||
if len(password) > 128:
|
||||
return False, "Password troppo lunga (max 128 caratteri)"
|
||||
|
||||
# Check for at least one uppercase letter
|
||||
if not re.search(r'[A-Z]', password):
|
||||
return False, "Password deve contenere almeno una lettera maiuscola"
|
||||
|
||||
# Check for at least one lowercase letter
|
||||
if not re.search(r'[a-z]', password):
|
||||
return False, "Password deve contenere almeno una lettera minuscola"
|
||||
|
||||
# Check for at least one digit
|
||||
if not re.search(r'\d', password):
|
||||
return False, "Password deve contenere almeno un numero"
|
||||
|
||||
return True, None
|
||||
|
||||
def validate_username(username: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Validate username format"""
|
||||
if not username:
|
||||
return False, "Username è richiesto"
|
||||
|
||||
if len(username) < 3:
|
||||
return False, "Username deve essere almeno 3 caratteri"
|
||||
|
||||
if len(username) > 50:
|
||||
return False, "Username troppo lungo (max 50 caratteri)"
|
||||
|
||||
# Check for valid characters (alphanumeric and underscore)
|
||||
if not re.match(r'^[a-zA-Z0-9_]+$', username):
|
||||
return False, "Username può contenere solo lettere, numeri e underscore"
|
||||
|
||||
return True, None
|
||||
|
||||
def validate_project_data(data: dict) -> Tuple[bool, dict]:
|
||||
"""Validate project data"""
|
||||
errors = {}
|
||||
|
||||
# Title validation
|
||||
if not data.get('title', '').strip():
|
||||
errors['title'] = 'Titolo è richiesto'
|
||||
elif len(data['title']) > 200:
|
||||
errors['title'] = 'Titolo troppo lungo (max 200 caratteri)'
|
||||
|
||||
# Description validation
|
||||
if data.get('description') and len(data['description']) > 1000:
|
||||
errors['description'] = 'Descrizione troppo lunga (max 1000 caratteri)'
|
||||
|
||||
# URL validations
|
||||
url_pattern = r'^https?://[^\s]+$'
|
||||
|
||||
if data.get('github_url') and not re.match(url_pattern, data['github_url']):
|
||||
errors['github_url'] = 'URL GitHub non valido'
|
||||
|
||||
if data.get('demo_url') and not re.match(url_pattern, data['demo_url']):
|
||||
errors['demo_url'] = 'URL Demo non valido'
|
||||
|
||||
if data.get('image_url') and not re.match(url_pattern, data['image_url']):
|
||||
errors['image_url'] = 'URL Immagine non valido'
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def validate_post_data(data: dict) -> Tuple[bool, dict]:
|
||||
"""Validate blog post data"""
|
||||
errors = {}
|
||||
|
||||
# Title validation
|
||||
if not data.get('title', '').strip():
|
||||
errors['title'] = 'Titolo è richiesto'
|
||||
elif len(data['title']) > 200:
|
||||
errors['title'] = 'Titolo troppo lungo (max 200 caratteri)'
|
||||
|
||||
# Content validation
|
||||
if not data.get('content', '').strip():
|
||||
errors['content'] = 'Contenuto è richiesto'
|
||||
|
||||
# Excerpt validation
|
||||
if data.get('excerpt') and len(data['excerpt']) > 500:
|
||||
errors['excerpt'] = 'Riassunto troppo lungo (max 500 caratteri)'
|
||||
|
||||
return len(errors) == 0, errors
|
||||
Reference in New Issue
Block a user