Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from flask_migrate import Migrate
from dotenv import load_dotenv
import os
from pathlib import Path

# Load environment variables
load_dotenv()
Expand All @@ -17,7 +18,10 @@ def create_app():
app = Flask(__name__)

# Configuration
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'change-me-in-production')
secret_key = os.getenv('SECRET_KEY')
if not secret_key:
raise RuntimeError('SECRET_KEY must be set via environment variable.')
app.config['SECRET_KEY'] = secret_key

database_url = os.getenv('SQLALCHEMY_DATABASE_URI')
if not database_url:
Expand All @@ -32,6 +36,7 @@ def create_app():
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', 'static/uploads')
app.config['MAX_CONTENT_LENGTH'] = int(os.getenv('MAX_CONTENT_LENGTH', 16777216))
Path(app.config['UPLOAD_FOLDER']).mkdir(parents=True, exist_ok=True)

# Initialize Flask extensions
db.init_app(app)
Expand All @@ -54,4 +59,4 @@ def create_app():
# Create database tables
db.create_all()

return app
return app
38 changes: 32 additions & 6 deletions app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,33 @@
from werkzeug.utils import secure_filename
from slugify import slugify
import os
import uuid
from sqlalchemy.exc import IntegrityError
from PIL import Image
from . import db
from .models import Post

admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}


def _save_featured_image(file_storage):
filename = secure_filename(file_storage.filename or '')
_, ext = os.path.splitext(filename.lower())
if ext not in ALLOWED_IMAGE_EXTENSIONS:
raise ValueError('Unsupported image format. Allowed: jpg, jpeg, png, gif, webp.')

try:
image = Image.open(file_storage.stream)
image.verify()
file_storage.stream.seek(0)
except Exception as exc:
raise ValueError('Uploaded file is not a valid image.') from exc

unique_name = f"{uuid.uuid4().hex}{ext}"
destination = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_name)
file_storage.save(destination)
return unique_name

def admin_required(f):
@wraps(f)
Expand Down Expand Up @@ -78,9 +100,11 @@ def create_post():
if 'featured_image' in request.files:
file = request.files['featured_image']
if file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
post.featured_image = filename
try:
post.featured_image = _save_featured_image(file)
except ValueError as exc:
flash(str(exc))
return render_template('admin/post_form.html', post=post)

db.session.add(post)
try:
Expand Down Expand Up @@ -109,9 +133,11 @@ def edit_post(id):
if 'featured_image' in request.files:
file = request.files['featured_image']
if file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
post.featured_image = filename
try:
post.featured_image = _save_featured_image(file)
except ValueError as exc:
flash(str(exc))
return render_template('admin/post_form.html', post=post)

try:
db.session.commit()
Expand Down
15 changes: 10 additions & 5 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from urllib.parse import urlparse, urljoin
from . import db
from .models import User

auth_bp = Blueprint('auth', __name__)


def _is_safe_redirect_url(target):
host_url = urlparse(request.host_url)
redirect_url = urlparse(urljoin(request.host_url, target or ''))
return redirect_url.scheme in ('http', 'https') and host_url.netloc == redirect_url.netloc

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
Expand All @@ -18,7 +25,9 @@ def login():
if user and user.check_password(password):
login_user(user)
next_page = request.args.get('next')
return redirect(next_page if next_page else url_for('blog.index'))
if next_page and _is_safe_redirect_url(next_page):
return redirect(next_page)
return redirect(url_for('blog.index'))

flash('Invalid email or password')

Expand Down Expand Up @@ -47,10 +56,6 @@ def register():
user = User(username=username, email=email)
user.set_password(password)

# First user is automatically an admin
if User.query.count() == 0:
user.is_admin = True

db.session.add(user)
db.session.commit()

Expand Down
17 changes: 17 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from sqlalchemy import and_, or_
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import bleach
import markdown
from . import db, login_manager

"""
Expand Down Expand Up @@ -93,3 +95,18 @@ def public_filter(cls):

def __repr__(self):
return f'<Post {self.title}>'

@property
def rendered_content(self):
rendered = markdown.markdown(self.content or '')
allowed_tags = set(bleach.sanitizer.ALLOWED_TAGS).union({
'p', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'hr', 'br', 'ul', 'ol', 'li', 'strong', 'em',
'a', 'img'
})
allowed_attrs = {
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
'a': ['href', 'title', 'rel'],
'img': ['src', 'alt', 'title'],
}
return bleach.clean(rendered, tags=allowed_tags, attributes=allowed_attrs, protocols={'http', 'https', 'mailto'}, strip=True)
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ Markdown==3.5.2
Pillow==10.2.0
gunicorn==21.2.0
email-validator==2.1.1
alembic==1.13.1
alembic==1.13.1
bleach==6.2.0

2 changes: 1 addition & 1 deletion templates/blog/post_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h1 class="mb-4">{{ post.title }}</h1>
</div>

<div class="blog-content">
{{ post.content | safe }}
{{ post.rendered_content | safe }}
</div>
</article>
{% endblock %}
Loading