Add authentication system and admin dashboard

Security Features:
- Added User model with bcrypt password hashing
- Implemented Flask-Login for session management
- Protected all API write operations with @login_required decorator
- Added authentication routes (login/logout)

Admin Dashboard:
- Created comprehensive admin dashboard with statistics
- Profile management interface
- Skills management (add/edit/delete)
- Projects management with full CRUD operations
- Social links management
- Modern responsive UI with Bootstrap 5

New Files:
- models.py: Added User model with bcrypt
- routes/auth.py: Login/logout functionality
- routes/admin.py: Complete admin dashboard with CRUD operations
- templates/auth/login.html: Login page
- templates/admin/base.html: Admin base template
- templates/admin/dashboard.html: Main dashboard
- templates/admin/profile.html: Profile editor
- templates/admin/skills.html: Skills manager
- templates/admin/projects.html: Projects list
- templates/admin/project_form.html: Project editor
- templates/admin/social_links.html: Social links manager

Modified Files:
- app.py: Integrated Flask-Login and bcrypt, registered new blueprints
- requirements.txt: Added Flask-Login, Flask-Bcrypt, bcrypt
- init_db.py: Creates default admin user (admin/admin123)
- routes/api.py: Protected all write operations with authentication

Default Credentials:
- Username: admin
- Password: admin123
- ⚠️ MUST be changed after first login!

Benefits:
- Secure API access with session-based authentication
- User-friendly admin interface for content management
- No need to edit code or database directly
- Bcrypt password hashing for security
- Protected against unauthorized access
This commit is contained in:
Claude
2025-11-13 13:49:36 +00:00
parent c6425235a2
commit aa2c704bfb
15 changed files with 1159 additions and 4 deletions

