8 Commits

Author SHA1 Message Date
0436d6e24a update app.py to provide nonce
All checks were successful
Docker Build and Publish / build (release) Successful in 6s
2025-01-31 18:41:44 -05:00
3e11c63e33 Fix admin page nonce
All checks were successful
Docker Build and Publish / build (release) Successful in 33s
2025-01-31 18:32:33 -05:00
5b0b30d69c Fix CSP Error
All checks were successful
Docker Build and Publish / build (push) Successful in 6s
Docker Build and Publish / build (release) Successful in 7s
Getting rid of inline onclick calls and registering the handler in the
primary script ensure securty (XSS).
2024-12-08 18:03:44 -05:00
9022facac5 EXIF Error handling 2024-11-14 18:56:46 -05:00
07725c99b4 Add release process
All checks were successful
Docker Build and Publish / build (push) Successful in 6s
Docker Build and Publish / build (release) Successful in 7s
2024-11-05 23:49:09 -05:00
05a184fcf7 Sizing Fix
All checks were successful
Docker Build and Publish / build (push) Successful in 7s
2024-11-05 22:11:45 -05:00
13e61b7bef remove duplicate secret key handling
All checks were successful
Docker Build and Publish / build (push) Successful in 6s
2024-11-05 19:40:29 -05:00
4c993ebacd Secret Key
All checks were successful
Docker Build and Publish / build (push) Successful in 6s
2024-11-05 19:36:44 -05:00
4 changed files with 102 additions and 41 deletions

View File

@ -3,9 +3,11 @@ name: Docker Build and Publish
on:
push:
branches: [ main ]
tags: [ 'v*' ]
tags: [ 'v*.*.*' ]
pull_request:
branches: [ main ]
release:
types: [published]
jobs:
build:
@ -20,11 +22,11 @@ jobs:
with:
images: git.dws.rip/${{ github.repository }}
tags: |
type=ref,event=branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=sha,format=long
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Login to Gitea Container Registry
uses: docker/login-action@v2

View File

@ -114,4 +114,12 @@ spectra/
- `FLASK_ENV`: Set to 'production' in production
- `WORKERS`: Number of Gunicorn workers (default: 4)
- `PORT`: Override default port (default: 5000)
- `PORT`: Override default port (default: 5000)
## Release Process
To create a release:
- Create and push a tag: `git tag v1.0.0 && git push origin v1.0.0`
- Create a release in Gitea UI using that tag
- The workflow will build and push the Docker image with appropriate version tags
- The Docker image will be available at: `git.dws.rip/your-repo/image:v1.0.0`

79
app.py
View File

