8 Commits

52 changed files with 3905 additions and 2485 deletions

View File

@@ -1,12 +1,33 @@
# Flask Application Configuration # Flask/Quart Configuration
APP_HOST=127.0.0.1 DEBUG=False
APP_PORT=5000 SECRET_KEY=your-super-secret-key-here-change-this
DEBUG=True
SECRET_KEY=change_this_to_a_random_secret_key
# MariaDB Database Configuration # Server Configuration
APP_HOST=0.0.0.0
APP_PORT=5000
# MySQL Database Configuration
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
DB_USER=portfolio_user DB_USER=hersel_user
DB_PASSWORD=portfolio_password DB_PASSWORD=your_secure_password_here
DB_NAME=portfolio_db DB_NAME=hersel_portfolio
# Upload Configuration
UPLOAD_FOLDER=static/uploads
MAX_CONTENT_LENGTH=16777216
# Email Configuration (optional)
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-email-password
# Pagination
POSTS_PER_PAGE=10
PROJECTS_PER_PAGE=12
# Cache
CACHE_TYPE=simple
CACHE_DEFAULT_TIMEOUT=300

172
DASHBOARD_FIX.md Normal file
View File

@@ -0,0 +1,172 @@
# Dashboard Routing Fix
Questo branch (`fix-dashboard-routing`) contiene le correzioni per risolvere i problemi di accesso alla dashboard nel branch `dynamic-site-enhancement`.
## Problemi Risolti
### 1. Template Mancante
- ✅ Aggiunto il template mancante `templates/dashboard/users.html`
- Il template è ora completo con paginazione e gestione degli errori
### 2. Gestione Errori Migliorata
- ✅ Aggiunta gestione degli errori in tutte le route della dashboard
- ✅ Messaggi di errore più dettagliati per il debugging
- ✅ Fallback sicuri quando il database non è accessibile
### 3. Route di Debug
- ✅ Aggiunta route `/dashboard/debug/auth` per controllare lo stato di autenticazione
- ✅ Aggiunta route `/dashboard/debug/access` per testare l'accesso alla dashboard
- ✅ Informazioni dettagliate su sessioni e privilegi utente
### 4. Script di Utilità
- ✅ Creato script `utils/create_admin.py` per creare utenti amministratore
- ✅ Supporto per creare, listare e promuovere utenti
## Come Risolvere i Problemi di Accesso
### Passo 1: Verifica la Configurazione
Assicurati che il file `config.py` abbia la `SECRET_KEY` configurata:
```python
SECRET_KEY = 'your-secret-key-here'
```
### Passo 2: Inizializza il Database
Assicurati che il database sia inizializzato e accessibile:
```bash
# Con Docker Compose
docker-compose up -d db
# Controlla i log per errori
docker-compose logs app
```
### Passo 3: Crea un Utente Amministratore
Usa lo script di utilità per creare un utente admin:
```bash
# Crea utente admin predefinito
python utils/create_admin.py create
# Oppure promuovi un utente esistente
python utils/create_admin.py promote username
# Lista tutti gli utenti
python utils/create_admin.py list
```
### Passo 4: Testa l'Accesso
1. **Controlla lo stato di autenticazione:**
```
GET /dashboard/debug/auth
```
Questo ti dirà se sei loggato e hai i privilegi corretti.
2. **Testa l'accesso alla dashboard:**
```
GET /dashboard/debug/access
```
Questo ti dirà se puoi accedere alla dashboard e perché.
3. **Effettua il login:**
- Vai a `/auth/login`
- Usa le credenziali dell'utente admin
- Dovresti essere reindirizzato automaticamente a `/dashboard/`
## Credenziali Admin Predefinite
Se usi lo script `create_admin.py create`, verranno create queste credenziali:
- **Username:** `admin`
- **Email:** `admin@hersel.it`
- **Password:** `admin123`
⚠️ **IMPORTANTE:** Cambia la password predefinita dopo il primo login!
## Troubleshooting
### Problema: "401 Login required"
**Soluzione:** Non sei loggato. Vai a `/auth/login` e effettua il login.
### Problema: "403 Admin access required"
**Soluzione:** Il tuo utente non ha privilegi di amministratore. Usa:
```bash
python utils/create_admin.py promote il_tuo_username
```
### Problema: "500 Internal Server Error"
**Possibili cause:**
1. Database non accessibile
2. Errore nei template
3. Configurazione mancante
**Debug:**
1. Controlla i log dell'applicazione
2. Usa le route di debug: `/dashboard/debug/auth` e `/dashboard/debug/access`
3. Verifica la configurazione del database
### Problema: Template non trovato
**Soluzione:** Assicurati che tutti i template esistano in `templates/dashboard/`:
- `index.html` ✅
- `projects.html` ✅
- `project_form.html` ✅
- `users.html` ✅ (aggiunto in questo fix)
- `base.html` ✅
## URL della Dashboard
Dopo aver risolto i problemi di autenticazione, puoi accedere a:
- **Dashboard principale:** `/dashboard/`
- **Gestione progetti:** `/dashboard/projects`
- **Nuovo progetto:** `/dashboard/projects/new`
- **Gestione utenti:** `/dashboard/users`
- **Debug autenticazione:** `/dashboard/debug/auth`
- **Test accesso:** `/dashboard/debug/access`
## Modifiche Apportate
1. **`templates/dashboard/users.html`** - Nuovo template per la gestione utenti
2. **`routes/dashboard.py`** - Migliorate gestione errori e aggiunte route di debug
3. **`utils/create_admin.py`** - Nuovo script per gestire utenti amministratore
4. **`DASHBOARD_FIX.md`** - Questa documentazione
## Come Applicare i Fix
1. **Merge di questo branch:**
```bash
git checkout dynamic-site-enhancement
git merge fix-dashboard-routing
```
2. **Oppure crea un PR:**
- Crea una pull request da `fix-dashboard-routing` a `dynamic-site-enhancement`
- Rivedi le modifiche e fai il merge
3. **Testa l'applicazione:**
```bash
# Restart dell'applicazione
docker-compose restart app
# Crea utente admin
docker-compose exec app python utils/create_admin.py create
# Testa l'accesso
curl http://localhost:5000/dashboard/debug/access
```
## Supporto
Se hai ancora problemi dopo aver applicato questi fix:
1. Controlla i log dell'applicazione
2. Usa le route di debug per diagnosticare il problema
3. Verifica che il database sia accessibile
4. Assicurati di avere un utente con privilegi di amministratore
Tutti i fix sono backward-compatible e non dovrebbero causare problemi al codice esistente.

228
README.md
View File

@@ -1,183 +1,93 @@
# Portfolio Dinamico - Hersel.it # Python Server - Hersel.it
Portfolio personale sviluppato con **Flask** e **MariaDB**, con gestione dinamica dei contenuti tramite API REST. Questo progetto è un'applicazione web sviluppata con **Quart** e configurata per essere eseguita tramite **Hypercorn**
## 🚀 Caratteristiche ## Requisiti
- **Framework**: Flask (migrato da Quart per semplificare l'architettura)
- **Database**: MariaDB per la gestione dinamica dei contenuti
- **ORM**: SQLAlchemy con Flask-SQLAlchemy
- **API REST**: Endpoint per gestire progetti, competenze, profilo e social links
- **Docker**: Configurazione completa con Docker Compose
- **Responsive**: Design responsive con Bootstrap 5
## 📋 Requisiti
- Python 3.10 o superiore - Python 3.10 o superiore
- MariaDB/MySQL 11.2 o superiore (o usa Docker Compose)
- Pip (gestore dei pacchetti Python) - Pip (gestore dei pacchetti Python)
## 🔧 Installazione Locale # Installazione
### 1. Clona il repository 1. Clona il repository:
```bash ```bash
git clone https://github.com/BluLupo/hersel.it.git git clone https://github.com/BluLupo/hersel.it.git
cd hersel.it cd hersel.it
``` ```
### 2. Crea e attiva ambiente virtuale 2. Crea Ambiente Virtuale
```bash ```bash
python3 -m venv env python3 -m venv env
source env/bin/activate # Linux/Mac ```
# oppure
env\Scripts\activate # Windows
```
### 3. Installa le dipendenze 3. Attiva Ambiente Virtuale
```bash ```bash
pip install -r requirements.txt source env/bin/activate
``` ```
### 4. Configura le variabili d'ambiente 4. Installa Le Dipendenze
```bash ```bash
cp .env.example .env pip install -r requirements.txt
# Modifica .env con le tue credenziali del database ```
```
### 5. Configura MariaDB # Configurazione
Crea il database e l'utente: Modifica il file <b>hypercorn_config.toml</b> se necessario per adattarlo al tuo ambiente
```sql Esempio di configurazione predefinita (hypercorn_config.toml):
CREATE DATABASE portfolio_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'portfolio_user'@'localhost' IDENTIFIED BY 'portfolio_password';
GRANT ALL PRIVILEGES ON portfolio_db.* TO 'portfolio_user'@'localhost';
FLUSH PRIVILEGES;
```
### 6. Inizializza il database ```toml
```bash bind = "0.0.0.0:5000"
python init_db.py workers = 1
``` reload = true
```
# Avvio Applicazione
```bash
hypercorn -c hypercorn_config.toml app:app
```
### 7. Avvia l'applicazione
```bash
# Modalità sviluppo
python app.py
# Modalità produzione con Gunicorn # 🚀 Avvio dell'applicazione con Docker
gunicorn -w 4 -b 0.0.0.0:5000 app:app Questa applicazione utilizza Quart come web framework asincrono e Hypercorn come ASGI server
```
## 🐳 Installazione con Docker ⚙️ Requisiti
### Requisiti
- Docker - Docker
- Docker Compose - Docker Compose
### Avvio rapido # 📄 Come avviare l'applicazione
1 - Crea un nuovo file docker-compose.yml nella tua macchina, con il seguente contenuto (oppure copialo direttamente da <a href="https://github.com/BluLupo/hersel.it/blob/master/docker-compose.yml">Qui</a> ):
```yml
version: "3.9"
services:
quartapp:
image: python:3.10-slim
container_name: quartapp
working_dir: /app
ports:
- "127.0.0.1:5000:5000"
restart: always
command: >
sh -c "
apt-get update &&
apt-get install -y git &&
[ -d /app/.git ] || git clone https://github.com/BluLupo/hersel.it.git /app &&
pip install --no-cache-dir -r requirements.txt &&
hypercorn -c hypercorn_config.toml app:app
"
environment:
- PYTHONUNBUFFERED=1
```
2 - Esegui il servizio con Docker Compose:
```bash ```bash
docker-compose up -d docker-compose up
``` ```
L'applicazione sarà disponibile su `http://localhost:5000` # 🔗 Accesso all'applicazione
Una volta avviata, l'applicazione sarà accessibile da:
Docker Compose avvierà automaticamente:
- Container MariaDB sulla porta 3306
- Container Flask sulla porta 5000
- Inizializzazione automatica del database
## 📁 Struttura del Progetto
```
hersel.it/
├── app.py # Applicazione Flask principale
├── config.py # Configurazione
├── models.py # Modelli SQLAlchemy
├── init_db.py # Script inizializzazione database
├── requirements.txt # Dipendenze Python
├── docker-compose.yml # Configurazione Docker
├── .env.example # Esempio variabili d'ambiente
├── routes/
│ ├── home.py # Route homepage
│ └── api.py # API REST endpoints
├── templates/ # Template Jinja2
│ ├── index.html
│ ├── head.html
│ ├── navbar.html
│ └── content/
│ ├── about.html
│ ├── project.html
│ └── links.html
└── static/ # File statici (CSS, JS, immagini)
```
## 🔌 API REST Endpoints
### Profile
- `GET /api/profile` - Ottieni informazioni profilo
- `PUT /api/profile` - Aggiorna profilo
### Skills
- `GET /api/skills` - Lista competenze
- `POST /api/skills` - Crea competenza
- `PUT /api/skills/<id>` - Aggiorna competenza
- `DELETE /api/skills/<id>` - Elimina competenza
### Projects
- `GET /api/projects` - Lista progetti
- `POST /api/projects` - Crea progetto
- `PUT /api/projects/<id>` - Aggiorna progetto
- `DELETE /api/projects/<id>` - Elimina progetto
### Social Links
- `GET /api/social-links` - Lista link social
- `POST /api/social-links` - Crea link social
- `PUT /api/social-links/<id>` - Aggiorna link social
- `DELETE /api/social-links/<id>` - Elimina link social
## 📊 Schema Database
### Tabelle
- `profile` - Informazioni personali
- `skills` - Competenze tecnologiche
- `projects` - Portfolio progetti
- `project_tags` - Tag/badge progetti
- `social_links` - Link profili social
## 🔄 Migrazione da Quart a Flask
Questo progetto è stato migrato da Quart (framework asincrono) a Flask (framework sincrono) per:
- Semplificare l'architettura
- Migliorare la compatibilità con librerie esistenti
- Facilitare il deployment con server WSGI standard (Gunicorn)
- Ridurre la complessità per un portfolio che non richiede operazioni async intensive
## 🛠️ Sviluppo
### Aggiungere un nuovo progetto via API
```bash ```bash
curl -X POST http://localhost:5000/api/projects \ http://127.0.0.1:5000
-H "Content-Type: application/json" \ ```
-d '{
"title": "Nuovo Progetto",
"description": "Descrizione del progetto",
"image_url": "img/project.webp",
"github_url": "https://github.com/username/repo",
"tags": [
{"name": "Python", "color_class": "bg-primary"},
{"name": "Flask", "color_class": "bg-info"}
]
}'
```
## 📝 Licenza
Copyright Hersel Giannella
## 🔗 Link Utili
- Portfolio Live: [https://hersel.it](https://hersel.it)
- GitHub: [https://github.com/BluLupo](https://github.com/BluLupo)
- LinkedIn: [https://linkedin.com/in/hersel](https://linkedin.com/in/hersel)

119
app.py
View File

@@ -2,67 +2,98 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright Hersel Giannella # Copyright Hersel Giannella
# Enhanced Quart Application with Database and Authentication
from flask import Flask, send_from_directory import asyncio
from flask_login import LoginManager from quart import Quart, send_from_directory, session, g, render_template
from config import config from config import config
from models import db, bcrypt, User from models.database import init_database, db_manager
from routes.home import route_home from utils.helpers import get_flash_messages
from routes.api import route_api from utils.auth import get_current_user
from routes.auth import route_auth
from routes.admin import route_admin
app = Flask( # Import Blueprints
from routes.home import home_bp
from routes.auth import auth_bp
from routes.dashboard import dashboard_bp
app = Quart(
__name__, __name__,
template_folder="templates", template_folder="templates",
static_folder="static", static_folder="static",
) )
# Load configuration # Configuration
app.config['SECRET_KEY'] = config.SECRET_KEY app.config.from_object(config)
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI app.secret_key = config.SECRET_KEY
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = config.SQLALCHEMY_TRACK_MODIFICATIONS
app.config['SQLALCHEMY_ECHO'] = config.SQLALCHEMY_ECHO
# File upload configuration # Template globals
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB max file size @app.template_global('get_flashed_messages')
app.config['UPLOAD_FOLDER'] = 'static/img' def template_get_flashed_messages(with_categories=False):
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif', 'webp'} return get_flash_messages()
# Initialize extensions # Context processor for current user
db.init_app(app) @app.before_request
bcrypt.init_app(app) async def load_current_user():
g.current_user = await get_current_user()
# Initialize Flask-Login @app.context_processor
login_manager = LoginManager() def inject_user():
login_manager.init_app(app) return {'current_user': getattr(g, 'current_user', None)}
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Per accedere a questa pagina devi effettuare il login.'
login_manager.login_message_category = 'warning'
@login_manager.user_loader # Static files routes
def load_user(user_id):
"""Load user for Flask-Login"""
return User.query.get(int(user_id))
# favicon.ico, sitemap.xml and robots.txt
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): async def favicon():
return send_from_directory(app.static_folder, 'favicon.ico') return await send_from_directory(app.static_folder, 'favicon.ico')
@app.route('/sitemap.xml') @app.route('/sitemap.xml')
def sitemap(): async def sitemap():
return send_from_directory(app.static_folder, 'sitemap.xml') return await send_from_directory(app.static_folder, 'sitemap.xml')
@app.route('/robots.txt') @app.route('/robots.txt')
def robots(): async def robots():
return send_from_directory(app.static_folder, 'robots.txt') return await send_from_directory(app.static_folder, 'robots.txt')
# BluePrint Routes # Register Blueprints
app.register_blueprint(route_home) app.register_blueprint(home_bp)
app.register_blueprint(route_api) app.register_blueprint(auth_bp)
app.register_blueprint(route_auth) app.register_blueprint(dashboard_bp)
app.register_blueprint(route_admin)
# Database initialization
@app.before_serving
async def initialize_app():
"""Initialize database and other services"""
print("🚀 Initializing Hersel.it application...")
try:
await init_database()
print("✅ Database initialized successfully")
except Exception as e:
print(f"❌ Error initializing database: {e}")
# Don't crash the app, but log the error
@app.after_serving
async def cleanup_app():
"""Cleanup resources"""
print("🔒 Shutting down Hersel.it application...")
await db_manager.close_pool()
print("✅ Application shutdown complete")
# Error handlers
@app.errorhandler(404)
async def not_found(error):
return await render_template('errors/404.html'), 404
@app.errorhandler(500)
async def internal_error(error):
return await render_template('errors/500.html'), 500
# Health check endpoint
@app.route('/health')
async def health_check():
return {'status': 'healthy', 'app': 'Hersel.it Portfolio'}
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=config.DEBUG, host=config.APP_HOST, port=config.APP_PORT) app.run(
debug=config.DEBUG,
host=config.APP_HOST,
port=config.APP_PORT
)

View File

@@ -1,32 +1,58 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright Hersel Giannella # Enhanced Configuration with Database Settings
from pydantic_settings import BaseSettings import os
from dotenv import load_dotenv
class Config(BaseSettings): # Load environment variables from .env file
APP_HOST: str = "127.0.0.1" load_dotenv()
APP_PORT: int = 5000
DEBUG: bool = True
SECRET_KEY: str = "default_secret_key"
# Database Configuration class Config:
DB_HOST: str = "localhost" # Flask/Quart Settings
DB_PORT: int = 3306 DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
DB_USER: str = "portfolio_user" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
DB_PASSWORD: str = "portfolio_password"
DB_NAME: str = "portfolio_db"
@property # Server Settings
def SQLALCHEMY_DATABASE_URI(self) -> str: APP_HOST = os.getenv('APP_HOST', '0.0.0.0')
"""Construct MariaDB connection string""" APP_PORT = int(os.getenv('APP_PORT', '5000'))
return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False # Database Settings
SQLALCHEMY_ECHO: bool = False # Set to True for SQL query debugging DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = int(os.getenv('DB_PORT', '3306'))
DB_USER = os.getenv('DB_USER', 'hersel_user')
DB_PASSWORD = os.getenv('DB_PASSWORD', 'your_password_here')
DB_NAME = os.getenv('DB_NAME', 'hersel_portfolio')
class Config: # Session Settings
env_file = ".env" SESSION_PERMANENT = False
SESSION_TYPE = 'filesystem'
PERMANENT_SESSION_LIFETIME = 60 * 60 * 24 * 7 # 7 days
# Upload Settings
UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER', 'static/uploads')
MAX_CONTENT_LENGTH = int(os.getenv('MAX_CONTENT_LENGTH', '16777216')) # 16MB
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'txt', 'doc', 'docx'}
# Email Settings (for future use)
MAIL_SERVER = os.getenv('MAIL_SERVER', 'localhost')
MAIL_PORT = int(os.getenv('MAIL_PORT', '587'))
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'True').lower() == 'true'
MAIL_USERNAME = os.getenv('MAIL_USERNAME', '')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD', '')
# Security Settings
WTF_CSRF_ENABLED = True
WTF_CSRF_TIME_LIMIT = None
# Pagination
POSTS_PER_PAGE = int(os.getenv('POSTS_PER_PAGE', '10'))
PROJECTS_PER_PAGE = int(os.getenv('PROJECTS_PER_PAGE', '12'))
# Cache Settings
CACHE_TYPE = os.getenv('CACHE_TYPE', 'simple')
CACHE_DEFAULT_TIMEOUT = int(os.getenv('CACHE_DEFAULT_TIMEOUT', '300'))
# Create config instance
config = Config() config = Config()

