Security Features:
- Added User model with bcrypt password hashing
- Implemented Flask-Login for session management
- Protected all API write operations with @login_required decorator
- Added authentication routes (login/logout)
Admin Dashboard:
- Created comprehensive admin dashboard with statistics
- Profile management interface
- Skills management (add/edit/delete)
- Projects management with full CRUD operations
- Social links management
- Modern responsive UI with Bootstrap 5
New Files:
- models.py: Added User model with bcrypt
- routes/auth.py: Login/logout functionality
- routes/admin.py: Complete admin dashboard with CRUD operations
- templates/auth/login.html: Login page
- templates/admin/base.html: Admin base template
- templates/admin/dashboard.html: Main dashboard
- templates/admin/profile.html: Profile editor
- templates/admin/skills.html: Skills manager
- templates/admin/projects.html: Projects list
- templates/admin/project_form.html: Project editor
- templates/admin/social_links.html: Social links manager
Modified Files:
- app.py: Integrated Flask-Login and bcrypt, registered new blueprints
- requirements.txt: Added Flask-Login, Flask-Bcrypt, bcrypt
- init_db.py: Creates default admin user (admin/admin123)
- routes/api.py: Protected all write operations with authentication
Default Credentials:
- Username: admin
- Password: admin123
- ⚠️ MUST be changed after first login!
Benefits:
- Secure API access with session-based authentication
- User-friendly admin interface for content management
- No need to edit code or database directly
- Bcrypt password hashing for security
- Protected against unauthorized access
252 lines
8.3 KiB
Python
252 lines
8.3 KiB
Python
#!/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'})
|