@ -11,7 +11,6 @@ from datetime import datetime
from pathlib import Path
from logging import getLogger
import logging
from logging import getLogger
from logging.config import dictConfig
import toml
@ -31,13 +30,31 @@ from models import Session as DBSession
from models import SiteConfig, init_db
from steganography import embed_message, extract_message
# Add this function to handle secret key persistence
def get_or_create_secret_key():
"""Get existing secret key or create a new one"""
secret_key_file = Path("secret.key")
try:
if secret_key_file.exists():
logger.info("Loading existing secret key")
return secret_key_file.read_bytes()
else:
logger.info("Generating new secret key")
secret_key = os.urandom(32) # Use 32 bytes for better security
secret_key_file.write_bytes(secret_key)
return secret_key
except Exception as e:
logger.error(f"Error handling secret key: {e}")
# Fallback to a memory-only key if file operations fail
return os.urandom(32)
DEFAULT_CONFIG = {
"server": {"host": "0.0.0.0", "port": 5000},
"directories": {"upload": "uploads", "thumbnail": "thumbnails"},
"admin": {"password": secrets.token_urlsafe(16)}, # Generate secure random password
}
# Add this logging configuration before creating the Flask app
# Configure logging
dictConfig({
'version': 1,
'formatters': {
@ -66,8 +83,9 @@ dictConfig({
# Get logger for this module
logger = getLogger(__name__)
# Create Flask app with persistent secret key
app = Flask(__name__)
app.secret_key = os.urandom(24)
app.secret_key = get_or_create_secret_key()
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@ -211,9 +229,6 @@ limiter = Limiter(
storage_uri="memory://",
)
# Generate a strong secret key at startup
app.secret_key = secrets.token_hex(32)
@app.before_request
def before_request():
g.csp_nonce = secrets.token_hex(16)
@ -274,7 +289,7 @@ def get_images():
images = []
for photo in photos:
factor = random.randint(2, 3)
if photo.height < 4000 or photo.width < 4000:
if photo.height < 4000 and photo.width < 4000:
factor = 1
if photo.orientation == 6 or photo.orientation == 8:
width, height = photo.height, photo.width
@ -316,6 +331,7 @@ def admin():
photos=photos,
accent_color=config["appearance"]["accent_color"],
config=config,
nonce=g.csp_nonce,
)
@app.route("/admin/logout")
@ -446,17 +462,22 @@ def admin_upload():
file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
file.save(file_path)
# Extract EXIF data
exif = None
# Extract EXIF data with error handling
exif = {}
exifraw = None
with Image.open(file_path) as img:
exifraw = img.info["exif"]
width, height = img.size
exif = {
ExifTags.TAGS[k]: v
for k, v in img._getexif().items()
if k in ExifTags.TAGS
}
width = height = 0
try:
with Image.open(file_path) as img:
width, height = img.size
if hasattr(img, '_getexif') and img._getexif() is not None:
exifraw = img.info.get("exif")
exif = {
ExifTags.TAGS[k]: v
for k, v in img._getexif().items()
if k in ExifTags.TAGS
}
except Exception as e:
logger.warning(f"Error reading EXIF data for {filename}: {str(e)}")
# Generate a unique key for the image
unique_key = hashlib.sha256(
@ -474,25 +495,25 @@ def admin_upload():
# Generate thumbnails
generate_thumbnails(filename)
# Get image dimensions
with Image.open(file_path) as img:
width, height = img.size
# Handle exposure time with error handling
try:
exposure_time = exif.get("ExposureTime", 0)
if isinstance(exposure_time, tuple):
exposure_fraction = f"{exposure_time[0]}/{exposure_time[1]}"
else:
exposure_fraction = f"1/{int(1/float(exposure_time))}" if exposure_time else "0"
except (TypeError, ZeroDivisionError):
exposure_fraction = "0"
exposure_time = exif["ExposureTime"]
if isinstance(exposure_time, tuple):
exposure_fraction = f"{exposure_time[0]}/{exposure_time[1]}"
else:
exposure_fraction = f"1/{int(1/float(exposure_time))}"
# Create database entry
# Create database entry with safe defaults
db_session = DBSession()
new_photo = Photo(
input_filename=filename,
thumbnail_filename=f"{os.path.splitext(filename)[0]}/256_{filename}",
focal_length=str(
exif.get("FocalLengthIn35mmFilm", exif.get("FocalLength", ""))
exif.get("FocalLengthIn35mmFilm", exif.get("FocalLength", "0"))
),
aperture=str(exif.get("FNumber", "")),
aperture=str(exif.get("FNumber", "0")),
shutter_speed=exposure_fraction,
date_taken=datetime.strptime(
str(exif.get("DateTime", "1970:01:01 00:00:00")), "%Y:%m:%d %H:%M:%S"

View File

@ -203,8 +203,8 @@
<td class="editable" data-field="iso">{{ photo.iso }}</td>
<td>{{ photo.width }}x{{ photo.height }}</td>
<td>
<button onclick="saveChanges(this)">Save</button>
<button onclick="deletePhoto(this)" class="delete-btn">Delete</button>
<button id="save-btn">Save</button>
<button class="delete-btn" id="delete-btn">Delete</button>
</td>
</tr>
{% endfor %}
@ -241,8 +241,11 @@
<input type="text" id="about.location" name="about.location" value="{{ config.about.location }}">
</div>
<div class="form-group">
<label for="about.profile_image">Profile Image Path:</label>
<input type="text" id="about.profile_image" name="about.profile_image" value="{{ config.about.profile_image }}">
<label for="about.profile_image">Profile Image:</label>
<div style="display: flex; align-items: center; gap: 1rem;">
<img id="profile-preview" src="/static/profile.jpeg" alt="Profile" style="width: 100px; height: 100px; object-fit: cover; border-radius: 50%;">
<input type="file" id="profile_image_upload" accept="image/jpeg,image/png" style="flex: 1;">
</div>
</div>
<div class="form-group">
<label for="about.bio">Bio (Markdown):</label>
@ -258,7 +261,7 @@
</div>
</div>
</div>
<script nonce="{{ g.csp_nonce }}">
<script nonce="{{ nonce }}">
function makeEditable(element) {
const value = element.textContent;
const input = document.createElement('input');
@ -335,11 +338,38 @@
}
}
document.getElementById('delete-btn').addEventListener('click', deletePhoto);
document.getElementById('save-btn').addEventListener('click', saveChanges);
document.getElementById('profile_image_upload').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('profile_image', file);
try {
const response = await fetch('/admin/upload_profile', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
document.getElementById('profile-preview').src = '/static/profile.jpeg?' + new Date().getTime();
} else {
alert('Error uploading profile image: ' + result.error);
}
} catch (error) {
alert('Error uploading profile image: ' + error);
}
});
document.getElementById('configForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {};
const inputs = e.target.querySelectorAll('input, textarea');
const inputs = e.target.querySelectorAll('input:not([type="file"]), textarea');
inputs.forEach(input => {
formData[input.name] = input.value;