View File

@@ -1,51 +1,53 @@
version: "3.9" version: '3.9'
services: services:
mariadb: # MySQL Database
image: mariadb:11.2 mysql:
container_name: portfolio_mariadb image: mysql:8.0
container_name: hersel_mysql
restart: always restart: always
environment: environment:
MYSQL_ROOT_PASSWORD: root_password_change_me MYSQL_ROOT_PASSWORD: root_password_change_me
MYSQL_DATABASE: portfolio_db MYSQL_DATABASE: hersel_portfolio
MYSQL_USER: portfolio_user MYSQL_USER: hersel_user
MYSQL_PASSWORD: portfolio_password MYSQL_PASSWORD: secure_password_123
volumes:
- mariadb_data:/var/lib/mysql
ports: ports:
- "127.0.0.1:3306:3306" - "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
command: --default-authentication-plugin=mysql_native_password
healthcheck: healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s timeout: 20s
timeout: 5s retries: 10
retries: 5
flaskapp: # Quart Application
image: python:3.10-slim quartapp:
container_name: portfolio_flaskapp build: .
working_dir: /app container_name: hersel_app
restart: always
ports: ports:
- "127.0.0.1:5000:5000" - "127.0.0.1:5000:5000"
restart: always
depends_on:
mariadb:
condition: service_healthy
command: >
sh -c "
apt-get update &&
apt-get install -y git default-libmysqlclient-dev build-essential pkg-config &&
[ -d /app/.git ] || git clone https://github.com/BluLupo/hersel.it.git /app &&
pip install --no-cache-dir -r requirements.txt &&
python init_db.py &&
gunicorn -w 4 -b 0.0.0.0:5000 app:app
"
environment: environment:
- PYTHONUNBUFFERED=1 - DEBUG=False
- DB_HOST=mariadb - SECRET_KEY=super-secret-key-change-in-production-please
- DB_HOST=mysql
- DB_PORT=3306 - DB_PORT=3306
- DB_USER=portfolio_user - DB_USER=hersel_user
- DB_PASSWORD=portfolio_password - DB_PASSWORD=secure_password_123
- DB_NAME=portfolio_db - DB_NAME=hersel_portfolio
- PYTHONUNBUFFERED=1
depends_on:
mysql:
condition: service_healthy
volumes:
- ./static/uploads:/app/static/uploads
volumes: volumes:
mariadb_data: mysql_data:
driver: local
networks:
default:
name: hersel_network

143
init.sql Normal file
View File

