Features implemented: - Application factory pattern with environment-based config - 7 modular blueprints (main, auth, admin, players, groups, content, api) - Flask-Caching with Redis support for production - Flask-Login authentication with bcrypt password hashing - API endpoints with rate limiting and Bearer token auth - Comprehensive error handling and logging - CLI commands (init-db, create-admin, seed-db) Blueprint Structure: - main: Dashboard with caching, health check endpoint - auth: Login, register, logout, password change - admin: User management, system settings, theme, logo upload - players: Full CRUD, fullscreen view, bulk operations, playlist management - groups: Group management, player assignments, content management - content: Upload with progress tracking, file management, preview/download - api: RESTful endpoints with authentication, rate limiting, player feedback Performance Optimizations: - Dashboard caching (60s timeout) - Playlist caching (5min timeout) - Redis caching for production - Memoized functions for expensive operations - Cache clearing on data changes Security Features: - Bcrypt password hashing - Flask-Login session management - admin_required decorator for authorization - Player authentication via auth codes - API Bearer token authentication - Rate limiting on API endpoints (60 req/min default) - Input validation and sanitization Documentation: - README.md: Full project documentation with quick start - PROGRESS.md: Detailed progress tracking and roadmap - BLUEPRINT_GUIDE.md: Quick reference for blueprint architecture Pending work: - Models migration from v1 with database indexes - Utils migration from v1 with type hints - Templates migration with updated route references - Docker multi-stage build configuration - Unit tests for all blueprints Ready for models and utils migration from digiserver v1
148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
"""
|
|
Authentication Blueprint - Login, Logout, Register
|
|
"""
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
|
from flask_login import login_user, logout_user, login_required, current_user
|
|
from extensions import db, bcrypt
|
|
from models.user import User
|
|
from utils.logger import log_action, log_user_created
|
|
from typing import Optional
|
|
|
|
auth_bp = Blueprint('auth', __name__)
|
|
|
|
|
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
"""User login"""
|
|
# Redirect if already logged in
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
if request.method == 'POST':
|
|
username = request.form.get('username', '').strip()
|
|
password = request.form.get('password', '')
|
|
remember = request.form.get('remember', False)
|
|
|
|
# Validate input
|
|
if not username or not password:
|
|
flash('Please provide both username and password.', 'danger')
|
|
return render_template('auth/login.html')
|
|
|
|
# Find user
|
|
user = User.query.filter_by(username=username).first()
|
|
|
|
# Verify credentials
|
|
if user and bcrypt.check_password_hash(user.password, password):
|
|
login_user(user, remember=remember)
|
|
log_action(f'User {username} logged in')
|
|
|
|
# Redirect to next page or dashboard
|
|
next_page = request.args.get('next')
|
|
if next_page:
|
|
return redirect(next_page)
|
|
return redirect(url_for('main.dashboard'))
|
|
else:
|
|
flash('Invalid username or password.', 'danger')
|
|
log_action(f'Failed login attempt for username: {username}')
|
|
|
|
# Check for logo
|
|
import os
|
|
login_picture_exists = os.path.exists(
|
|
os.path.join(auth_bp.root_path or '.', 'static/resurse/login_picture.png')
|
|
)
|
|
|
|
return render_template('auth/login.html', login_picture_exists=login_picture_exists)
|
|
|
|
|
|
@auth_bp.route('/logout')
|
|
@login_required
|
|
def logout():
|
|
"""User logout"""
|
|
username = current_user.username
|
|
logout_user()
|
|
log_action(f'User {username} logged out')
|
|
flash('You have been logged out.', 'info')
|
|
return redirect(url_for('auth.login'))
|
|
|
|
|
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
|
def register():
|
|
"""User registration"""
|
|
# Redirect if already logged in
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
if request.method == 'POST':
|
|
username = request.form.get('username', '').strip()
|
|
password = request.form.get('password', '')
|
|
confirm_password = request.form.get('confirm_password', '')
|
|
|
|
# Validate input
|
|
if not username or not password:
|
|
flash('Username and password are required.', 'danger')
|
|
return render_template('auth/register.html')
|
|
|
|
if password != confirm_password:
|
|
flash('Passwords do not match.', 'danger')
|
|
return render_template('auth/register.html')
|
|
|
|
if len(password) < 6:
|
|
flash('Password must be at least 6 characters long.', 'danger')
|
|
return render_template('auth/register.html')
|
|
|
|
# Check if user already exists
|
|
if User.query.filter_by(username=username).first():
|
|
flash('Username already exists.', 'danger')
|
|
return render_template('auth/register.html')
|
|
|
|
# Create new user
|
|
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
|
new_user = User(
|
|
username=username,
|
|
password=hashed_password,
|
|
role='viewer' # Default role
|
|
)
|
|
|
|
db.session.add(new_user)
|
|
db.session.commit()
|
|
|
|
log_user_created(username, 'viewer')
|
|
flash('Registration successful! Please log in.', 'success')
|
|
return redirect(url_for('auth.login'))
|
|
|
|
return render_template('auth/register.html')
|
|
|
|
|
|
@auth_bp.route('/change-password', methods=['GET', 'POST'])
|
|
@login_required
|
|
def change_password():
|
|
"""Change user password"""
|
|
if request.method == 'POST':
|
|
current_password = request.form.get('current_password', '')
|
|
new_password = request.form.get('new_password', '')
|
|
confirm_password = request.form.get('confirm_password', '')
|
|
|
|
# Verify current password
|
|
if not bcrypt.check_password_hash(current_user.password, current_password):
|
|
flash('Current password is incorrect.', 'danger')
|
|
return render_template('auth/change_password.html')
|
|
|
|
# Validate new password
|
|
if new_password != confirm_password:
|
|
flash('New passwords do not match.', 'danger')
|
|
return render_template('auth/change_password.html')
|
|
|
|
if len(new_password) < 6:
|
|
flash('Password must be at least 6 characters long.', 'danger')
|
|
return render_template('auth/change_password.html')
|
|
|
|
# Update password
|
|
current_user.password = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
|
db.session.commit()
|
|
|
|
log_action(f'User {current_user.username} changed password')
|
|
flash('Password changed successfully.', 'success')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
return render_template('auth/change_password.html')
|