285
routes/admin.py Normal file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright Hersel Giannella
"""
Admin dashboard routes for managing portfolio content
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from models import db, Profile, Skill, Project, ProjectTag, SocialLink
route_admin = Blueprint('admin', __name__, url_prefix='/admin')
@route_admin.route('/')
@login_required
def dashboard():
"""Admin dashboard home"""
# Statistiche
stats = {
'projects': Project.query.count(),
'skills': Skill.query.count(),
'social_links': SocialLink.query.count(),
'published_projects': Project.query.filter_by(is_published=True).count()
}
return render_template('admin/dashboard.html', stats=stats)
# ============================================================================
# PROFILE MANAGEMENT
# ============================================================================
@route_admin.route('/profile')
@login_required
def profile_manage():
"""Manage profile information"""
profile = Profile.query.first()
return render_template('admin/profile.html', profile=profile)
@route_admin.route('/profile/edit', methods=['POST'])
@login_required
def profile_edit():
"""Edit profile information"""
profile = Profile.query.first()
if not profile:
flash('Profilo non trovato.', 'danger')
return redirect(url_for('admin.profile_manage'))
profile.title = request.form.get('title', profile.title)
profile.lead_text = request.form.get('lead_text', profile.lead_text)
profile.description_1 = request.form.get('description_1', profile.description_1)
profile.description_2 = request.form.get('description_2', profile.description_2)
profile.years_experience = int(request.form.get('years_experience', profile.years_experience))
profile.cv_url = request.form.get('cv_url', profile.cv_url)
db.session.commit()
flash('Profilo aggiornato con successo!', 'success')
return redirect(url_for('admin.profile_manage'))
# ============================================================================
# SKILLS MANAGEMENT
# ============================================================================
@route_admin.route('/skills')
@login_required
def skills_manage():
"""Manage skills"""
skills = Skill.query.order_by(Skill.display_order).all()
return render_template('admin/skills.html', skills=skills)
@route_admin.route('/skills/add', methods=['POST'])
@login_required
def skill_add():
"""Add new skill"""
skill = Skill(
name=request.form.get('name'),
icon_class=request.form.get('icon_class'),
category=request.form.get('category'),
display_order=int(request.form.get('display_order', 0)),
is_active=request.form.get('is_active') == 'on'
)
db.session.add(skill)
db.session.commit()
flash('Skill aggiunta con successo!', 'success')
return redirect(url_for('admin.skills_manage'))
@route_admin.route('/skills/<int:skill_id>/edit', methods=['POST'])
@login_required
def skill_edit(skill_id):
"""Edit skill"""
skill = Skill.query.get_or_404(skill_id)
skill.name = request.form.get('name', skill.name)
skill.icon_class = request.form.get('icon_class', skill.icon_class)
skill.category = request.form.get('category', skill.category)
skill.display_order = int(request.form.get('display_order', skill.display_order))
skill.is_active = request.form.get('is_active') == 'on'
db.session.commit()
flash('Skill aggiornata con successo!', 'success')
return redirect(url_for('admin.skills_manage'))
@route_admin.route('/skills/<int:skill_id>/delete', methods=['POST'])
@login_required
def skill_delete(skill_id):
"""Delete skill"""
skill = Skill.query.get_or_404(skill_id)
db.session.delete(skill)
db.session.commit()
flash('Skill eliminata con successo!', 'success')
return redirect(url_for('admin.skills_manage'))
# ============================================================================
# PROJECTS MANAGEMENT
# ============================================================================
@route_admin.route('/projects')
@login_required
def projects_manage():
"""Manage projects"""
projects = Project.query.order_by(Project.display_order).all()
return render_template('admin/projects.html', projects=projects)
@route_admin.route('/projects/add', methods=['GET', 'POST'])
@login_required
def project_add():
"""Add new project"""
if request.method == 'POST':
project = Project(
title=request.form.get('title'),
description=request.form.get('description'),
image_url=request.form.get('image_url'),
demo_url=request.form.get('demo_url'),
github_url=request.form.get('github_url'),
display_order=int(request.form.get('display_order', 0)),
animation_delay=request.form.get('animation_delay', '0s'),
is_published=request.form.get('is_published') == 'on'
)
db.session.add(project)
db.session.flush()
# Aggiungi tags
tags_input = request.form.get('tags', '')
if tags_input:
tags_list = [tag.strip() for tag in tags_input.split(',') if tag.strip()]
for idx, tag_name in enumerate(tags_list):
# Estrai colore se specificato (formato: "Python:bg-primary")
if ':' in tag_name:
tag_name, color = tag_name.split(':', 1)
else:
color = 'bg-primary'
tag = ProjectTag(
project_id=project.id,
name=tag_name.strip(),
color_class=color.strip(),
display_order=idx
)
db.session.add(tag)
db.session.commit()
flash('Progetto aggiunto con successo!', 'success')
return redirect(url_for('admin.projects_manage'))
return render_template('admin/project_form.html', project=None)
@route_admin.route('/projects/<int:project_id>/edit', methods=['GET', 'POST'])
@login_required
def project_edit(project_id):
"""Edit project"""
project = Project.query.get_or_404(project_id)
if request.method == 'POST':
project.title = request.form.get('title', project.title)
project.description = request.form.get('description', project.description)
project.image_url = request.form.get('image_url', project.image_url)
project.demo_url = request.form.get('demo_url', project.demo_url)
project.github_url = request.form.get('github_url', project.github_url)
project.display_order = int(request.form.get('display_order', project.display_order))
project.animation_delay = request.form.get('animation_delay', project.animation_delay)
project.is_published = request.form.get('is_published') == 'on'
# Aggiorna tags
ProjectTag.query.filter_by(project_id=project.id).delete()
tags_input = request.form.get('tags', '')
if tags_input:
tags_list = [tag.strip() for tag in tags_input.split(',') if tag.strip()]
for idx, tag_name in enumerate(tags_list):
if ':' in tag_name:
tag_name, color = tag_name.split(':', 1)
else:
color = 'bg-primary'
tag = ProjectTag(
project_id=project.id,
name=tag_name.strip(),
color_class=color.strip(),
display_order=idx
)
db.session.add(tag)
db.session.commit()
flash('Progetto aggiornato con successo!', 'success')
return redirect(url_for('admin.projects_manage'))
return render_template('admin/project_form.html', project=project)
@route_admin.route('/projects/<int:project_id>/delete', methods=['POST'])
@login_required
def project_delete(project_id):
"""Delete project"""
project = Project.query.get_or_404(project_id)
db.session.delete(project)
db.session.commit()
flash('Progetto eliminato con successo!', 'success')
return redirect(url_for('admin.projects_manage'))
# ============================================================================
# SOCIAL LINKS MANAGEMENT
# ============================================================================
@route_admin.route('/social-links')
@login_required
def social_links_manage():
"""Manage social links"""
social_links = SocialLink.query.order_by(SocialLink.display_order).all()
return render_template('admin/social_links.html', social_links=social_links)
@route_admin.route('/social-links/add', methods=['POST'])
@login_required
def social_link_add():
"""Add new social link"""
link = SocialLink(
platform_name=request.form.get('platform_name'),
url=request.form.get('url'),
icon_class=request.form.get('icon_class'),
display_order=int(request.form.get('display_order', 0)),
animation_delay=request.form.get('animation_delay', '0s'),
is_active=request.form.get('is_active') == 'on'
)
db.session.add(link)
db.session.commit()
flash('Link social aggiunto con successo!', 'success')
return redirect(url_for('admin.social_links_manage'))
@route_admin.route('/social-links/<int:link_id>/edit', methods=['POST'])
@login_required
def social_link_edit(link_id):
"""Edit social link"""
link = SocialLink.query.get_or_404(link_id)
link.platform_name = request.form.get('platform_name', link.platform_name)
link.url = request.form.get('url', link.url)
link.icon_class = request.form.get('icon_class', link.icon_class)
link.display_order = int(request.form.get('display_order', link.display_order))
link.animation_delay = request.form.get('animation_delay', link.animation_delay)
link.is_active = request.form.get('is_active') == 'on'
db.session.commit()
flash('Link social aggiornato con successo!', 'success')
return redirect(url_for('admin.social_links_manage'))
@route_admin.route('/social-links/<int:link_id>/delete', methods=['POST'])
@login_required
def social_link_delete(link_id):
"""Delete social link"""
link = SocialLink.query.get_or_404(link_id)
db.session.delete(link)
db.session.commit()
flash('Link social eliminato con successo!', 'success')
return redirect(url_for('admin.social_links_manage'))

View File

@@ -6,9 +6,11 @@
"""
API Routes for managing portfolio data dynamically
Provides REST endpoints for CRUD operations on Profile, Skills, Projects, and Social Links
All write operations (POST, PUT, DELETE) require authentication
"""
from flask import Blueprint, jsonify, request
from flask_login import login_required
from models import db, Profile, Skill, Project, ProjectTag, SocialLink
route_api = Blueprint('api', __name__, url_prefix='/api')
@@ -28,6 +30,7 @@ def get_profile():
@route_api.route('/profile', methods=['PUT'])
@login_required
def update_profile():
"""Update profile information"""
profile = Profile.query.first()
@@ -58,6 +61,7 @@ def get_skills():
@route_api.route('/skills', methods=['POST'])
@login_required
def create_skill():
"""Create a new skill"""
data = request.json
@@ -75,6 +79,7 @@ def create_skill():
@route_api.route('/skills/<int:skill_id>', methods=['PUT'])
@login_required
def update_skill(skill_id):
"""Update a skill"""
skill = Skill.query.get_or_404(skill_id)
@@ -92,6 +97,7 @@ def update_skill(skill_id):
@route_api.route('/skills/<int:skill_id>', methods=['DELETE'])
@login_required
def delete_skill(skill_id):
"""Delete a skill"""
skill = Skill.query.get_or_404(skill_id)
@@ -112,6 +118,7 @@ def get_projects():
@route_api.route('/projects', methods=['POST'])
@login_required
def create_project():
"""Create a new project"""
data = request.json
@@ -144,6 +151,7 @@ def create_project():
@route_api.route('/projects/<int:project_id>', methods=['PUT'])
@login_required
def update_project(project_id):
"""Update a project"""
project = Project.query.get_or_404(project_id)
@@ -177,6 +185,7 @@ def update_project(project_id):
@route_api.route('/projects/<int:project_id>', methods=['DELETE'])
@login_required
def delete_project(project_id):
"""Delete a project"""
project = Project.query.get_or_404(project_id)
@@ -197,6 +206,7 @@ def get_social_links():
@route_api.route('/social-links', methods=['POST'])
@login_required
def create_social_link():
"""Create a new social link"""
data = request.json
@@ -214,6 +224,7 @@ def create_social_link():
@route_api.route('/social-links/<int:link_id>', methods=['PUT'])
@login_required
def update_social_link(link_id):
"""Update a social link"""
link = SocialLink.query.get_or_404(link_id)
@@ -231,6 +242,7 @@ def update_social_link(link_id):
@route_api.route('/social-links/<int:link_id>', methods=['DELETE'])
@login_required
def delete_social_link(link_id):
"""Delete a social link"""
link = SocialLink.query.get_or_404(link_id)

62
routes/auth.py Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright Hersel Giannella
"""
Authentication routes for login/logout
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, current_user
from models import User, db
from datetime import datetime
route_auth = Blueprint('auth', __name__, url_prefix='/auth')
@route_auth.route('/login', methods=['GET', 'POST'])
def login():
"""Login page"""
# Se l'utente è già autenticato, reindirizza alla dashboard
if current_user.is_authenticated:
return redirect(url_for('admin.dashboard'))
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
remember = request.form.get('remember', False)
if not username or not password:
flash('Per favore inserisci username e password.', 'danger')
return render_template('auth/login.html')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
if not user.is_active:
flash('Il tuo account è stato disabilitato.', 'danger')
return render_template('auth/login.html')
# Aggiorna last_login
user.last_login = datetime.utcnow()
db.session.commit()
login_user(user, remember=remember)
flash(f'Benvenuto {user.username}!', 'success')
# Redirect alla pagina richiesta o alla dashboard
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('admin.dashboard'))
else:
flash('Username o password non corretti.', 'danger')
return render_template('auth/login.html')
@route_auth.route('/logout')
def logout():
"""Logout user"""
logout_user()
flash('Logout effettuato con successo.', 'info')
return redirect(url_for('route_home.home'))