@@ -0,0 +1,143 @@
-- Initial database setup for Hersel.it Portfolio
-- This file is automatically executed when MySQL container starts
USE hersel_portfolio;
-- Enable UTF8MB4 charset for emoji and international characters
SET NAMES utf8mb4;
SET character_set_client = utf8mb4;
-- Create categories table
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(7) DEFAULT '#007bff',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
role ENUM('admin', 'user') DEFAULT 'user',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create projects table
CREATE TABLE IF NOT EXISTS projects (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
content LONGTEXT,
image_url VARCHAR(500),
github_url VARCHAR(500),
demo_url VARCHAR(500),
technologies JSON,
category_id INT,
is_featured BOOLEAN DEFAULT FALSE,
is_published BOOLEAN DEFAULT TRUE,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create posts table (for future blog functionality)
CREATE TABLE IF NOT EXISTS posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
excerpt TEXT,
content LONGTEXT,
featured_image VARCHAR(500),
category_id INT,
author_id INT,
is_published BOOLEAN DEFAULT FALSE,
published_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create settings table
CREATE TABLE IF NOT EXISTS settings (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value LONGTEXT,
description TEXT,
type ENUM('text', 'textarea', 'boolean', 'json') DEFAULT 'text',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Insert default categories
INSERT INTO categories (name, slug, description, color) VALUES
('Web Development', 'web-development', 'Progetti di sviluppo web', '#007bff'),
('Mobile Apps', 'mobile-apps', 'Applicazioni mobile', '#28a745'),
('Desktop Apps', 'desktop-apps', 'Applicazioni desktop', '#ffc107'),
('APIs', 'apis', 'API e servizi web', '#dc3545'),
('Tools & Utilities', 'tools-utilities', 'Strumenti e utility', '#6f42c1'),
('Open Source', 'open-source', 'Progetti open source', '#20c997')
ON DUPLICATE KEY UPDATE name=VALUES(name);
-- Insert default settings
INSERT INTO settings (setting_key, setting_value, description, type) VALUES
('site_name', 'Hersel.it', 'Nome del sito', 'text'),
('site_description', 'Portfolio personale di Hersel Giannella - Developer', 'Descrizione del sito', 'textarea'),
('admin_email', 'admin@hersel.it', 'Email amministratore', 'text'),
('site_logo', '/static/img/logo.png', 'URL del logo', 'text'),
('social_github', 'https://github.com/BluLupo', 'GitHub URL', 'text'),
('social_linkedin', '', 'LinkedIn URL', 'text'),
('social_twitter', '', 'Twitter URL', 'text'),
('site_maintenance', 'false', 'Modalità manutenzione', 'boolean'),
('analytics_code', '', 'Codice Analytics', 'textarea'),
('projects_per_page', '12', 'Progetti per pagina', 'text'),
('featured_projects_limit', '6', 'Limite progetti in evidenza', 'text')
ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value);
-- Create admin user (password: AdminPass123!)
-- This creates a default admin user for initial access
-- Password hash for 'AdminPass123!' generated with bcrypt
INSERT INTO users (username, email, password_hash, first_name, last_name, role) VALUES
('admin', 'admin@hersel.it', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqyaqr06eoAGNz9CpahtY1q', 'Admin', 'User', 'admin')
ON DUPLICATE KEY UPDATE role=VALUES(role);
-- Insert sample projects (optional)
INSERT INTO projects (title, slug, description, content, github_url, technologies, is_featured, is_published, created_by) VALUES
('Hersel.it Portfolio', 'hersel-it-portfolio', 'Portfolio dinamico sviluppato con Quart e MySQL',
'<h2>Portfolio Dinamico</h2><p>Questo portfolio è stato sviluppato utilizzando le seguenti tecnologie:</p><ul><li>Python con Quart framework</li><li>MySQL per il database</li><li>Bootstrap 5 per il frontend</li><li>Docker per il deployment</li></ul>',
'https://github.com/BluLupo/hersel.it',
'["Python", "Quart", "MySQL", "Bootstrap", "Docker"]',
TRUE, TRUE, 1),
('API REST con Quart', 'api-rest-quart', 'API RESTful asincrona per gestione dati',
'<h2>API REST</h2><p>API asincrona sviluppata con Quart per gestire operazioni CRUD su database MySQL.</p>',
'',
'["Python", "Quart", "MySQL", "REST API", "Async"]',
FALSE, TRUE, 1)
ON DUPLICATE KEY UPDATE title=VALUES(title);
-- Create indexes for better performance
CREATE INDEX idx_projects_published ON projects(is_published);
CREATE INDEX idx_projects_featured ON projects(is_featured);
CREATE INDEX idx_projects_category ON projects(category_id);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_posts_published ON posts(is_published);
-- Show confirmation message
SELECT 'Database initialized successfully!' as Status;
SELECT COUNT(*) as 'Total Categories' FROM categories;
SELECT COUNT(*) as 'Total Settings' FROM settings;
SELECT COUNT(*) as 'Admin Users' FROM users WHERE role='admin';
SELECT COUNT(*) as 'Sample Projects' FROM projects;

View File

@@ -1,198 +0,0 @@
"""
Database initialization script
Populates the database with initial portfolio data and creates default admin user
"""
from app import app
from models import db, User, Profile, Skill, Project, ProjectTag, SocialLink
def init_database():
"""Initialize database with portfolio data"""
with app.app_context():
# Drop all tables and recreate
print("Dropping all tables...")
db.drop_all()
print("Creating all tables...")
db.create_all()
# Create default admin user
print("Creating default admin user...")
admin = User(
username='admin',
email='admin@hersel.it'
)
admin.set_password('admin123') # CHANGE THIS PASSWORD AFTER FIRST LOGIN!
db.session.add(admin)
# Create profile information
print("Adding profile information...")
profile = Profile(
title="Il ponte tra sistemi e sviluppo web",
lead_text="Con oltre 7 Anni di esperienza nello sviluppo di applicazioni web con Python Flask, offro soluzioni complete end-to-end.",
description_1="La mia doppia specializzazione mi permette di comprendere a fondo l'intero ciclo di vita delle applicazioni, dall'architettura del server fino all'implementazione e al deployment.",
description_2="Mi piace risolvere problemi complessi e creare soluzioni che siano robuste, scalabili e facili da mantenere.",
years_experience=7,
profile_image='img/personal.webp'
)
db.session.add(profile)
# Create skills
print("Adding skills...")
skills_data = [
{"name": "Linux", "icon_class": "fab fa-linux", "category": "OS", "display_order": 1},
{"name": "Windows", "icon_class": "fab fa-windows", "category": "OS", "display_order": 2},
{"name": "Python", "icon_class": "fab fa-python", "category": "Language", "display_order": 3},
{"name": "Flask", "icon_class": "fas fa-flask", "category": "Framework", "display_order": 4},
{"name": "Database", "icon_class": "fas fa-database", "category": "Tool", "display_order": 5},
{"name": "Docker", "icon_class": "fab fa-docker", "category": "Tool", "display_order": 6},
{"name": "Server", "icon_class": "fas fa-server", "category": "Infrastructure", "display_order": 7},
{"name": "Networking", "icon_class": "fas fa-network-wired", "category": "Infrastructure", "display_order": 8},
]
for skill_data in skills_data:
skill = Skill(**skill_data)
db.session.add(skill)
# Create projects
print("Adding projects...")
# Project 1: Database Backup Script
project1 = Project(
title="Script di Backup Database (MariaDB/MySQL)",
description="Script in Bash per sistemi Linux che permette l'automazione dei backup database",
image_url="img/bash.webp",
github_url="https://github.com/BluLupo/server-script/tree/main/db_bash_backup-main",
display_order=1,
animation_delay="0s"
)
db.session.add(project1)
db.session.flush() # Get project1.id
# Project 1 tags
project1_tags = [
ProjectTag(project_id=project1.id, name="Bash", color_class="bg-primary", display_order=1),
ProjectTag(project_id=project1.id, name="Linux", color_class="bg-info", display_order=2),
]
db.session.add_all(project1_tags)
# Project 2: ByteStash
project2 = Project(
title="Personal ByteStash",
description="Ho realizzato un repository personale di snippet sfruttando Bytestash, ottimizzando la gestione del codice riutilizzabile e migliorando la produttività nello sviluppo di progetti software.",
image_url="img/byte.webp",
demo_url="https://bytestash.gwserver.it/public/snippets",
display_order=2,
animation_delay="0.2s"
)
db.session.add(project2)
db.session.flush()
# Project 2 tags
project2_tags = [
ProjectTag(project_id=project2.id, name="LXC", color_class="bg-warning text-dark", display_order=1),
ProjectTag(project_id=project2.id, name="Proxmox", color_class="bg-dark", display_order=2),
ProjectTag(project_id=project2.id, name="Nginx", color_class="bg-info", display_order=3),
ProjectTag(project_id=project2.id, name="Reverse Proxy", color_class="bg-secondary", display_order=4),
ProjectTag(project_id=project2.id, name="Linux", color_class="bg-primary", display_order=5),
ProjectTag(project_id=project2.id, name="Self-hosted", color_class="bg-primary", display_order=6),
]
db.session.add_all(project2_tags)
# Project 3: Nextcloud
project3 = Project(
title="Nextcloud Personale",
description="Installazione di Nextcloud su container LXC con database PostgreSQL e caching Redis, integrato in una rete privata con gestione IP tramite server DHCP.",
image_url="img/next.webp",
demo_url="https://cloud.gwserver.it",
display_order=3,
animation_delay="0.4s"
)
db.session.add(project3)
db.session.flush()
# Project 3 tags
project3_tags = [
ProjectTag(project_id=project3.id, name="Nextcloud", color_class="bg-primary", display_order=1),
ProjectTag(project_id=project3.id, name="PostgreSQL", color_class="bg-secondary", display_order=2),
ProjectTag(project_id=project3.id, name="Redis", color_class="bg-info", display_order=3),
ProjectTag(project_id=project3.id, name="LXC", color_class="bg-warning text-dark", display_order=4),
ProjectTag(project_id=project3.id, name="Proxmox", color_class="bg-dark", display_order=5),
ProjectTag(project_id=project3.id, name="Rete Privata", color_class="bg-success", display_order=6),
ProjectTag(project_id=project3.id, name="DHCP Server", color_class="bg-secondary", display_order=7),
]
db.session.add_all(project3_tags)
# Create social links
print("Adding social links...")
social_links_data = [
{
"platform_name": "LinkedIn",
"url": "https://linkedin.com/in/hersel",
"icon_class": "fab fa-linkedin",
"display_order": 1,
"animation_delay": "0.1s"
},
{
"platform_name": "GitHub",
"url": "https://github.com/blulupo",
"icon_class": "fab fa-github",
"display_order": 2,
"animation_delay": "0.2s"
},
{
"platform_name": "Stack Overflow",
"url": "https://stackoverflow.com/users/11765177/hersel-giannella",
"icon_class": "fab fa-stack-overflow",
"display_order": 3,
"animation_delay": "0.3s"
},
{
"platform_name": "CodeWars",
"url": "https://www.codewars.com/users/BluLupo",
"icon_class": "fas fa-code",
"display_order": 4,
"animation_delay": "0.4s"
},
{
"platform_name": "Blog",
"url": "https://blog.hersel.it",
"icon_class": "fas fa-blog",
"display_order": 5,
"animation_delay": "0.5s"
},
{
"platform_name": "Email",
"url": "mailto:info@hersel.it",
"icon_class": "fas fa-envelope",
"display_order": 6,
"animation_delay": "0.6s"
},
]
for link_data in social_links_data:
social_link = SocialLink(**link_data)
db.session.add(social_link)
# Commit all changes
print("Committing changes to database...")
db.session.commit()
print("\n✅ Database initialized successfully!")
print(f" - Admin User: 1 record")
print(f" - Profile: 1 record")
print(f" - Skills: {len(skills_data)} records")
print(f" - Projects: 3 records")
print(f" - Project Tags: {len(project1_tags) + len(project2_tags) + len(project3_tags)} records")
print(f" - Social Links: {len(social_links_data)} records")
print("\n" + "="*60)
print("🔐 DEFAULT ADMIN CREDENTIALS")
print("="*60)
print(f" Username: admin")
print(f" Password: admin123")
print(f" ⚠️ CHANGE THIS PASSWORD IMMEDIATELY AFTER FIRST LOGIN!")
print("="*60)
if __name__ == '__main__':
init_database()

169
models.py
View File

@@ -1,169 +0,0 @@
"""
Database models for Portfolio Application
Uses SQLAlchemy ORM with MariaDB
"""
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from flask_bcrypt import Bcrypt
from datetime import datetime
db = SQLAlchemy()
bcrypt = Bcrypt()
class User(UserMixin, db.Model):
"""Store admin users for authentication"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
def set_password(self, password):
"""Hash password using bcrypt"""
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
def check_password(self, password):
"""Verify password against hash"""
return bcrypt.check_password_hash(self.password_hash, password)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None,
'last_login': self.last_login.isoformat() if self.last_login else None
}
class Profile(db.Model):
"""Store personal profile information"""
__tablename__ = 'profile'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
lead_text = db.Column(db.Text, nullable=False)
description_1 = db.Column(db.Text)
description_2 = db.Column(db.Text)
years_experience = db.Column(db.Integer, default=7)
cv_url = db.Column(db.String(500))
profile_image = db.Column(db.String(500), default='img/personal.webp')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'lead_text': self.lead_text,
'description_1': self.description_1,
'description_2': self.description_2,
'years_experience': self.years_experience,
'cv_url': self.cv_url,
'profile_image': self.profile_image
}
class Skill(db.Model):
"""Store technical skills/technologies"""
__tablename__ = 'skills'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
icon_class = db.Column(db.String(100), nullable=False)
category = db.Column(db.String(50)) # OS, Language, Framework, Tool, etc.
proficiency_level = db.Column(db.Integer) # 1-5 (optional)
display_order = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'icon_class': self.icon_class,
'category': self.category,
'proficiency_level': self.proficiency_level,
'display_order': self.display_order
}
class Project(db.Model):
"""Store portfolio projects"""
__tablename__ = 'projects'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=False)
image_url = db.Column(db.String(500))
demo_url = db.Column(db.String(500))
github_url = db.Column(db.String(500))
display_order = db.Column(db.Integer, default=0)
animation_delay = db.Column(db.String(10), default='0s') # e.g., '0.2s'
is_published = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship
tags = db.relationship('ProjectTag', backref='project', lazy=True, cascade='all, delete-orphan')
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'description': self.description,
'image_url': self.image_url,
'demo_url': self.demo_url,
'github_url': self.github_url,
'display_order': self.display_order,
'animation_delay': self.animation_delay,
'is_published': self.is_published,
'tags': [tag.to_dict() for tag in self.tags]
}
class ProjectTag(db.Model):
"""Store tags/badges for projects"""
__tablename__ = 'project_tags'
id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
name = db.Column(db.String(50), nullable=False)
color_class = db.Column(db.String(50), default='bg-primary') # Bootstrap badge classes
display_order = db.Column(db.Integer, default=0)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'color_class': self.color_class,
'display_order': self.display_order
}
class SocialLink(db.Model):
"""Store social media and profile links"""
__tablename__ = 'social_links'
id = db.Column(db.Integer, primary_key=True)
platform_name = db.Column(db.String(100), nullable=False)
url = db.Column(db.String(500), nullable=False)
icon_class = db.Column(db.String(100), nullable=False)
display_order = db.Column(db.Integer, default=0)
animation_delay = db.Column(db.String(10), default='0s') # e.g., '0.1s'
is_active = db.Column(db.Boolean, default=True)
def to_dict(self):
return {
'id': self.id,
'platform_name': self.platform_name,
'url': self.url,
'icon_class': self.icon_class,
'display_order': self.display_order,
'animation_delay': self.animation_delay
}

8
models/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
# Database Models Package
from .user import User
from .post import Post
from .project import Project
from .category import Category
from .settings import Settings
__all__ = ['User', 'Post', 'Project', 'Category', 'Settings']

97
models/category.py Normal file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Category Model
from typing import Optional, Dict, List
from .database import db_manager
from datetime import datetime
class Category:
def __init__(self, **kwargs):
self.id = kwargs.get('id')
self.name = kwargs.get('name')
self.slug = kwargs.get('slug')
self.description = kwargs.get('description')
self.color = kwargs.get('color', '#007bff')
self.created_at = kwargs.get('created_at')
async def save(self) -> int:
"""Save category to database"""
if self.id:
# Update existing category
query = """
UPDATE categories SET
name=%s, slug=%s, description=%s, color=%s
WHERE id=%s
"""
params = (self.name, self.slug, self.description, self.color, self.id)
await db_manager.execute_update(query, params)
return self.id
else:
# Insert new category
query = """
INSERT INTO categories (name, slug, description, color)
VALUES (%s, %s, %s, %s)
"""
params = (self.name, self.slug, self.description, self.color)
category_id = await db_manager.execute_insert(query, params)
self.id = category_id
return category_id
@classmethod
async def find_by_id(cls, category_id: int) -> Optional['Category']:
"""Find category by ID"""
query = "SELECT * FROM categories WHERE id = %s"
results = await db_manager.execute_query(query, (category_id,))
if results:
return cls(**results[0])
return None
@classmethod
async def find_by_slug(cls, slug: str) -> Optional['Category']:
"""Find category by slug"""
query = "SELECT * FROM categories WHERE slug = %s"
results = await db_manager.execute_query(query, (slug,))
if results:
return cls(**results[0])
return None
@classmethod
async def get_all(cls, limit: int = 50) -> List['Category']:
"""Get all categories"""
query = "SELECT * FROM categories ORDER BY name ASC LIMIT %s"
results = await db_manager.execute_query(query, (limit,))
return [cls(**row) for row in results]
@classmethod
async def count(cls) -> int:
"""Count total categories"""
query = "SELECT COUNT(*) as count FROM categories"
results = await db_manager.execute_query(query)
return results[0]['count'] if results else 0
async def delete(self) -> bool:
"""Delete category"""
query = "DELETE FROM categories WHERE id = %s"
affected = await db_manager.execute_update(query, (self.id,))
return affected > 0
def to_dict(self) -> Dict:
"""Convert category to dictionary"""
return {
'id': self.id,
'name': self.name,
'slug': self.slug,
'description': self.description,
'color': self.color,
'created_at': self.created_at.isoformat() if self.created_at else None
}
@staticmethod
def generate_slug(name: str) -> str:
"""Generate URL-friendly slug from name"""
import re
slug = re.sub(r'[^\w\s-]', '', name).strip().lower()
slug = re.sub(r'[\s_-]+', '-', slug)
return slug

194
models/database.py Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Database Configuration and Connection Manager
import asyncio
import aiomysql
from typing import Optional
from config import config
class DatabaseManager:
def __init__(self):
self.pool: Optional[aiomysql.Pool] = None
async def init_pool(self):
"""Initialize connection pool"""
try:
self.pool = await aiomysql.create_pool(
host=config.DB_HOST,
port=config.DB_PORT,
user=config.DB_USER,
password=config.DB_PASSWORD,
db=config.DB_NAME,
minsize=1,
maxsize=10,
autocommit=True,
charset='utf8mb4'
)
print("✅ Database connection pool initialized")
except Exception as e:
print(f"❌ Error initializing database pool: {e}")
raise
async def close_pool(self):
"""Close connection pool"""
if self.pool:
self.pool.close()
await self.pool.wait_closed()
print("🔒 Database connection pool closed")
async def get_connection(self):
"""Get connection from pool"""
if not self.pool:
await self.init_pool()
return self.pool.acquire()
async def execute_query(self, query: str, params: tuple = None):
"""Execute a query and return results"""
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(query, params)
return await cursor.fetchall()
async def execute_insert(self, query: str, params: tuple = None):
"""Execute insert query and return last insert id"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, params)
return cursor.lastrowid
async def execute_update(self, query: str, params: tuple = None):
"""Execute update/delete query and return affected rows"""
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, params)
return cursor.rowcount
# Global database manager instance
db_manager = DatabaseManager()
async def init_database():
"""Initialize database and create tables"""
await db_manager.init_pool()
await create_tables()
async def create_tables():
"""Create database tables if they don't exist"""
tables = [
# Users table
"""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
role ENUM('admin', 'user') DEFAULT 'user',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
""",
# Categories table
"""
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
color VARCHAR(7) DEFAULT '#007bff',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
""",
# Projects table
"""
CREATE TABLE IF NOT EXISTS projects (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
content LONGTEXT,
image_url VARCHAR(500),
github_url VARCHAR(500),
demo_url VARCHAR(500),
technologies JSON,
category_id INT,
is_featured BOOLEAN DEFAULT FALSE,
is_published BOOLEAN DEFAULT TRUE,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
""",
# Posts table (for blog functionality)
"""
CREATE TABLE IF NOT EXISTS posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
excerpt TEXT,
content LONGTEXT,
featured_image VARCHAR(500),
category_id INT,
author_id INT,
is_published BOOLEAN DEFAULT FALSE,
published_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
""",
# Settings table
"""
CREATE TABLE IF NOT EXISTS settings (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value LONGTEXT,
description TEXT,
type ENUM('text', 'textarea', 'boolean', 'json') DEFAULT 'text',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
"""
]
for table_sql in tables:
try:
await db_manager.execute_update(table_sql)
print(f"✅ Table created/verified")
except Exception as e:
print(f"❌ Error creating table: {e}")
# Insert default settings
await insert_default_settings()
async def insert_default_settings():
"""Insert default settings if they don't exist"""
default_settings = [
('site_name', 'Hersel.it', 'Nome del sito', 'text'),
('site_description', 'Portfolio personale di Hersel Giannella', 'Descrizione del sito', 'textarea'),
('admin_email', 'admin@hersel.it', 'Email amministratore', 'text'),
('site_logo', '/static/img/logo.png', 'URL del logo', 'text'),
('social_github', 'https://github.com/BluLupo', 'GitHub URL', 'text'),
('social_linkedin', '', 'LinkedIn URL', 'text'),
('social_twitter', '', 'Twitter URL', 'text'),
('site_maintenance', 'false', 'Modalità manutenzione', 'boolean'),
('analytics_code', '', 'Codice Analytics', 'textarea')
]
for key, value, desc, type_val in default_settings:
try:
await db_manager.execute_insert(
"INSERT IGNORE INTO settings (setting_key, setting_value, description, type) VALUES (%s, %s, %s, %s)",
(key, value, desc, type_val)
)
except Exception as e:
print(f"Warning: Could not insert setting {key}: {e}")

116
models/post.py Normal file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Post Model (for future blog functionality)
from typing import Optional, Dict, List
from .database import db_manager
from datetime import datetime
class Post:
def __init__(self, **kwargs):
self.id = kwargs.get('id')
self.title = kwargs.get('title')
self.slug = kwargs.get('slug')
self.excerpt = kwargs.get('excerpt')
self.content = kwargs.get('content')
self.featured_image = kwargs.get('featured_image')
self.category_id = kwargs.get('category_id')
self.author_id = kwargs.get('author_id')
self.is_published = kwargs.get('is_published', False)
self.published_at = kwargs.get('published_at')
self.created_at = kwargs.get('created_at')
self.updated_at = kwargs.get('updated_at')
async def save(self) -> int:
"""Save post to database"""
if self.id:
# Update existing post
query = """
UPDATE posts SET
title=%s, slug=%s, excerpt=%s, content=%s, featured_image=%s,
category_id=%s, is_published=%s, published_at=%s, updated_at=NOW()
WHERE id=%s
"""
params = (self.title, self.slug, self.excerpt, self.content, self.featured_image,
self.category_id, self.is_published, self.published_at, self.id)
await db_manager.execute_update(query, params)
return self.id
else:
# Insert new post
query = """
INSERT INTO posts (title, slug, excerpt, content, featured_image, category_id,
author_id, is_published, published_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
params = (self.title, self.slug, self.excerpt, self.content, self.featured_image,
self.category_id, self.author_id, self.is_published, self.published_at)
post_id = await db_manager.execute_insert(query, params)
self.id = post_id
return post_id
@classmethod
async def find_by_id(cls, post_id: int) -> Optional['Post']:
"""Find post by ID"""
query = "SELECT * FROM posts WHERE id = %s"
results = await db_manager.execute_query(query, (post_id,))
if results:
return cls(**results[0])
return None
@classmethod
async def find_by_slug(cls, slug: str) -> Optional['Post']:
"""Find post by slug"""
query = "SELECT * FROM posts WHERE slug = %s AND is_published = TRUE"
results = await db_manager.execute_query(query, (slug,))
if results:
return cls(**results[0])
return None
@classmethod
async def get_all(cls, published_only: bool = True, limit: int = 50, offset: int = 0) -> List['Post']:
"""Get all posts with pagination"""
query = "SELECT * FROM posts"
params = []
if published_only:
query += " WHERE is_published = TRUE"
query += " ORDER BY created_at DESC LIMIT %s OFFSET %s"
params.extend([limit, offset])
results = await db_manager.execute_query(query, tuple(params))
return [cls(**row) for row in results]
@classmethod
async def count(cls, published_only: bool = True) -> int:
"""Count total posts"""
query = "SELECT COUNT(*) as count FROM posts"
if published_only:
query += " WHERE is_published = TRUE"
results = await db_manager.execute_query(query)
return results[0]['count'] if results else 0
async def delete(self) -> bool:
"""Delete post"""
query = "DELETE FROM posts WHERE id = %s"
affected = await db_manager.execute_update(query, (self.id,))
return affected > 0
def to_dict(self) -> Dict:
"""Convert post to dictionary"""
return {
'id': self.id,
'title': self.title,
'slug': self.slug,
'excerpt': self.excerpt,
'content': self.content,
'featured_image': self.featured_image,
'category_id': self.category_id,
'author_id': self.author_id,
'is_published': self.is_published,
'published_at': self.published_at.isoformat() if self.published_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}

183
models/project.py Normal file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Project Model
import json
from typing import Optional, Dict, List
from .database import db_manager
from datetime import datetime
class Project:
def __init__(self, **kwargs):
self.id = kwargs.get('id')
self.title = kwargs.get('title')
self.slug = kwargs.get('slug')
self.description = kwargs.get('description')
self.content = kwargs.get('content')
self.image_url = kwargs.get('image_url')
self.github_url = kwargs.get('github_url')
self.demo_url = kwargs.get('demo_url')
self.technologies = kwargs.get('technologies', [])
self.category_id = kwargs.get('category_id')
self.is_featured = kwargs.get('is_featured', False)
self.is_published = kwargs.get('is_published', True)
self.created_by = kwargs.get('created_by')
self.created_at = kwargs.get('created_at')
self.updated_at = kwargs.get('updated_at')
# Handle JSON technologies field
if isinstance(self.technologies, str):
try:
self.technologies = json.loads(self.technologies)
except:
self.technologies = []
@property
def technologies_json(self) -> str:
"""Get technologies as JSON string"""
return json.dumps(self.technologies) if self.technologies else '[]'
async def save(self) -> int:
"""Save project to database"""
if self.id:
# Update existing project
query = """
UPDATE projects SET
title=%s, slug=%s, description=%s, content=%s, image_url=%s,
github_url=%s, demo_url=%s, technologies=%s, category_id=%s,
is_featured=%s, is_published=%s, updated_at=NOW()
WHERE id=%s
"""
params = (self.title, self.slug, self.description, self.content, self.image_url,
self.github_url, self.demo_url, self.technologies_json, self.category_id,
self.is_featured, self.is_published, self.id)
await db_manager.execute_update(query, params)
return self.id
else:
# Insert new project
query = """
INSERT INTO projects (title, slug, description, content, image_url, github_url, demo_url,
technologies, category_id, is_featured, is_published, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
params = (self.title, self.slug, self.description, self.content, self.image_url,
self.github_url, self.demo_url, self.technologies_json, self.category_id,
self.is_featured, self.is_published, self.created_by)
project_id = await db_manager.execute_insert(query, params)
self.id = project_id
return project_id
@classmethod
async def find_by_id(cls, project_id: int) -> Optional['Project']:
"""Find project by ID"""
query = "SELECT * FROM projects WHERE id = %s"
results = await db_manager.execute_query(query, (project_id,))
if results:
return cls(**results[0])
return None
@classmethod
async def find_by_slug(cls, slug: str) -> Optional['Project']:
"""Find project by slug"""
query = "SELECT * FROM projects WHERE slug = %s AND is_published = TRUE"
results = await db_manager.execute_query(query, (slug,))
if results:
return cls(**results[0])
return None
@classmethod
async def get_all(cls, published_only: bool = True, limit: int = 50, offset: int = 0) -> List['Project']:
"""Get all projects with pagination"""
query = "SELECT * FROM projects"
params = []
if published_only:
query += " WHERE is_published = TRUE"
query += " ORDER BY created_at DESC LIMIT %s OFFSET %s"
params.extend([limit, offset])
results = await db_manager.execute_query(query, tuple(params))
return [cls(**row) for row in results]
@classmethod
async def get_featured(cls, limit: int = 6) -> List['Project']:
"""Get featured projects"""
query = """
SELECT * FROM projects
WHERE is_published = TRUE AND is_featured = TRUE
ORDER BY created_at DESC
LIMIT %s
"""
results = await db_manager.execute_query(query, (limit,))
return [cls(**row) for row in results]
@classmethod
async def get_by_category(cls, category_id: int, limit: int = 20) -> List['Project']:
"""Get projects by category"""
query = """
SELECT * FROM projects
WHERE category_id = %s AND is_published = TRUE
ORDER BY created_at DESC
LIMIT %s
"""
results = await db_manager.execute_query(query, (category_id, limit))
return [cls(**row) for row in results]
@classmethod
async def search(cls, search_term: str, limit: int = 20) -> List['Project']:
"""Search projects by title and description"""
query = """
SELECT * FROM projects
WHERE (title LIKE %s OR description LIKE %s) AND is_published = TRUE
ORDER BY created_at DESC
LIMIT %s
"""
search_pattern = f"%{search_term}%"
results = await db_manager.execute_query(query, (search_pattern, search_pattern, limit))
return [cls(**row) for row in results]
@classmethod
async def count(cls, published_only: bool = True) -> int:
"""Count total projects"""
query = "SELECT COUNT(*) as count FROM projects"
if published_only:
query += " WHERE is_published = TRUE"
results = await db_manager.execute_query(query)
return results[0]['count'] if results else 0
async def delete(self) -> bool:
"""Delete project"""
query = "DELETE FROM projects WHERE id = %s"
affected = await db_manager.execute_update(query, (self.id,))
return affected > 0
def to_dict(self) -> Dict:
"""Convert project to dictionary"""
return {
'id': self.id,
'title': self.title,
'slug': self.slug,
'description': self.description,
'content': self.content,
'image_url': self.image_url,
'github_url': self.github_url,
'demo_url': self.demo_url,
'technologies': self.technologies,
'category_id': self.category_id,
'is_featured': self.is_featured,
'is_published': self.is_published,
'created_by': self.created_by,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
@staticmethod
def generate_slug(title: str) -> str:
"""Generate URL-friendly slug from title"""
import re
slug = re.sub(r'[^\w\s-]', '', title).strip().lower()
slug = re.sub(r'[\s_-]+', '-', slug)
return slug

127
models/settings.py Normal file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Settings Model
import json
from typing import Optional, Dict, List, Any
from .database import db_manager
class Settings:
def __init__(self, **kwargs):
self.id = kwargs.get('id')
self.setting_key = kwargs.get('setting_key')
self.setting_value = kwargs.get('setting_value')
self.description = kwargs.get('description')
self.type = kwargs.get('type', 'text')
self.updated_at = kwargs.get('updated_at')
@property
def parsed_value(self) -> Any:
"""Parse setting value based on type"""
if not self.setting_value:
return None
if self.type == 'boolean':
return self.setting_value.lower() in ('true', '1', 'yes', 'on')
elif self.type == 'json':
try:
return json.loads(self.setting_value)
except:
return {}
else:
return self.setting_value
async def save(self) -> int:
"""Save setting to database"""
if self.id:
# Update existing setting
query = """
UPDATE settings SET
setting_key=%s, setting_value=%s, description=%s, type=%s, updated_at=NOW()
WHERE id=%s
"""
params = (self.setting_key, self.setting_value, self.description, self.type, self.id)
await db_manager.execute_update(query, params)
return self.id
else:
# Insert new setting
query = """
INSERT INTO settings (setting_key, setting_value, description, type)
VALUES (%s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
setting_value=VALUES(setting_value),
description=VALUES(description),
type=VALUES(type),
updated_at=NOW()
"""
params = (self.setting_key, self.setting_value, self.description, self.type)
setting_id = await db_manager.execute_insert(query, params)
self.id = setting_id or self.id
return self.id
@classmethod
async def get(cls, key: str, default: Any = None) -> Any:
"""Get setting value by key"""
query = "SELECT * FROM settings WHERE setting_key = %s"
results = await db_manager.execute_query(query, (key,))
if results:
setting = cls(**results[0])
return setting.parsed_value
return default
@classmethod
async def set(cls, key: str, value: Any, description: str = '', setting_type: str = 'text') -> bool:
"""Set setting value"""
# Convert value to string based on type
if setting_type == 'boolean':
str_value = 'true' if value else 'false'
elif setting_type == 'json':
str_value = json.dumps(value) if isinstance(value, (dict, list)) else str(value)
else:
str_value = str(value)
setting = cls(
setting_key=key,
setting_value=str_value,
description=description,
type=setting_type
)
try:
await setting.save()
return True
except:
return False
@classmethod
async def get_all(cls) -> List['Settings']:
"""Get all settings"""
query = "SELECT * FROM settings ORDER BY setting_key ASC"
results = await db_manager.execute_query(query)
return [cls(**row) for row in results]
@classmethod
async def get_by_prefix(cls, prefix: str) -> List['Settings']:
"""Get settings by key prefix"""
query = "SELECT * FROM settings WHERE setting_key LIKE %s ORDER BY setting_key ASC"
results = await db_manager.execute_query(query, (f"{prefix}%",))
return [cls(**row) for row in results]
async def delete(self) -> bool:
"""Delete setting"""
query = "DELETE FROM settings WHERE id = %s"
affected = await db_manager.execute_update(query, (self.id,))
return affected > 0
def to_dict(self) -> Dict:
"""Convert setting to dictionary"""
return {
'id': self.id,
'setting_key': self.setting_key,
'setting_value': self.setting_value,
'parsed_value': self.parsed_value,
'description': self.description,
'type': self.type,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}

154
models/user.py Normal file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# User Model
import bcrypt
from typing import Optional, Dict, List
from .database import db_manager
from datetime import datetime
class User:
def __init__(self, **kwargs):
self.id = kwargs.get('id')
self.username = kwargs.get('username')
self.email = kwargs.get('email')
self.password_hash = kwargs.get('password_hash')
self.first_name = kwargs.get('first_name')
self.last_name = kwargs.get('last_name')
self.role = kwargs.get('role', 'user')
self.is_active = kwargs.get('is_active', True)
self.created_at = kwargs.get('created_at')
self.updated_at = kwargs.get('updated_at')
@property
def full_name(self) -> str:
"""Get user's full name"""
if self.first_name and self.last_name:
return f"{self.first_name} {self.last_name}"
return self.username
@property
def is_admin(self) -> bool:
"""Check if user is admin"""
return self.role == 'admin'
def set_password(self, password: str) -> None:
"""Hash and set user password"""
salt = bcrypt.gensalt()
self.password_hash = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
def check_password(self, password: str) -> bool:
"""Check if provided password matches hash"""
if not self.password_hash:
return False
return bcrypt.checkpw(password.encode('utf-8'), self.password_hash.encode('utf-8'))
async def save(self) -> int:
"""Save user to database"""
if self.id:
# Update existing user
query = """
UPDATE users SET
username=%s, email=%s, first_name=%s, last_name=%s,
role=%s, is_active=%s, updated_at=NOW()
WHERE id=%s
"""
params = (self.username, self.email, self.first_name, self.last_name,
self.role, self.is_active, self.id)
await db_manager.execute_update(query, params)
return self.id
else:
# Insert new user
query = """
INSERT INTO users (username, email, password_hash, first_name, last_name, role, is_active)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
params = (self.username, self.email, self.password_hash, self.first_name,
self.last_name, self.role, self.is_active)
user_id = await db_manager.execute_insert(query, params)
self.id = user_id
return user_id
@classmethod
async def find_by_id(cls, user_id: int) -> Optional['User']:
"""Find user by ID"""
query = "SELECT * FROM users WHERE id = %s AND is_active = TRUE"
results = await db_manager.execute_query(query, (user_id,))
if results:
return cls(**results[0])
return None
@classmethod
async def find_by_username(cls, username: str) -> Optional['User']:
"""Find user by username"""
query = "SELECT * FROM users WHERE username = %s AND is_active = TRUE"
results = await db_manager.execute_query(query, (username,))
if results:
return cls(**results[0])
return None
@classmethod
async def find_by_email(cls, email: str) -> Optional['User']:
"""Find user by email"""
query = "SELECT * FROM users WHERE email = %s AND is_active = TRUE"
results = await db_manager.execute_query(query, (email,))
if results:
return cls(**results[0])
return None
@classmethod
async def authenticate(cls, username_or_email: str, password: str) -> Optional['User']:
"""Authenticate user with username/email and password"""
# Try to find by username first, then by email
user = await cls.find_by_username(username_or_email)
if not user:
user = await cls.find_by_email(username_or_email)
if user and user.check_password(password):
return user
return None
@classmethod
async def get_all(cls, limit: int = 50, offset: int = 0) -> List['User']:
"""Get all users with pagination"""
query = """
SELECT * FROM users
ORDER BY created_at DESC
LIMIT %s OFFSET %s
"""
results = await db_manager.execute_query(query, (limit, offset))
return [cls(**row) for row in results]
@classmethod
async def count(cls) -> int:
"""Count total users"""
query = "SELECT COUNT(*) as count FROM users WHERE is_active = TRUE"
results = await db_manager.execute_query(query)
return results[0]['count'] if results else 0
async def delete(self) -> bool:
"""Soft delete user (mark as inactive)"""
query = "UPDATE users SET is_active = FALSE WHERE id = %s"
affected = await db_manager.execute_update(query, (self.id,))
return affected > 0
def to_dict(self, include_sensitive: bool = False) -> Dict:
"""Convert user to dictionary"""
data = {
'id': self.id,
'username': self.username,
'email': self.email,
'first_name': self.first_name,
'last_name': self.last_name,
'full_name': self.full_name,
'role': self.role,
'is_active': self.is_active,
'is_admin': self.is_admin,
'created_at': self.created_at.isoformat() if self.created_at else None
}
if include_sensitive:
data['password_hash'] = self.password_hash
return data

View File

@@ -1,37 +1,41 @@
# Core Flask Framework # Core Framework
Flask==3.1.0 Quart==0.20.0
Jinja2==3.1.5 Hypercorn==0.17.3
Werkzeug==3.1.3
click==8.1.8 # Database
itsdangerous==2.2.0 aiomysql==0.2.0
MarkupSafe==3.0.2 PyMySQL==1.1.0
blinker==1.9.0 cryptography==41.0.8
# Authentication # Authentication
Flask-Login==0.6.3 bcrypt==4.1.2
Flask-Bcrypt==1.0.1
bcrypt==4.2.1
# Database - Flask-SQLAlchemy and MariaDB/MySQL driver # Utilities
Flask-SQLAlchemy==3.1.1 aiofiles==24.1.0
SQLAlchemy==2.0.36 python-dotenv==1.0.1
PyMySQL==1.1.1 Jinja2==3.1.5
cryptography==44.0.0 MarkupSafe==3.0.2
Werkzeug==3.1.3
# Configuration Management # Core Dependencies
click==8.1.8
blinker==1.9.0
itsdangerous==2.2.0
typing_extensions==4.12.2
# HTTP/Network
h11==0.14.0
h2==4.1.0
hpack==4.0.0
hyperframe==6.0.1
priority==2.0.0
wsproto==1.2.0
httpx==0.27.0
# Optional
Flask==3.1.0
pydantic==2.10.4 pydantic==2.10.4
pydantic-settings==2.7.1 pydantic-settings==2.7.1
pydantic_core==2.27.2 pydantic_core==2.27.2
python-dotenv==1.0.1
annotated-types==0.7.0 annotated-types==0.7.0
typing_extensions==4.12.2
# Testing
httpx==0.27.0
pytest==8.3.4
# Documentation
Sphinx==8.2.3 Sphinx==8.2.3
# WSGI Server (Production alternative to Flask dev server)
gunicorn==23.0.0

View File

@@ -1,351 +0,0 @@
#!/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, current_app
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from models import db, Profile, Skill, Project, ProjectTag, SocialLink
import os
route_admin = Blueprint('admin', __name__, url_prefix='/admin')
def allowed_file(filename):
"""Check if file extension is allowed"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
def save_uploaded_file(file):
"""Save uploaded file and return the relative path"""
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# Add timestamp to avoid collisions
from datetime import datetime
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_')
filename = timestamp + filename
# Create upload folder if it doesn't exist
upload_folder = current_app.config['UPLOAD_FOLDER']
os.makedirs(upload_folder, exist_ok=True)
filepath = os.path.join(upload_folder, filename)
file.save(filepath)
# Return relative path from static folder
return f"img/{filename}"
return None
@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'))
# Handle profile image upload
profile_image_url = request.form.get('profile_image', profile.profile_image)
if 'profile_image_file' in request.files:
file = request.files['profile_image_file']
if file.filename:
uploaded_path = save_uploaded_file(file)
if uploaded_path:
profile_image_url = uploaded_path
else:
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', '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)
profile.profile_image = profile_image_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':
# Handle image upload
image_url = request.form.get('image_url') # Manual URL input
if 'image_file' in request.files:
file = request.files['image_file']
if file.filename: # Se è stato selezionato un file
uploaded_path = save_uploaded_file(file)
if uploaded_path:
image_url = uploaded_path
else:
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
return redirect(url_for('admin.project_add'))
project = Project(
title=request.form.get('title'),
description=request.form.get('description'),
image_url=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':
# Handle image upload
image_url = request.form.get('image_url', project.image_url)
if 'image_file' in request.files:
file = request.files['image_file']
if file.filename: # Se è stato selezionato un file
uploaded_path = save_uploaded_file(file)
if uploaded_path:
image_url = uploaded_path
else:
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
return redirect(url_for('admin.project_edit', project_id=project_id))
project.title = request.form.get('title', project.title)
project.description = request.form.get('description', project.description)
project.image_url = 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

@@ -1,251 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright Hersel Giannella
"""
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')
# ============================================================================
# PROFILE ENDPOINTS
# ============================================================================
@route_api.route('/profile', methods=['GET'])
def get_profile():
"""Get profile information"""
profile = Profile.query.first()
if profile:
return jsonify(profile.to_dict())
return jsonify({'message': 'Profile not found'}), 404
@route_api.route('/profile', methods=['PUT'])
@login_required
def update_profile():
"""Update profile information"""
profile = Profile.query.first()
if not profile:
return jsonify({'message': 'Profile not found'}), 404
data = request.json
profile.title = data.get('title', profile.title)
profile.lead_text = data.get('lead_text', profile.lead_text)
profile.description_1 = data.get('description_1', profile.description_1)
profile.description_2 = data.get('description_2', profile.description_2)
profile.years_experience = data.get('years_experience', profile.years_experience)
profile.cv_url = data.get('cv_url', profile.cv_url)
db.session.commit()
return jsonify(profile.to_dict())
# ============================================================================
# SKILLS ENDPOINTS
# ============================================================================
@route_api.route('/skills', methods=['GET'])
def get_skills():
"""Get all skills"""
skills = Skill.query.order_by(Skill.display_order).all()
return jsonify([skill.to_dict() for skill in skills])
@route_api.route('/skills', methods=['POST'])
@login_required
def create_skill():
"""Create a new skill"""
data = request.json
skill = Skill(
name=data['name'],
icon_class=data['icon_class'],
category=data.get('category'),
proficiency_level=data.get('proficiency_level'),
display_order=data.get('display_order', 0),
is_active=data.get('is_active', True)
)
db.session.add(skill)
db.session.commit()
return jsonify(skill.to_dict()), 201
@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)
data = request.json
skill.name = data.get('name', skill.name)
skill.icon_class = data.get('icon_class', skill.icon_class)
skill.category = data.get('category', skill.category)
skill.proficiency_level = data.get('proficiency_level', skill.proficiency_level)
skill.display_order = data.get('display_order', skill.display_order)
skill.is_active = data.get('is_active', skill.is_active)
db.session.commit()
return jsonify(skill.to_dict())
@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)
db.session.delete(skill)
db.session.commit()
return jsonify({'message': 'Skill deleted successfully'})
# ============================================================================
# PROJECTS ENDPOINTS
# ============================================================================
@route_api.route('/projects', methods=['GET'])
def get_projects():
"""Get all projects"""
projects = Project.query.order_by(Project.display_order).all()
return jsonify([project.to_dict() for project in projects])
@route_api.route('/projects', methods=['POST'])
@login_required
def create_project():
"""Create a new project"""
data = request.json
project = Project(
title=data['title'],
description=data['description'],
image_url=data.get('image_url'),
demo_url=data.get('demo_url'),
github_url=data.get('github_url'),
display_order=data.get('display_order', 0),
animation_delay=data.get('animation_delay', '0s'),
is_published=data.get('is_published', True)
)
db.session.add(project)
db.session.flush()
# Add tags
tags_data = data.get('tags', [])
for tag_data in tags_data:
tag = ProjectTag(
project_id=project.id,
name=tag_data['name'],
color_class=tag_data.get('color_class', 'bg-primary'),
display_order=tag_data.get('display_order', 0)
)
db.session.add(tag)
db.session.commit()
return jsonify(project.to_dict()), 201
@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)
data = request.json
project.title = data.get('title', project.title)
project.description = data.get('description', project.description)
project.image_url = data.get('image_url', project.image_url)
project.demo_url = data.get('demo_url', project.demo_url)
project.github_url = data.get('github_url', project.github_url)
project.display_order = data.get('display_order', project.display_order)
project.animation_delay = data.get('animation_delay', project.animation_delay)
project.is_published = data.get('is_published', project.is_published)
# Update tags if provided
if 'tags' in data:
# Remove old tags
ProjectTag.query.filter_by(project_id=project.id).delete()
# Add new tags
for tag_data in data['tags']:
tag = ProjectTag(
project_id=project.id,
name=tag_data['name'],
color_class=tag_data.get('color_class', 'bg-primary'),
display_order=tag_data.get('display_order', 0)
)
db.session.add(tag)
db.session.commit()
return jsonify(project.to_dict())
@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)
db.session.delete(project)
db.session.commit()
return jsonify({'message': 'Project deleted successfully'})
# ============================================================================
# SOCIAL LINKS ENDPOINTS
# ============================================================================
@route_api.route('/social-links', methods=['GET'])
def get_social_links():
"""Get all social links"""
links = SocialLink.query.order_by(SocialLink.display_order).all()
return jsonify([link.to_dict() for link in links])
@route_api.route('/social-links', methods=['POST'])
@login_required
def create_social_link():
"""Create a new social link"""
data = request.json
link = SocialLink(
platform_name=data['platform_name'],
url=data['url'],
icon_class=data['icon_class'],
display_order=data.get('display_order', 0),
animation_delay=data.get('animation_delay', '0s'),
is_active=data.get('is_active', True)
)
db.session.add(link)
db.session.commit()
return jsonify(link.to_dict()), 201
@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)
data = request.json
link.platform_name = data.get('platform_name', link.platform_name)
link.url = data.get('url', link.url)
link.icon_class = data.get('icon_class', link.icon_class)
link.display_order = data.get('display_order', link.display_order)
link.animation_delay = data.get('animation_delay', link.animation_delay)
link.is_active = data.get('is_active', link.is_active)
db.session.commit()
return jsonify(link.to_dict())
@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)
db.session.delete(link)
db.session.commit()
return jsonify({'message': 'Social link deleted successfully'})

View File

@@ -1,101 +1,123 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright Hersel Giannella # Authentication Routes
""" from quart import Blueprint, request, render_template, redirect, url_for, session, flash
Authentication routes for login/logout from models.user import User
""" from utils.auth import login_user, logout_user, get_current_user
from utils.validators import validate_email, validate_password, validate_username
from utils.helpers import flash_message
from flask import Blueprint, render_template, request, redirect, url_for, flash auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
from flask_login import login_user, logout_user, current_user, login_required
from models import User, db
from datetime import datetime
route_auth = Blueprint('auth', __name__, url_prefix='/auth') @auth_bp.route('/login', methods=['GET', 'POST'])
async def login():
@route_auth.route('/login', methods=['GET', 'POST'])
def login():
"""Login page""" """Login page"""
# Se l'utente è già autenticato, reindirizza alla dashboard if request.method == 'GET':
if current_user.is_authenticated: return await render_template('auth/login.html')
return redirect(url_for('admin.dashboard'))
if request.method == 'POST': form_data = await request.form
username = request.form.get('username') username_or_email = form_data.get('username', '').strip()
password = request.form.get('password') password = form_data.get('password', '')
remember = request.form.get('remember', False)
if not username or not password: if not username_or_email or not password:
flash('Per favore inserisci username e password.', 'danger') flash_message('Username/Email e password sono richiesti', 'error')
return render_template('auth/login.html') return await render_template('auth/login.html')
user = User.query.filter_by(username=username).first() # Authenticate user
user = await User.authenticate(username_or_email, password)
if user and user.check_password(password): if user:
if not user.is_active: login_user(user)
flash('Il tuo account è stato disabilitato.', 'danger') flash_message(f'Benvenuto, {user.full_name}!', 'success')
return render_template('auth/login.html')
# Aggiorna last_login # Redirect to dashboard if admin, home otherwise
user.last_login = datetime.utcnow() if user.is_admin:
db.session.commit() return redirect(url_for('dashboard.index'))
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: else:
flash('Username o password non corretti.', 'danger') return redirect(url_for('home.index'))
else:
flash_message('Credenziali non valide', 'error')
return await render_template('auth/login.html')
return render_template('auth/login.html') @auth_bp.route('/register', methods=['GET', 'POST'])
async def register():
"""Registration page"""
if request.method == 'GET':
return await render_template('auth/register.html')
form_data = await request.form
username = form_data.get('username', '').strip()
email = form_data.get('email', '').strip()
password = form_data.get('password', '')
confirm_password = form_data.get('confirm_password', '')
first_name = form_data.get('first_name', '').strip()
last_name = form_data.get('last_name', '').strip()
@route_auth.route('/logout') errors = {}
def logout():
# Validate inputs
is_valid, error = validate_username(username)
if not is_valid:
errors['username'] = error
is_valid, error = validate_email(email)
if not is_valid:
errors['email'] = error
is_valid, error = validate_password(password)
if not is_valid:
errors['password'] = error
if password != confirm_password:
errors['confirm_password'] = 'Le password non coincidono'
# Check if user already exists
if not errors:
existing_user = await User.find_by_username(username)
if existing_user:
errors['username'] = 'Username già in uso'
existing_email = await User.find_by_email(email)
if existing_email:
errors['email'] = 'Email già registrata'
if errors:
for field, error in errors.items():
flash_message(error, 'error')
return await render_template('auth/register.html')
# Create new user
user = User(
username=username,
email=email,
first_name=first_name,
last_name=last_name,
role='user' # First user can be manually promoted to admin
)
user.set_password(password)
try:
await user.save()
flash_message('Registrazione completata! Ora puoi effettuare il login.', 'success')
return redirect(url_for('auth.login'))
except Exception as e:
flash_message('Errore durante la registrazione. Riprova.', 'error')
return await render_template('auth/register.html')
@auth_bp.route('/logout')
async def logout():
"""Logout user""" """Logout user"""
logout_user() logout_user()
flash('Logout effettuato con successo.', 'info') flash_message('Logout effettuato con successo', 'success')
return redirect(url_for('route_home.home')) return redirect(url_for('home.index'))
@auth_bp.route('/profile')
async def profile():
"""User profile page"""
user = await get_current_user()
if not user:
return redirect(url_for('auth.login'))
@route_auth.route('/change-password', methods=['GET', 'POST']) return await render_template('auth/profile.html', user=user)
@login_required
def change_password():
"""Change password page"""
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')
# Validation
if not current_password or not new_password or not confirm_password:
flash('Tutti i campi sono obbligatori.', 'danger')
return render_template('auth/change_password.html')
# Check current password
if not current_user.check_password(current_password):
flash('La password attuale non è corretta.', 'danger')
return render_template('auth/change_password.html')
# Check if new passwords match
if new_password != confirm_password:
flash('Le nuove password non corrispondono.', 'danger')
return render_template('auth/change_password.html')
# Check password length
if len(new_password) < 6:
flash('La nuova password deve essere di almeno 6 caratteri.', 'danger')
return render_template('auth/change_password.html')
# Update password
current_user.set_password(new_password)
db.session.commit()
flash('Password modificata con successo!', 'success')
return redirect(url_for('admin.dashboard'))
return render_template('auth/change_password.html')

318
routes/dashboard.py Normal file
View File

@@ -0,0 +1,318 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Dashboard Routes (Admin)
from quart import Blueprint, request, render_template, redirect, url_for, jsonify, session
from models.user import User
from models.project import Project
from models.category import Category
from utils.auth import admin_required, get_current_user
from utils.helpers import flash_message, generate_slug, calculate_pagination
from utils.validators import validate_project_data
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
# Debug route to check authentication status
@dashboard_bp.route('/debug/auth')
async def debug_auth():
"""Debug route to check authentication status"""
current_user = await get_current_user()
session_data = dict(session)
debug_info = {
'session_exists': 'user_id' in session,
'user_id_in_session': session.get('user_id'),
'username_in_session': session.get('username'),
'is_admin_in_session': session.get('is_admin'),
'current_user_found': current_user is not None,
'current_user_is_admin': current_user.is_admin if current_user else None,
'current_user_details': {
'id': current_user.id,
'username': current_user.username,
'email': current_user.email,
'role': current_user.role,
'is_admin': current_user.is_admin
} if current_user else None,
'session_data': session_data
}
return jsonify(debug_info)
# Public route to check dashboard access without admin_required decorator
@dashboard_bp.route('/debug/access')
async def debug_access():
"""Debug route to check dashboard access requirements"""
try:
current_user = await get_current_user()
if not current_user:
return jsonify({
'status': 'error',
'message': 'Nessun utente loggato',
'redirect': url_for('auth.login')
}), 401
if not current_user.is_admin:
return jsonify({
'status': 'error',
'message': f'Utente {current_user.username} non ha privilegi di amministratore',
'user_role': current_user.role,
'is_admin': current_user.is_admin,
'redirect': url_for('home.index')
}), 403
return jsonify({
'status': 'success',
'message': f'Accesso consentito per {current_user.username}',
'user': {
'id': current_user.id,
'username': current_user.username,
'role': current_user.role,
'is_admin': current_user.is_admin
},
'dashboard_url': url_for('dashboard.index')
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Errore durante il controllo accesso: {str(e)}'
}), 500
@dashboard_bp.route('/')
@admin_required
async def index():
"""Dashboard home"""
try:
current_user = await get_current_user()
# Get statistics with error handling
stats = {
'total_users': 0,
'total_projects': 0,
'published_projects': 0,
'featured_projects': 0
}
try:
stats['total_users'] = await User.count()
except Exception as e:
print(f"Error getting user count: {e}")
try:
stats['total_projects'] = await Project.count(published_only=False)
stats['published_projects'] = await Project.count(published_only=True)
featured_projects = await Project.get_featured()
stats['featured_projects'] = len(featured_projects) if featured_projects else 0
except Exception as e:
print(f"Error getting project stats: {e}")
# Get recent projects with error handling
recent_projects = []
try:
recent_projects = await Project.get_all(published_only=False, limit=5)
except Exception as e:
print(f"Error getting recent projects: {e}")
return await render_template('dashboard/index.html',
user=current_user,
stats=stats,
recent_projects=recent_projects)
except Exception as e:
flash_message(f'Errore nel caricamento della dashboard: {str(e)}', 'error')
return redirect(url_for('home.index'))
@dashboard_bp.route('/projects')
@admin_required
async def projects():
"""Projects management"""
page = int(request.args.get('page', 1))
per_page = 10
try:
# Get projects with pagination
projects = await Project.get_all(published_only=False, limit=per_page, offset=(page-1)*per_page)
total_projects = await Project.count(published_only=False)
pagination = calculate_pagination(total_projects, page, per_page)
return await render_template('dashboard/projects.html',
projects=projects,
pagination=pagination)
except Exception as e:
flash_message(f'Errore nel caricamento dei progetti: {str(e)}', 'error')
return redirect(url_for('dashboard.index'))
@dashboard_bp.route('/projects/new', methods=['GET', 'POST'])
@admin_required
async def new_project():
"""Create new project"""
if request.method == 'GET':
try:
categories = await Category.get_all()
return await render_template('dashboard/project_form.html',
project=None,
categories=categories,
action='create')
except Exception as e:
flash_message(f'Errore nel caricamento delle categorie: {str(e)}', 'error')
return redirect(url_for('dashboard.projects'))
form_data = await request.form
data = {
'title': form_data.get('title', '').strip(),
'description': form_data.get('description', '').strip(),
'content': form_data.get('content', '').strip(),
'github_url': form_data.get('github_url', '').strip(),
'demo_url': form_data.get('demo_url', '').strip(),
'image_url': form_data.get('image_url', '').strip(),
'technologies': form_data.getlist('technologies'),
'category_id': int(form_data.get('category_id')) if form_data.get('category_id') else None,
'is_featured': bool(form_data.get('is_featured')),
'is_published': bool(form_data.get('is_published'))
}
# Validate data
is_valid, errors = validate_project_data(data)
if not is_valid:
for field, error in errors.items():
flash_message(error, 'error')
categories = await Category.get_all()
return await render_template('dashboard/project_form.html',
project=data,
categories=categories,
action='create')
# Create project
current_user = await get_current_user()
project = Project(
title=data['title'],
slug=generate_slug(data['title']),
description=data['description'],
content=data['content'],
github_url=data['github_url'],
demo_url=data['demo_url'],
image_url=data['image_url'],
technologies=data['technologies'],
category_id=data['category_id'],
is_featured=data['is_featured'],
is_published=data['is_published'],
created_by=current_user.id
)
try:
await project.save()
flash_message('Progetto creato con successo!', 'success')
return redirect(url_for('dashboard.projects'))
except Exception as e:
flash_message('Errore durante la creazione del progetto', 'error')
categories = await Category.get_all()
return await render_template('dashboard/project_form.html',
project=data,
categories=categories,
action='create')
@dashboard_bp.route('/projects/<int:project_id>/edit', methods=['GET', 'POST'])
@admin_required
async def edit_project(project_id):
"""Edit project"""
project = await Project.find_by_id(project_id)
if not project:
flash_message('Progetto non trovato', 'error')
return redirect(url_for('dashboard.projects'))
if request.method == 'GET':
categories = await Category.get_all()
return await render_template('dashboard/project_form.html',
project=project,
categories=categories,
action='edit')
form_data = await request.form
data = {
'title': form_data.get('title', '').strip(),
'description': form_data.get('description', '').strip(),
'content': form_data.get('content', '').strip(),
'github_url': form_data.get('github_url', '').strip(),
'demo_url': form_data.get('demo_url', '').strip(),
'image_url': form_data.get('image_url', '').strip(),
'technologies': form_data.getlist('technologies'),
'category_id': int(form_data.get('category_id')) if form_data.get('category_id') else None,
'is_featured': bool(form_data.get('is_featured')),
'is_published': bool(form_data.get('is_published'))
}
# Validate data
is_valid, errors = validate_project_data(data)
if not is_valid:
for field, error in errors.items():
flash_message(error, 'error')
categories = await Category.get_all()
return await render_template('dashboard/project_form.html',
project=project,
categories=categories,
action='edit')
# Update project
project.title = data['title']
project.slug = generate_slug(data['title'])
project.description = data['description']
project.content = data['content']
project.github_url = data['github_url']
project.demo_url = data['demo_url']
project.image_url = data['image_url']
project.technologies = data['technologies']
project.category_id = data['category_id']
project.is_featured = data['is_featured']
project.is_published = data['is_published']
try:
await project.save()
flash_message('Progetto aggiornato con successo!', 'success')
return redirect(url_for('dashboard.projects'))
except Exception as e:
flash_message('Errore durante l\'aggiornamento del progetto', 'error')
categories = await Category.get_all()
return await render_template('dashboard/project_form.html',
project=project,
categories=categories,
action='edit')
@dashboard_bp.route('/projects/<int:project_id>/delete', methods=['POST'])
@admin_required
async def delete_project(project_id):
"""Delete project"""
project = await Project.find_by_id(project_id)
if not project:
return jsonify({'error': 'Progetto non trovato'}), 404
try:
await project.delete()
flash_message('Progetto eliminato con successo!', 'success')
return redirect(url_for('dashboard.projects'))
except Exception as e:
flash_message('Errore durante l\'eliminazione del progetto', 'error')
return redirect(url_for('dashboard.projects'))
@dashboard_bp.route('/users')
@admin_required
async def users():
"""Users management"""
page = int(request.args.get('page', 1))
per_page = 10
try:
users = await User.get_all(limit=per_page, offset=(page-1)*per_page)
total_users = await User.count()
pagination = calculate_pagination(total_users, page, per_page)
return await render_template('dashboard/users.html',
users=users,
pagination=pagination)
except Exception as e:
flash_message(f'Errore nel caricamento degli utenti: {str(e)}', 'error')
return redirect(url_for('dashboard.index'))

View File

@@ -2,25 +2,56 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright Hersel Giannella # Copyright Hersel Giannella
# Home Routes
from flask import Blueprint, render_template from quart import Blueprint, render_template
from models import Profile, Skill, Project, SocialLink from models.project import Project
from models.settings import Settings
route_home = Blueprint('route_home', __name__) # Blueprint with correct name
home_bp = Blueprint('home', __name__)
@route_home.route('/') @home_bp.route('/')
def home(): async def index():
"""Render home page with dynamic data from database""" """Homepage with featured projects"""
# Fetch all data from database # Get featured projects
profile = Profile.query.first() featured_projects = await Project.get_featured(limit=6)
skills = Skill.query.filter_by(is_active=True).order_by(Skill.display_order).all()
projects = Project.query.filter_by(is_published=True).order_by(Project.display_order).all()
social_links = SocialLink.query.filter_by(is_active=True).order_by(SocialLink.display_order).all()
return render_template( # Get site settings
'index.html', site_name = await Settings.get('site_name', 'Hersel.it')
profile=profile, site_description = await Settings.get('site_description', 'Portfolio personale di Hersel Giannella')
skills=skills,
projects=projects, return await render_template('home/index.html',
social_links=social_links featured_projects=featured_projects,
) site_name=site_name,
site_description=site_description)
@home_bp.route('/progetti')
async def projects():
"""Projects page"""
# Get all published projects
projects = await Project.get_all(published_only=True, limit=50)
return await render_template('home/projects.html', projects=projects)
@home_bp.route('/progetto/<slug>')
async def project_detail(slug):
"""Single project page"""
project = await Project.find_by_slug(slug)
if not project:
return await render_template('errors/404.html'), 404
return await render_template('home/project_detail.html', project=project)
@home_bp.route('/about')
async def about():
"""About page"""
return await render_template('home/about.html')
@home_bp.route('/contatti')
async def contact():
"""Contact page"""
return await render_template('home/contact.html')
# Keep backward compatibility
route_home = home_bp

View File

@@ -1,143 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin Dashboard{% endblock %} - Portfolio</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--sidebar-width: 250px;
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: var(--sidebar-width);
background: var(--primary-gradient);
color: white;
padding: 0;
overflow-y: auto;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-menu {
padding: 1rem 0;
}
.sidebar-menu a {
color: rgba(255,255,255,0.8);
text-decoration: none;
padding: 0.75rem 1.5rem;
display: block;
transition: all 0.3s;
}
.sidebar-menu a:hover,
.sidebar-menu a.active {
background: rgba(255,255,255,0.1);
color: white;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
}
.top-bar {
background: white;
padding: 1rem 2rem;
margin: -2rem -2rem 2rem -2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.stat-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.btn-gradient {
background: var(--primary-gradient);
border: none;
color: white;
}
.btn-gradient:hover {
opacity: 0.9;
color: white;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h4 class="mb-0"><i class="fas fa-briefcase me-2"></i>Portfolio Admin</h4>
<small class="text-white-50">Ciao, {{ current_user.username }}</small>
</div>
<nav class="sidebar-menu">
<a href="{{ url_for('admin.dashboard') }}" class="{% if request.endpoint == 'admin.dashboard' %}active{% endif %}">
<i class="fas fa-th-large me-2"></i>Dashboard
</a>
<a href="{{ url_for('admin.profile_manage') }}" class="{% if 'profile' in request.endpoint %}active{% endif %}">
<i class="fas fa-user me-2"></i>Profilo
</a>
<a href="{{ url_for('admin.skills_manage') }}" class="{% if 'skills' in request.endpoint %}active{% endif %}">
<i class="fas fa-code me-2"></i>Competenze
</a>
<a href="{{ url_for('admin.projects_manage') }}" class="{% if 'projects' in request.endpoint %}active{% endif %}">
<i class="fas fa-folder-open me-2"></i>Progetti
</a>
<a href="{{ url_for('admin.social_links_manage') }}" class="{% if 'social' in request.endpoint %}active{% endif %}">
<i class="fas fa-share-alt me-2"></i>Link Social
</a>
<hr class="border-white my-3 mx-3">
<a href="{{ url_for('auth.change_password') }}" class="{% if request.endpoint == 'auth.change_password' %}active{% endif %}">
<i class="fas fa-key me-2"></i>Cambia Password
</a>
<a href="{{ url_for('route_home.home') }}" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>Visualizza Sito
</a>
<a href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</a>
</nav>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="top-bar">
<div class="d-flex justify-content-between align-items-center">
<h3 class="mb-0">{% block page_title %}Dashboard{% endblock %}</h3>
<div>
<span class="badge bg-success"><i class="fas fa-check-circle me-1"></i>Online</span>
</div>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -1,126 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Progetti</h6>
<h2 class="mb-0">{{ stats.projects }}</h2>
</div>
<div class="fs-1 text-primary">
<i class="fas fa-folder-open"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Competenze</h6>
<h2 class="mb-0">{{ stats.skills }}</h2>
</div>
<div class="fs-1 text-success">
<i class="fas fa-code"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Link Social</h6>
<h2 class="mb-0">{{ stats.social_links }}</h2>
</div>
<div class="fs-1 text-info">
<i class="fas fa-share-alt"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Pubblicati</h6>
<h2 class="mb-0">{{ stats.published_projects }}</h2>
</div>
<div class="fs-1 text-warning">
<i class="fas fa-eye"></i>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Panoramica</h5>
</div>
<div class="card-body">
<h6>Benvenuto nella Dashboard Admin</h6>
<p class="text-muted">
Da qui puoi gestire tutti i contenuti del tuo portfolio. Usa il menu a sinistra per navigare tra le diverse sezioni.
</p>
<div class="mt-4">
<h6>Azioni Rapide</h6>
<div class="d-flex gap-2 flex-wrap">
<a href="{{ url_for('admin.projects_manage') }}" class="btn btn-gradient">
<i class="fas fa-plus me-2"></i>Nuovo Progetto
</a>
<a href="{{ url_for('admin.skills_manage') }}" class="btn btn-outline-primary">
<i class="fas fa-plus me-2"></i>Nuova Skill
</a>
<a href="{{ url_for('route_home.home') }}" class="btn btn-outline-secondary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>Visualizza Sito
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Info Sistema</h5>
</div>
<div class="card-body">
<div class="mb-3">
<small class="text-muted">Utente</small>
<p class="mb-0"><strong>{{ current_user.username }}</strong></p>
</div>
<div class="mb-3">
<small class="text-muted">Email</small>
<p class="mb-0">{{ current_user.email }}</p>
</div>
<div class="mb-3">
<small class="text-muted">Ultimo Accesso</small>
<p class="mb-0">
{% if current_user.last_login %}
{{ current_user.last_login.strftime('%d/%m/%Y %H:%M') }}
{% else %}
Primo accesso
{% endif %}
</p>
</div>
<hr>
<div class="d-grid">
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-danger">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,77 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Gestione Profilo{% endblock %}
{% block page_title %}Gestione Profilo{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('admin.profile_edit') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">Titolo</label>
<input type="text" class="form-control" id="title" name="title"
value="{{ profile.title if profile else '' }}" required>
</div>
<div class="mb-3">
<label for="lead_text" class="form-label">Testo Principale</label>
<textarea class="form-control" id="lead_text" name="lead_text" rows="3" required>{{ profile.lead_text if profile else '' }}</textarea>
</div>
<div class="mb-3">
<label for="description_1" class="form-label">Descrizione 1</label>
<textarea class="form-control" id="description_1" name="description_1" rows="3">{{ profile.description_1 if profile else '' }}</textarea>
</div>
<div class="mb-3">
<label for="description_2" class="form-label">Descrizione 2</label>
<textarea class="form-control" id="description_2" name="description_2" rows="3">{{ profile.description_2 if profile else '' }}</textarea>
</div>
<div class="mb-3">
<label for="years_experience" class="form-label">Anni di Esperienza</label>
<input type="number" class="form-control" id="years_experience" name="years_experience"
value="{{ profile.years_experience if profile else 0 }}">
</div>
<div class="mb-3">
<label for="cv_url" class="form-label">URL CV (opzionale)</label>
<input type="url" class="form-control" id="cv_url" name="cv_url"
value="{{ profile.cv_url if profile else '' }}">
</div>
<div class="mb-3">
<label class="form-label">Immagine di Profilo</label>
<div class="row">
<div class="col-md-6">
<label for="profile_image_file" class="form-label text-muted small">Upload Immagine</label>
<input type="file" class="form-control" id="profile_image_file" name="profile_image_file" accept="image/*">
<small class="text-muted">Formati supportati: png, jpg, jpeg, gif, webp (max 16 MB)</small>
</div>
<div class="col-md-6">
<label for="profile_image" class="form-label text-muted small">Oppure inserisci URL manualmente</label>
<input type="text" class="form-control" id="profile_image" name="profile_image"
value="{{ profile.profile_image if profile else '' }}" placeholder="img/personal.webp">
<small class="text-muted">Percorso relativo alla cartella static/</small>
</div>
</div>
{% if profile and profile.profile_image %}
<div class="mt-2">
<small class="text-muted">Immagine attuale:</small><br>
<img src="{{ url_for('static', filename=profile.profile_image) }}" alt="Profile" class="img-thumbnail rounded-circle" style="max-width: 200px; max-height: 200px;">
</div>
{% endif %}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-gradient">
<i class="fas fa-save me-2"></i>Salva Modifiche
</button>
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Annulla
</a>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,101 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}{{ 'Modifica' if project else 'Nuovo' }} Progetto{% endblock %}
{% block page_title %}{{ 'Modifica' if project else 'Nuovo' }} Progetto{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">Titolo *</label>
<input type="text" class="form-control" id="title" name="title"
value="{{ project.title if project else '' }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Descrizione *</label>
<textarea class="form-control" id="description" name="description" rows="4" required>{{ project.description if project else '' }}</textarea>
</div>
<div class="mb-3">
<label class="form-label">Immagine del Progetto</label>
<div class="row">
<div class="col-md-6">
<label for="image_file" class="form-label text-muted small">Upload Immagine</label>
<input type="file" class="form-control" id="image_file" name="image_file" accept="image/*">
<small class="text-muted">Formati supportati: png, jpg, jpeg, gif, webp (max 16 MB)</small>
</div>
<div class="col-md-6">
<label for="image_url" class="form-label text-muted small">Oppure inserisci URL manualmente</label>
<input type="text" class="form-control" id="image_url" name="image_url"
value="{{ project.image_url if project else '' }}" placeholder="img/project.webp">
<small class="text-muted">Percorso relativo alla cartella static/</small>
</div>
</div>
{% if project and project.image_url %}
<div class="mt-2">
<small class="text-muted">Immagine attuale:</small><br>
<img src="{{ url_for('static', filename=project.image_url) }}" alt="{{ project.title }}" style="max-width: 200px; max-height: 150px;" class="img-thumbnail">
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="github_url" class="form-label">URL GitHub</label>
<input type="url" class="form-control" id="github_url" name="github_url"
value="{{ project.github_url if project else '' }}">
</div>
<div class="col-md-6 mb-3">
<label for="demo_url" class="form-label">URL Demo</label>
<input type="url" class="form-control" id="demo_url" name="demo_url"
value="{{ project.demo_url if project else '' }}">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="display_order" class="form-label">Ordine Visualizzazione</label>
<input type="number" class="form-control" id="display_order" name="display_order"
value="{{ project.display_order if project else 0 }}">
</div>
<div class="col-md-4 mb-3">
<label for="animation_delay" class="form-label">Delay Animazione</label>
<input type="text" class="form-control" id="animation_delay" name="animation_delay"
value="{{ project.animation_delay if project else '0s' }}" placeholder="0.2s">
</div>
<div class="col-md-4 mb-3">
<div class="form-check mt-4">
<input type="checkbox" class="form-check-input" id="is_published" name="is_published"
{% if not project or project.is_published %}checked{% endif %}>
<label class="form-check-label" for="is_published">
Pubblica il progetto
</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="tags" class="form-label">Tags (separati da virgola)</label>
<input type="text" class="form-control" id="tags" name="tags"
value="{% if project %}{% for tag in project.tags|sort(attribute='display_order') %}{{ tag.name }}:{{ tag.color_class }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}"
placeholder="Python:bg-primary, Flask:bg-info, Docker:bg-success">
<small class="text-muted">Formato: Nome:colore, Nome:colore (es: Python:bg-primary, Flask:bg-info)</small>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-gradient">
<i class="fas fa-save me-2"></i>Salva
</button>
<a href="{{ url_for('admin.projects_manage') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Annulla
</a>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,52 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Gestione Progetti{% endblock %}
{% block page_title %}Gestione Progetti{% endblock %}
{% block content %}
<div class="mb-3">
<a href="{{ url_for('admin.project_add') }}" class="btn btn-gradient">
<i class="fas fa-plus me-2"></i>Nuovo Progetto
</a>
</div>
<div class="row g-4">
{% for project in projects %}
<div class="col-md-4">
<div class="card h-100">
{% if project.image_url %}
<img src="{{ url_for('static', filename=project.image_url) }}" class="card-img-top" alt="{{ project.title }}">
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ project.title }}</h5>
<p class="card-text text-muted small">{{ project.description[:100] }}...</p>
<div class="mb-2">
{% for tag in project.tags %}
<span class="badge {{ tag.color_class }} me-1">{{ tag.name }}</span>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{% if project.is_published %}
<i class="fas fa-eye text-success"></i> Pubblicato
{% else %}
<i class="fas fa-eye-slash text-danger"></i> Bozza
{% endif %}
</small>
<div>
<a href="{{ url_for('admin.project_edit', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit"></i>
</a>
<form method="POST" action="{{ url_for('admin.project_delete', project_id=project.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,132 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Gestione Competenze{% endblock %}
{% block page_title %}Gestione Competenze{% endblock %}
{% block content %}
<div class="card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">Aggiungi Nuova Competenza</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.skill_add') }}" class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" name="name" placeholder="Nome (es. Python)" required>
</div>
<div class="col-md-3">
<input type="text" class="form-control" name="icon_class" placeholder="Icona (es. fab fa-python)" required>
</div>
<div class="col-md-2">
<input type="text" class="form-control" name="category" placeholder="Categoria">
</div>
<div class="col-md-1">
<input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0">
</div>
<div class="col-md-1">
<div class="form-check mt-2">
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_new" checked>
<label class="form-check-label" for="is_active_new">
Attiva
</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-gradient w-100">
<i class="fas fa-plus me-2"></i>Aggiungi
</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">Lista Competenze</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Icona</th>
<th>Nome</th>
<th>Categoria</th>
<th>Ordine</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for skill in skills %}
<tr>
<td><i class="{{ skill.icon_class }} fa-2x text-primary"></i></td>
<td>{{ skill.name }}</td>
<td><span class="badge bg-secondary">{{ skill.category or '-' }}</span></td>
<td>{{ skill.display_order }}</td>
<td>
{% if skill.is_active %}
<span class="badge bg-success">Attiva</span>
{% else %}
<span class="badge bg-danger">Disattiva</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-primary me-1" data-bs-toggle="modal" data-bs-target="#editModal{{ skill.id }}">
<i class="fas fa-edit"></i>
</button>
<form method="POST" action="{{ url_for('admin.skill_delete', skill_id=skill.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<!-- Edit Modal -->
<div class="modal fade" id="editModal{{ skill.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modifica Competenza</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('admin.skill_edit', skill_id=skill.id) }}">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nome</label>
<input type="text" class="form-control" name="name" value="{{ skill.name }}" required>
</div>
<div class="mb-3">
<label class="form-label">Icona (Font Awesome class)</label>
<input type="text" class="form-control" name="icon_class" value="{{ skill.icon_class }}" required>
</div>
<div class="mb-3">
<label class="form-label">Categoria</label>
<input type="text" class="form-control" name="category" value="{{ skill.category or '' }}">
</div>
<div class="mb-3">
<label class="form-label">Ordine di Visualizzazione</label>
<input type="number" class="form-control" name="display_order" value="{{ skill.display_order }}">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_{{ skill.id }}" {% if skill.is_active %}checked{% endif %}>
<label class="form-check-label" for="is_active_{{ skill.id }}">
Competenza Attiva
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-gradient">Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,133 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Gestione Link Social{% endblock %}
{% block page_title %}Gestione Link Social{% endblock %}
{% block content %}
<div class="card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">Aggiungi Nuovo Link</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.social_link_add') }}" class="row g-3">
<div class="col-md-2">
<input type="text" class="form-control" name="platform_name" placeholder="Nome (es. LinkedIn)" required>
</div>
<div class="col-md-3">
<input type="url" class="form-control" name="url" placeholder="URL completo" required>
</div>
<div class="col-md-2">
<input type="text" class="form-control" name="icon_class" placeholder="Icona" required>
</div>
<div class="col-md-1">
<input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0">
</div>
<div class="col-md-2">
<div class="form-check mt-2">
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_new" checked>
<label class="form-check-label" for="is_active_new">
Attivo
</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-gradient w-100">
<i class="fas fa-plus me-2"></i>Aggiungi
</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Icona</th>
<th>Piattaforma</th>
<th>URL</th>
<th>Ordine</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for link in social_links %}
<tr>
<td><i class="{{ link.icon_class }} fa-2x text-primary"></i></td>
<td>{{ link.platform_name }}</td>
<td><a href="{{ link.url }}" target="_blank" class="text-truncate d-inline-block" style="max-width: 200px;">{{ link.url }}</a></td>
<td>{{ link.display_order }}</td>
<td>
{% if link.is_active %}
<span class="badge bg-success">Attivo</span>
{% else %}
<span class="badge bg-danger">Disattivo</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-primary me-1" data-bs-toggle="modal" data-bs-target="#editModal{{ link.id }}">
<i class="fas fa-edit"></i>
</button>
<form method="POST" action="{{ url_for('admin.social_link_delete', link_id=link.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<!-- Edit Modal -->
<div class="modal fade" id="editModal{{ link.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modifica Link Social</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('admin.social_link_edit', link_id=link.id) }}">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nome Piattaforma</label>
<input type="text" class="form-control" name="platform_name" value="{{ link.platform_name }}" required>
</div>
<div class="mb-3">
<label class="form-label">URL</label>
<input type="url" class="form-control" name="url" value="{{ link.url }}" required>
</div>
<div class="mb-3">
<label class="form-label">Icona (Font Awesome class)</label>
<input type="text" class="form-control" name="icon_class" value="{{ link.icon_class }}" required>
</div>
<div class="mb-3">
<label class="form-label">Ordine di Visualizzazione</label>
<input type="number" class="form-control" name="display_order" value="{{ link.display_order }}">
</div>
<div class="mb-3">
<label class="form-label">Delay Animazione (es. 0.1s)</label>
<input type="text" class="form-control" name="animation_delay" value="{{ link.animation_delay }}">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_{{ link.id }}" {% if link.is_active %}checked{% endif %}>
<label class="form-check-label" for="is_active_{{ link.id }}">
Link Attivo
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-gradient">Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,57 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Cambia Password{% endblock %}
{% block page_title %}Cambia Password{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<div class="text-center mb-4">
<i class="fas fa-key fa-3x text-primary mb-3"></i>
<p class="text-muted">Inserisci la tua password attuale e la nuova password</p>
</div>
<form method="POST" action="{{ url_for('auth.change_password') }}">
<div class="mb-3">
<label for="current_password" class="form-label">
<i class="fas fa-lock me-2"></i>Password Attuale *
</label>
<input type="password" class="form-control" id="current_password" name="current_password" required autofocus>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">
<i class="fas fa-key me-2"></i>Nuova Password *
</label>
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6">
<small class="text-muted">Minimo 6 caratteri</small>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">
<i class="fas fa-check-circle me-2"></i>Conferma Nuova Password *
</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="6">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-gradient">
<i class="fas fa-save me-2"></i>Cambia Password
</button>
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Annulla
</a>
</div>
</form>
</div>
</div>
<div class="alert alert-info mt-3">
<i class="fas fa-info-circle me-2"></i>
<strong>Nota:</strong> Dopo aver cambiato la password, verrai reindirizzato alla dashboard.
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,84 +1,38 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="it">
<head> {% block title %}Login - Hersel.it{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>Login - Portfolio Admin</title> <div class="container mt-5">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <div class="row justify-content-center">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <div class="col-md-6 col-lg-5">
<style> <div class="card shadow">
body { <div class="card-body p-5">
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); <div class="text-center mb-4">
min-height: 100vh; <h2 class="card-title">Accedi</h2>
display: flex; <p class="text-muted">Benvenuto su Hersel.it</p>
align-items: center;
justify-content: center;
}
.login-container {
max-width: 450px;
width: 100%;
}
.login-card {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
padding: 2.5rem;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header i {
font-size: 3rem;
color: #667eea;
margin-bottom: 1rem;
}
.btn-login {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 0.75rem;
font-weight: 600;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<i class="fas fa-lock"></i>
<h2>Admin Login</h2>
<p class="text-muted">Accedi per gestire il tuo portfolio</p>
</div> </div>
{% with messages = get_flashed_messages(with_categories=true) %} <form method="POST">
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.login') }}">
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label"> <label for="username" class="form-label">Username o Email</label>
<i class="fas fa-user me-2"></i>Username <div class="input-group">
</label> <span class="input-group-text">
<input type="text" class="form-control" id="username" name="username" required autofocus> <i class="bi bi-person"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label"> <label for="password" class="form-label">Password</label>
<i class="fas fa-key me-2"></i>Password <div class="input-group">
</label> <span class="input-group-text">
<i class="bi bi-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required> <input type="password" class="form-control" id="password" name="password" required>
</div> </div>
</div>
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember"> <input type="checkbox" class="form-check-input" id="remember" name="remember">
@@ -87,26 +41,22 @@
</label> </label>
</div> </div>
<button type="submit" class="btn btn-primary btn-login w-100"> <div class="d-grid">
<i class="fas fa-sign-in-alt me-2"></i>Accedi <button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right"></i> Accedi
</button> </button>
</div>
</form> </form>
<div class="text-center mt-4"> <div class="text-center mt-4">
<a href="{{ url_for('route_home.home') }}" class="text-decoration-none"> <p class="mb-0">
<i class="fas fa-arrow-left me-2"></i>Torna al Portfolio Non hai un account?
</a> <a href="{{ url_for('auth.register') }}" class="text-decoration-none">Registrati qui</a>
</p>
</div> </div>
</div> </div>
<div class="text-center mt-3 text-white">
<small>
<i class="fas fa-info-circle me-1"></i>
Credenziali di default: admin / admin123
</small>
</div> </div>
</div> </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </div>
</body> {% endblock %}
</html>

100
templates/auth/profile.html Normal file
View File

@@ -0,0 +1,100 @@
{% extends "base.html" %}
{% block title %}Il Mio Profilo - Hersel.it{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-lg-4">
<div class="card">
<div class="card-body text-center">
<div class="bg-primary bg-opacity-10 rounded-circle p-4 d-inline-block mb-3">
<i class="bi bi-person-circle display-3 text-primary"></i>
</div>
<h4>{{ user.full_name }}</h4>
<p class="text-muted">@{{ user.username }}</p>
{% if user.is_admin %}
<span class="badge bg-danger">Amministratore</span>
{% else %}
<span class="badge bg-primary">Utente</span>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Informazioni Profilo</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-3">
<strong>Username:</strong>
</div>
<div class="col-sm-9">
{{ user.username }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3">
<strong>Email:</strong>
</div>
<div class="col-sm-9">
{{ user.email }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3">
<strong>Nome Completo:</strong>
</div>
<div class="col-sm-9">
{{ user.full_name }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3">
<strong>Ruolo:</strong>
</div>
<div class="col-sm-9">
{% if user.is_admin %}
<span class="badge bg-danger">Amministratore</span>
{% else %}
<span class="badge bg-primary">Utente</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3">
<strong>Registrato il:</strong>
</div>
<div class="col-sm-9">
{{ user.created_at.strftime('%d/%m/%Y alle %H:%M') if user.created_at else 'N/D' }}
</div>
</div>
<hr>
<div class="d-flex gap-2">
{% if user.is_admin %}
<a href="{{ url_for('dashboard.index') }}" class="btn btn-primary">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
{% endif %}
<button class="btn btn-outline-primary" onclick="alert('Funzione in sviluppo')">
<i class="bi bi-pencil"></i> Modifica Profilo
</button>
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-danger">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block title %}Registrazione - Hersel.it{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card shadow">
<div class="card-body p-5">
<div class="text-center mb-4">
<h2 class="card-title">Registrati</h2>
<p class="text-muted">Crea il tuo account su Hersel.it</p>
</div>
<form method="POST">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="first_name" class="form-label">Nome</label>
<input type="text" class="form-control" id="first_name" name="first_name">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="last_name" class="form-label">Cognome</label>
<input type="text" class="form-control" id="last_name" name="last_name">
</div>
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-text">Minimo 3 caratteri, solo lettere, numeri e underscore</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-envelope"></i>
</span>
<input type="email" class="form-control" id="email" name="email" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="form-text">Minimo 8 caratteri con maiuscola, minuscola e numero</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Conferma Password</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock-fill"></i>
</span>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="terms" name="terms" required>
<label class="form-check-label" for="terms">
Accetto i <a href="#" class="text-decoration-none">Termini e Condizioni</a>
</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-person-plus"></i> Registrati
</button>
</div>
</form>
<div class="text-center mt-4">
<p class="mb-0">
Hai già un account?
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">Accedi qui</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

121
templates/base.html Normal file
View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Hersel.it - Portfolio{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
{% block extra_head %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('home.index') }}">
<strong>Hersel.it</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home.index') }}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#progetti">Progetti</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#contatti">Contatti</a>
</li>
</ul>
<ul class="navbar-nav">
{% if session.get('user_id') %}
{% if session.get('is_admin') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard.index') }}">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{ session.get('username') }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('auth.profile') }}">Profilo</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">Registrati</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container mt-3">
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Main Content -->
<main>
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>Hersel Giannella</h5>
<p>Developer & Portfolio</p>
</div>
<div class="col-md-6 text-end">
<a href="https://github.com/BluLupo" class="text-light me-3">
<i class="bi bi-github"></i> GitHub
</a>
<a href="mailto:info@hersel.it" class="text-light">
<i class="bi bi-envelope"></i> Email
</a>
</div>
</div>
<hr>
<div class="row">
<div class="col text-center">
<small>&copy; 2024 Hersel.it - Tutti i diritti riservati</small>
</div>
</div>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_scripts %}{% endblock %}
</body>
</html>

View File

@@ -3,44 +3,20 @@
<h2 class="text-center section-title animate__animated animate__fadeIn">Chi Sono</h2> <h2 class="text-center section-title animate__animated animate__fadeIn">Chi Sono</h2>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-lg-6 animate__animated animate__fadeInLeft"> <div class="col-lg-6 animate__animated animate__fadeInLeft">
{% if profile %}
<h3 class="mb-4">{{ profile.title }}</h3>
<p class="lead">{{ profile.lead_text }}</p>
{% if profile.description_1 %}
<p>{{ profile.description_1 }}</p>
{% endif %}
{% if profile.description_2 %}
<p>{{ profile.description_2 }}</p>
{% endif %}
<div class="mt-4">
{% if profile.cv_url %}
<a href="{{ profile.cv_url }}" class="btn btn-primary me-2">Scarica CV</a>
{% endif %}
<a href="#links" class="btn btn-outline-secondary">I Miei Profili</a>
</div>
{% else %}
<h3 class="mb-4">Il ponte tra sistemi e sviluppo web</h3> <h3 class="mb-4">Il ponte tra sistemi e sviluppo web</h3>
<p class="lead">Con oltre 7 Anni di esperienza nello sviluppo di applicazioni web con Python Flask, offro soluzioni complete end-to-end.</p> <p class="lead">Con oltre 7 Anni di esperienza nello sviluppo di applicazioni web con Python Flask, offro soluzioni complete end-to-end.</p>
<p>La mia doppia specializzazione mi permette di comprendere a fondo l'intero ciclo di vita delle applicazioni, dall'architettura del server fino all'implementazione e al deployment.</p> <p>La mia doppia specializzazione mi permette di comprendere a fondo l'intero ciclo di vita delle applicazioni, dall'architettura del server fino all'implementazione e al deployment.</p>
<p>Mi piace risolvere problemi complessi e creare soluzioni che siano robuste, scalabili e facili da mantenere.</p> <p>Mi piace risolvere problemi complessi e creare soluzioni che siano robuste, scalabili e facili da mantenere.</p>
<div class="mt-4"> <div class="mt-4">
<!--<a href="#" class="btn btn-primary me-2">Scarica CV</a>-->
<a href="#links" class="btn btn-outline-secondary">I Miei Profili</a> <a href="#links" class="btn btn-outline-secondary">I Miei Profili</a>
</div> </div>
{% endif %}
</div> </div>
<div class="col-lg-6 animate__animated animate__fadeInRight"> <div class="col-lg-6 animate__animated animate__fadeInRight">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-body p-4"> <div class="card-body p-4">
<h4 class="mb-4 text-primary">Tecnologie che utilizzo</h4> <h4 class="mb-4 text-primary">Tecnologie che utilizzo</h4>
<div class="d-flex flex-wrap justify-content-center"> <div class="d-flex flex-wrap justify-content-center">
{% if skills %}
{% for skill in skills %}
<div class="text-center m-3">
<i class="{{ skill.icon_class }} tech-icon"></i>
<p>{{ skill.name }}</p>
</div>
{% endfor %}
{% else %}
<div class="text-center m-3"> <div class="text-center m-3">
<i class="fab fa-linux tech-icon"></i> <i class="fab fa-linux tech-icon"></i>
<p>Linux</p> <p>Linux</p>
@@ -73,7 +49,6 @@
<i class="fas fa-network-wired tech-icon"></i> <i class="fas fa-network-wired tech-icon"></i>
<p>Networking</p> <p>Networking</p>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,16 +6,6 @@
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-body p-5"> <div class="card-body p-5">
<div class="d-flex flex-wrap justify-content-center mb-4"> <div class="d-flex flex-wrap justify-content-center mb-4">
{% if social_links %}
{% for link in social_links %}
<a href="{{ link.url }}" class="m-3 text-decoration-none animate__animated animate__bounceIn" {% if link.animation_delay != '0s' %}style="animation-delay: {{ link.animation_delay }}"{% endif %}>
<div class="text-center">
<i class="{{ link.icon_class }} social-icon"></i>
<p class="mt-2 fs-5">{{ link.platform_name }}</p>
</div>
</a>
{% endfor %}
{% else %}
<a href="https://linkedin.com/in/hersel" class="m-3 text-decoration-none animate__animated animate__bounceIn" style="animation-delay: 0.1s"> <a href="https://linkedin.com/in/hersel" class="m-3 text-decoration-none animate__animated animate__bounceIn" style="animation-delay: 0.1s">
<div class="text-center"> <div class="text-center">
<i class="fab fa-linkedin social-icon"></i> <i class="fab fa-linkedin social-icon"></i>
@@ -52,7 +42,6 @@
<p class="mt-2 fs-5">Email</p> <p class="mt-2 fs-5">Email</p>
</div> </div>
</a> </a>
{% endif %}
</div> </div>
<p class="lead mt-4">Scopri di più sul mio lavoro e segui i miei progetti attraverso questi canali</p> <p class="lead mt-4">Scopri di più sul mio lavoro e segui i miei progetti attraverso questi canali</p>
</div> </div>

View File

@@ -2,32 +2,6 @@
<div class="container"> <div class="container">
<h2 class="text-center section-title animate__animated animate__fadeIn">I Miei Progetti</h2> <h2 class="text-center section-title animate__animated animate__fadeIn">I Miei Progetti</h2>
<div class="row"> <div class="row">
{% if projects %}
{% for project in projects %}
<!-- {{ project.title }} -->
<div class="col-lg-4 col-md-6 mb-4 animate__animated animate__fadeInUp" {% if project.animation_delay != '0s' %}style="animation-delay: {{ project.animation_delay }}"{% endif %}>
<div class="card project-card shadow-sm">
{% if project.image_url %}
<img src="{{ url_for('static', filename=project.image_url) }}" class="card-img-top" alt="{{ project.title }}">
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ project.title }}</h5>
<p class="card-text">{{ project.description }}</p>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
{% for tag in project.tags|sort(attribute='display_order') %}
<span class="badge {{ tag.color_class }} me-1">{{ tag.name }}</span>
{% endfor %}
</div>
{% if project.github_url or project.demo_url %}
<a href="{{ project.demo_url if project.demo_url else project.github_url }}" class="btn btn-sm btn-outline-primary">Dettagli</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<!-- Progetto 1 --> <!-- Progetto 1 -->
<div class="col-lg-4 col-md-6 mb-4 animate__animated animate__fadeInUp"> <div class="col-lg-4 col-md-6 mb-4 animate__animated animate__fadeInUp">
<div class="card project-card shadow-sm"> <div class="card project-card shadow-sm">
@@ -90,7 +64,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<!-- <!--

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Dashboard - Hersel.it{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
.sidebar {
min-height: 100vh;
background-color: #f8f9fa;
}
.sidebar .nav-link {
color: #333;
}
.sidebar .nav-link:hover {
background-color: #e9ecef;
}
.sidebar .nav-link.active {
background-color: #0d6efd;
color: white;
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav class="col-md-3 col-lg-2 d-md-block sidebar collapse">
<div class="position-sticky pt-3">
<div class="text-center mb-4">
<a href="{{ url_for('home.index') }}" class="text-decoration-none">
<h4 class="text-primary">Hersel.it</h4>
</a>
<small class="text-muted">Dashboard Admin</small>
</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'dashboard.index' }}" href="{{ url_for('dashboard.index') }}">
<i class="bi bi-speedometer2"></i> Overview
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if 'project' in request.endpoint }}" href="{{ url_for('dashboard.projects') }}">
<i class="bi bi-folder"></i> Progetti
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'dashboard.users' }}" href="{{ url_for('dashboard.users') }}">
<i class="bi bi-people"></i> Utenti
</a>
</li>
</ul>
<hr>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home.index') }}">
<i class="bi bi-house"></i> Vai al Sito
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</li>
</ul>
</div>
</nav>
<!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% block page_title %}Dashboard{% endblock %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
{% block page_actions %}{% endblock %}
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,159 @@
{% extends "dashboard/base.html" %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<div class="mb-4">
<h3>Benvenuto, {{ user.full_name }}!</h3>
<p class="text-muted">Ecco una panoramica del tuo portfolio</p>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4>{{ stats.total_projects }}</h4>
<p class="mb-0">Progetti Totali</p>
</div>
<div class="align-self-center">
<i class="bi bi-folder fs-1"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4>{{ stats.published_projects }}</h4>
<p class="mb-0">Pubblicati</p>
</div>
<div class="align-self-center">
<i class="bi bi-check-circle fs-1"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4>{{ stats.featured_projects }}</h4>
<p class="mb-0">In Evidenza</p>
</div>
<div class="align-self-center">
<i class="bi bi-star fs-1"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4>{{ stats.total_users }}</h4>
<p class="mb-0">Utenti</p>
</div>
<div class="align-self-center">
<i class="bi bi-people fs-1"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Projects -->
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Progetti Recenti</h5>
</div>
<div class="card-body">
{% if recent_projects %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Titolo</th>
<th>Stato</th>
<th>Creato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for project in recent_projects %}
<tr>
<td>
<strong>{{ project.title }}</strong>
{% if project.is_featured %}
<span class="badge bg-warning ms-1">Featured</span>
{% endif %}
</td>
<td>
{% if project.is_published %}
<span class="badge bg-success">Pubblicato</span>
{% else %}
<span class="badge bg-secondary">Bozza</span>
{% endif %}
</td>
<td>{{ project.created_at.strftime('%d/%m/%Y') if project.created_at else 'N/D' }}</td>
<td>
<a href="{{ url_for('dashboard.edit_project', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">Nessun progetto ancora creato.</p>
<a href="{{ url_for('dashboard.new_project') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> Crea il primo progetto
</a>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Azioni Rapide</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('dashboard.new_project') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> Nuovo Progetto
</a>
<a href="{{ url_for('dashboard.projects') }}" class="btn btn-outline-primary">
<i class="bi bi-folder"></i> Gestisci Progetti
</a>
<a href="{{ url_for('home.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-eye"></i> Visualizza Sito
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,149 @@
{% extends "dashboard/base.html" %}
{% block page_title %}
{% if action == 'create' %}Nuovo Progetto{% else %}Modifica Progetto{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="title" class="form-label">Titolo *</label>
<input type="text" class="form-control" id="title" name="title"
value="{{ project.title if project else '' }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Descrizione Breve</label>
<textarea class="form-control" id="description" name="description" rows="3"
maxlength="1000">{{ project.description if project else '' }}</textarea>
<div class="form-text">Massimo 1000 caratteri</div>
</div>
<div class="mb-3">
<label for="content" class="form-label">Contenuto Completo</label>
<textarea class="form-control" id="content" name="content" rows="10">{{ project.content if project else '' }}</textarea>
<div class="form-text">Supporta HTML di base</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="github_url" class="form-label">URL GitHub</label>
<input type="url" class="form-control" id="github_url" name="github_url"
value="{{ project.github_url if project else '' }}"
placeholder="https://github.com/username/repo">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="demo_url" class="form-label">URL Demo/Live</label>
<input type="url" class="form-control" id="demo_url" name="demo_url"
value="{{ project.demo_url if project else '' }}"
placeholder="https://example.com">
</div>
</div>
</div>
<div class="mb-3">
<label for="image_url" class="form-label">URL Immagine</label>
<input type="url" class="form-control" id="image_url" name="image_url"
value="{{ project.image_url if project else '' }}"
placeholder="https://example.com/image.jpg">
</div>
<div class="mb-3">
<label for="technologies" class="form-label">Tecnologie</label>
<div class="row">
{% set common_techs = ['Python', 'JavaScript', 'HTML', 'CSS', 'React', 'Vue.js', 'Node.js', 'MySQL', 'PostgreSQL', 'MongoDB', 'Docker', 'Git', 'Bootstrap', 'jQuery'] %}
{% for tech in common_techs %}
<div class="col-md-3 col-sm-4 col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="technologies"
value="{{ tech }}" id="tech_{{ loop.index }}"
{% if project and tech in project.technologies %}checked{% endif %}>
<label class="form-check-label" for="tech_{{ loop.index }}">
{{ tech }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="category_id" class="form-label">Categoria</label>
<select class="form-select" id="category_id" name="category_id">
<option value="">Seleziona categoria</option>
{% for category in categories %}
<option value="{{ category.id }}"
{% if project and project.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_featured" name="is_featured" value="1"
{% if project and project.is_featured %}checked{% endif %}>
<label class="form-check-label" for="is_featured">
Progetto in evidenza
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_published" name="is_published" value="1"
{% if not project or project.is_published %}checked{% endif %}>
<label class="form-check-label" for="is_published">
Pubblica progetto
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('dashboard.projects') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Annulla
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check"></i>
{% if action == 'create' %}Crea Progetto{% else %}Aggiorna Progetto{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body">
<h6 class="card-title">Suggerimenti</h6>
<ul class="list-unstyled small">
<li><i class="bi bi-lightbulb text-warning"></i> Un titolo chiaro e descrittivo attira più attenzione</li>
<li><i class="bi bi-lightbulb text-warning"></i> La descrizione breve appare nelle anteprime</li>
<li><i class="bi bi-lightbulb text-warning"></i> Usa il contenuto completo per dettagli tecnici</li>
<li><i class="bi bi-lightbulb text-warning"></i> I progetti "featured" appaiono in homepage</li>
</ul>
</div>
</div>
{% if project and project.image_url %}
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title">Anteprima Immagine</h6>
<img src="{{ project.image_url }}" class="img-fluid rounded" alt="Project preview">
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,117 @@
{% extends "dashboard/base.html" %}
{% block page_title %}Gestione Progetti{% endblock %}
{% block page_actions %}
<a href="{{ url_for('dashboard.new_project') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> Nuovo Progetto
</a>
{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">
{% if projects %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Titolo</th>
<th>Stato</th>
<th>Featured</th>
<th>Creato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr>
<td>
<strong>{{ project.title }}</strong>
{% if project.description %}
<br><small class="text-muted">{{ project.description[:100] }}...</small>
{% endif %}
</td>
<td>
{% if project.is_published %}
<span class="badge bg-success">Pubblicato</span>
{% else %}
<span class="badge bg-secondary">Bozza</span>
{% endif %}
</td>
<td>
{% if project.is_featured %}
<span class="badge bg-warning">Featured</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>{{ project.created_at.strftime('%d/%m/%Y') if project.created_at else 'N/D' }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('dashboard.edit_project', project_id=project.id) }}"
class="btn btn-sm btn-outline-primary" title="Modifica">
<i class="bi bi-pencil"></i>
</a>
<form method="POST" action="{{ url_for('dashboard.delete_project', project_id=project.id) }}"
style="display: inline;"
onsubmit="return confirm('Sei sicuro di voler eliminare questo progetto?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Elimina">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
<nav aria-label="Pagination">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="?page={{ pagination.prev_page }}">Precedente</a>
</li>
{% endif %}
{% for page_num in range(1, pagination.total_pages + 1) %}
{% if page_num == pagination.page %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% elif page_num <= 3 or page_num > pagination.total_pages - 3 or (page_num >= pagination.page - 1 and page_num <= pagination.page + 1) %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% elif page_num == 4 or page_num == pagination.total_pages - 3 %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ pagination.next_page }}">Successivo</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-folder2-open display-1 text-muted"></i>
<h4 class="mt-3">Nessun progetto ancora</h4>
<p class="text-muted">Inizia creando il tuo primo progetto</p>
<a href="{{ url_for('dashboard.new_project') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> Crea Primo Progetto
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,157 @@
{% extends "dashboard/base.html" %}
{% block title %}Gestione Utenti - Dashboard{% endblock %}
{% block dashboard_content %}
<div class="dashboard-header">
<h1 class="dashboard-title">
<i class="fas fa-users"></i>
Gestione Utenti
</h1>
<div class="dashboard-actions">
<span class="badge badge-info">{{ pagination.total_items if pagination else 0 }} utenti totali</span>
</div>
</div>
<div class="dashboard-content">
{% if users %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Username</th>
<th>Nome Completo</th>
<th>Email</th>
<th>Ruolo</th>
<th>Stato</th>
<th>Registrato</th>
<th>Ultimo Accesso</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>
<strong>{{ user.username }}</strong>
{% if user.is_admin %}
<span class="badge badge-danger ms-1">Admin</span>
{% endif %}
</td>
<td>{{ user.full_name or '-' }}</td>
<td>{{ user.email }}</td>
<td>
<span class="badge {% if user.role == 'admin' %}badge-danger{% else %}badge-secondary{% endif %}">
{{ user.role.title() }}
</span>
</td>
<td>
<span class="badge {% if user.is_active %}badge-success{% else %}badge-warning{% endif %}">
{% if user.is_active %}Attivo{% else %}Inattivo{% endif %}
</span>
</td>
<td>
{% if user.created_at %}
<small>{{ user.created_at.strftime('%d/%m/%Y %H:%M') }}</small>
{% else %}
-
{% endif %}
</td>
<td>
{% if user.last_login %}
<small>{{ user.last_login.strftime('%d/%m/%Y %H:%M') }}</small>
{% else %}
<span class="text-muted">Mai</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination and pagination.total_pages > 1 %}
<nav aria-label="User pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('dashboard.users', page=pagination.prev_num) }}">
<i class="fas fa-chevron-left"></i> Precedente
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('dashboard.users', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('dashboard.users', page=pagination.next_num) }}">
Successivo <i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info text-center">
<i class="fas fa-info-circle fa-2x mb-3"></i>
<h4>Nessun utente trovato</h4>
<p class="mb-0">Non ci sono utenti registrati nel sistema.</p>
</div>
{% endif %}
</div>
<style>
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.dashboard-title {
color: #495057;
margin: 0;
font-weight: 600;
}
.dashboard-title i {
color: #6c757d;
margin-right: 0.5rem;
}
.table th {
font-weight: 600;
border-top: none;
}
.badge {
font-size: 0.75em;
}
.alert i {
color: #0dcaf0;
}
</style>
{% endblock %}

28
templates/errors/404.html Normal file
View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Pagina Non Trovata - Hersel.it{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6 text-center py-5">
<div class="py-5">
<i class="bi bi-exclamation-triangle-fill display-1 text-warning"></i>
<h1 class="display-4 fw-bold mt-4">404</h1>
<h2 class="mb-4">Pagina Non Trovata</h2>
<p class="lead text-muted mb-4">
Spiacente, la pagina che stai cercando non esiste o è stata spostata.
</p>
<div class="d-flex gap-3 justify-content-center">
<a href="{{ url_for('home.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Torna alla Home
</a>
<a href="{{ url_for('home.projects') }}" class="btn btn-outline-primary">
<i class="bi bi-folder"></i> Vedi i Progetti
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

29
templates/errors/500.html Normal file
View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block title %}Errore del Server - Hersel.it{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6 text-center py-5">
<div class="py-5">
<i class="bi bi-exclamation-octagon-fill display-1 text-danger"></i>
<h1 class="display-4 fw-bold mt-4">500</h1>
<h2 class="mb-4">Errore del Server</h2>
<p class="lead text-muted mb-4">
Si è verificato un errore interno del server.
Il problema è stato segnalato e verrà risolto al più presto.
</p>
<div class="d-flex gap-3 justify-content-center">
<a href="{{ url_for('home.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Torna alla Home
</a>
<a href="javascript:history.back()" class="btn btn-outline-primary">
<i class="bi bi-arrow-left"></i> Torna Indietro
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

213
templates/home/index.html Normal file
View File

@@ -0,0 +1,213 @@
{% extends "base.html" %}
{% block title %}{{ site_name }} - {{ site_description }}{% endblock %}
{% block content %}
<!-- Hero Section -->
<section class="bg-primary text-white py-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-8">
<h1 class="display-4 fw-bold">Ciao, sono Hersel Giannella</h1>
<p class="lead mb-4">{{ site_description }}</p>
<div class="d-flex gap-3">
<a href="#progetti" class="btn btn-light btn-lg">
<i class="bi bi-folder"></i> I Miei Progetti
</a>
<a href="{{ url_for('home.contact') }}" class="btn btn-outline-light btn-lg">
<i class="bi bi-envelope"></i> Contattami
</a>
</div>
</div>
<div class="col-lg-4 text-center">
<div class="bg-white bg-opacity-10 rounded-circle p-4 d-inline-block">
<i class="bi bi-code-slash display-1"></i>
</div>
</div>
</div>
</div>
</section>
<!-- Featured Projects Section -->
<section id="progetti" class="py-5">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold">Progetti in Evidenza</h2>
<p class="text-muted">Alcuni dei miei lavori più interessanti</p>
</div>
{% if featured_projects %}
<div class="row g-4">
{% for project in featured_projects %}
<div class="col-lg-4 col-md-6">
<div class="card h-100 shadow-sm hover-card">
{% if project.image_url %}
<img src="{{ project.image_url }}" class="card-img-top" alt="{{ project.title }}" style="height: 200px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="bi bi-folder display-4 text-muted"></i>
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ project.title }}</h5>
<p class="card-text text-muted">{{ project.description[:100] }}{% if project.description|length > 100 %}...{% endif %}</p>
{% if project.technologies %}
<div class="mb-3">
{% for tech in project.technologies[:3] %}
<span class="badge bg-secondary me-1">{{ tech }}</span>
{% endfor %}
{% if project.technologies|length > 3 %}
<span class="badge bg-light text-dark">+{{ project.technologies|length - 3 }}</span>
{% endif %}
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a href="{{ url_for('home.project_detail', slug=project.slug) }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Dettagli
</a>
{% if project.github_url %}
<a href="{{ project.github_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-github"></i> GitHub
</a>
{% endif %}
</div>
{% if project.demo_url %}
<a href="{{ project.demo_url }}" target="_blank" class="btn btn-sm btn-success">
<i class="bi bi-box-arrow-up-right"></i> Demo
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="text-center mt-5">
<a href="{{ url_for('home.projects') }}" class="btn btn-outline-primary btn-lg">
<i class="bi bi-folder"></i> Vedi Tutti i Progetti
</a>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-folder2-open display-1 text-muted"></i>
<h4 class="mt-3">Progetti in arrivo</h4>
<p class="text-muted">Sto lavorando su alcuni progetti interessanti!</p>
</div>
{% endif %}
</div>
</section>
<!-- Skills Section -->
<section class="bg-light py-5">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold">Le Mie Competenze</h2>
<p class="text-muted">Tecnologie e strumenti che utilizzo</p>
</div>
<div class="row g-4">
<div class="col-lg-3 col-md-6">
<div class="text-center">
<div class="bg-primary bg-opacity-10 rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-code-slash fs-1 text-primary"></i>
</div>
<h5>Backend Development</h5>
<p class="text-muted">Python, Quart, Flask, FastAPI</p>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="text-center">
<div class="bg-success bg-opacity-10 rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-palette fs-1 text-success"></i>
</div>
<h5>Frontend Development</h5>
<p class="text-muted">HTML, CSS, JavaScript, Bootstrap</p>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="text-center">
<div class="bg-warning bg-opacity-10 rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-database fs-1 text-warning"></i>
</div>
<h5>Database</h5>
<p class="text-muted">MySQL, PostgreSQL, MongoDB</p>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="text-center">
<div class="bg-info bg-opacity-10 rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-tools fs-1 text-info"></i>
</div>
<h5>DevOps & Tools</h5>
<p class="text-muted">Docker, Git, Linux, CI/CD</p>
</div>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section id="contatti" class="py-5">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold">Parliamo del Tuo Progetto</h2>
<p class="text-muted">Sono sempre interessato a nuove opportunità e collaborazioni</p>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="row g-4">
<div class="col-md-4 text-center">
<div class="bg-primary bg-opacity-10 rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-envelope fs-2 text-primary"></i>
</div>
<h6>Email</h6>
<a href="mailto:info@hersel.it" class="text-decoration-none">info@hersel.it</a>
</div>
<div class="col-md-4 text-center">
<div class="bg-dark rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-github fs-2 text-white"></i>
</div>
<h6>GitHub</h6>
<a href="https://github.com/BluLupo" target="_blank" class="text-decoration-none">BluLupo</a>
</div>
<div class="col-md-4 text-center">
<div class="bg-info bg-opacity-10 rounded-circle p-3 d-inline-block mb-3">
<i class="bi bi-linkedin fs-2 text-info"></i>
</div>
<h6>LinkedIn</h6>
<a href="#" target="_blank" class="text-decoration-none">Hersel Giannella</a>
</div>
</div>
<div class="text-center mt-5">
<a href="{{ url_for('home.contact') }}" class="btn btn-primary btn-lg">
<i class="bi bi-chat-dots"></i> Inizia una Conversazione
</a>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extra_head %}
<style>
.hover-card {
transition: transform 0.2s;
}
.hover-card:hover {
transform: translateY(-5px);
}
</style>
{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Progetti - Hersel.it{% endblock %}
{% block content %}
<div class="container py-5">
<div class="text-center mb-5">
<h1 class="fw-bold">I Miei Progetti</h1>
<p class="lead text-muted">Una raccolta dei lavori che ho realizzato</p>
</div>
{% if projects %}
<div class="row g-4">
{% for project in projects %}
<div class="col-lg-4 col-md-6">
<div class="card h-100 shadow-sm">
{% if project.image_url %}
<img src="{{ project.image_url }}" class="card-img-top" alt="{{ project.title }}" style="height: 200px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="bi bi-folder display-4 text-muted"></i>
</div>
{% endif %}
<div class="card-body d-flex flex-column">
<div class="mb-auto">
<h5 class="card-title">{{ project.title }}</h5>
<p class="card-text text-muted">{{ project.description[:150] }}{% if project.description|length > 150 %}...{% endif %}</p>
{% if project.technologies %}
<div class="mb-3">
{% for tech in project.technologies[:4] %}
<span class="badge bg-secondary me-1 mb-1">{{ tech }}</span>
{% endfor %}
{% if project.technologies|length > 4 %}
<span class="badge bg-light text-dark">+{{ project.technologies|length - 4 }}</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="btn-group">
<a href="{{ url_for('home.project_detail', slug=project.slug) }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Dettagli
</a>
{% if project.github_url %}
<a href="{{ project.github_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-github"></i> Codice
</a>
{% endif %}
</div>
{% if project.demo_url %}
<a href="{{ project.demo_url }}" target="_blank" class="btn btn-sm btn-success">
<i class="bi bi-box-arrow-up-right"></i> Demo Live
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-folder2-open display-1 text-muted"></i>
<h4 class="mt-3">Nessun progetto disponibile</h4>
<p class="text-muted">Sto lavorando su alcuni progetti interessanti!</p>
<a href="{{ url_for('home.index') }}" class="btn btn-primary">
<i class="bi bi-arrow-left"></i> Torna alla Home
</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -19,7 +19,7 @@
</div> </div>
<div class="col-lg-6 d-flex justify-content-center animate__animated animate__fadeInRight"> <div class="col-lg-6 d-flex justify-content-center animate__animated animate__fadeInRight">
<div class="text-center"> <div class="text-center">
<img src="{{ url_for('static', filename=profile.profile_image if profile and profile.profile_image else 'img/personal.webp') }}" alt="Profile" class="img-fluid rounded-circle shadow" style="max-width: 350px;"> <img src="{{ url_for('static', filename='img/personal.webp') }}" alt="Profile" class="img-fluid rounded-circle shadow" style="max-width: 350px;">
</div> </div>
</div> </div>
</div> </div>

10
utils/__init__.py Normal file
View 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
View 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)

147
utils/create_admin.py Normal file
View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Utility script to create an admin user
import asyncio
import sys
import os
# Add the parent directory to Python path to import our modules
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from models.database import init_database
from models.user import User
async def create_admin_user():
"""Create an admin user for dashboard access"""
try:
# Initialize database
await init_database()
print("✅ Database connection established")
# Check if admin user already exists
existing_admin = await User.find_by_username('admin')
if existing_admin:
print("⚠️ Admin user already exists!")
print(f" Username: {existing_admin.username}")
print(f" Email: {existing_admin.email}")
print(f" Role: {existing_admin.role}")
print(f" Is Admin: {existing_admin.is_admin}")
return
# Create admin user
admin_user = User(
username='admin',
email='admin@hersel.it',
first_name='Admin',
last_name='User',
role='admin'
)
# Set password (change this to a secure password)
admin_password = 'admin123' # CHANGE THIS IN PRODUCTION!
admin_user.set_password(admin_password)
# Save user
await admin_user.save()
print("🎉 Admin user created successfully!")
print("📝 Login credentials:")
print(f" Username: {admin_user.username}")
print(f" Email: {admin_user.email}")
print(f" Password: {admin_password}")
print(f" Role: {admin_user.role}")
print(f" Is Admin: {admin_user.is_admin}")
print("")
print("🔐 IMPORTANT: Change the default password after first login!")
print("📍 You can now access the dashboard at: /dashboard/")
except Exception as e:
print(f"❌ Error creating admin user: {e}")
import traceback
traceback.print_exc()
async def list_users():
"""List all users in the system"""
try:
await init_database()
users = await User.get_all()
if not users:
print("No users found in the system.")
return
print("\n👥 Users in the system:")
print("-" * 80)
print(f"{'ID':<5} {'Username':<15} {'Email':<25} {'Role':<10} {'Admin':<7} {'Active':<8}")
print("-" * 80)
for user in users:
print(f"{user.id:<5} {user.username:<15} {user.email:<25} {user.role:<10} {user.is_admin:<7} {user.is_active:<8}")
print("-" * 80)
print(f"Total users: {len(users)}")
except Exception as e:
print(f"❌ Error listing users: {e}")
async def promote_user_to_admin(username):
"""Promote an existing user to admin"""
try:
await init_database()
user = await User.find_by_username(username)
if not user:
print(f"❌ User '{username}' not found.")
return
# Update user role
user.role = 'admin'
await user.save()
print(f"✅ User '{username}' promoted to admin successfully!")
print(f" Username: {user.username}")
print(f" Email: {user.email}")
print(f" Role: {user.role}")
print(f" Is Admin: {user.is_admin}")
except Exception as e:
print(f"❌ Error promoting user: {e}")
def print_usage():
"""Print usage instructions"""
print("Usage:")
print(" python utils/create_admin.py create # Create default admin user")
print(" python utils/create_admin.py list # List all users")
print(" python utils/create_admin.py promote <username> # Promote user to admin")
print("")
print("Examples:")
print(" python utils/create_admin.py create")
print(" python utils/create_admin.py promote john_doe")
async def main():
"""Main function"""
if len(sys.argv) < 2:
print_usage()
return
command = sys.argv[1].lower()
if command == 'create':
await create_admin_user()
elif command == 'list':
await list_users()
elif command == 'promote':
if len(sys.argv) < 3:
print("❌ Please provide a username to promote.")
print(" Usage: python utils/create_admin.py promote <username>")
return
username = sys.argv[2]
await promote_user_to_admin(username)
else:
print(f"❌ Unknown command: {command}")
print_usage()
if __name__ == '__main__':
asyncio.run(main())

74
utils/helpers.py Normal file
View 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
View 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