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:
285
routes/admin.py
Normal file
285
routes/admin.py
Normal 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'))
|
||||
@@ -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
62
routes/auth.py
Normal 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'))
|
||||
Reference in New Issue
Block a user