Compare commits

...

7 Commits
v1.0.1 ... main

Author SHA1 Message Date
2ea5131739 Fix multiple buttons
All checks were successful
Docker Build and Publish / build (release) Successful in 6s
2025-02-01 11:23:58 -05:00
c10ac8669c Deletes work, tested on DWS
All checks were successful
Docker Build and Publish / build (release) Successful in 6s
2025-01-31 18:54:04 -05:00
682cdfa95c update listener model 2025-01-31 18:49:44 -05:00
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
2 changed files with 80 additions and 35 deletions

50
app.py
View File

@ -331,6 +331,7 @@ def admin():
photos=photos, photos=photos,
accent_color=config["appearance"]["accent_color"], accent_color=config["appearance"]["accent_color"],
config=config, config=config,
nonce=g.csp_nonce,
) )
@app.route("/admin/logout") @app.route("/admin/logout")
@ -461,17 +462,22 @@ def admin_upload():
file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename) file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
file.save(file_path) file.save(file_path)
# Extract EXIF data # Extract EXIF data with error handling
exif = None exif = {}
exifraw = None exifraw = None
with Image.open(file_path) as img: width = height = 0
exifraw = img.info["exif"] try:
width, height = img.size with Image.open(file_path) as img:
exif = { width, height = img.size
ExifTags.TAGS[k]: v if hasattr(img, '_getexif') and img._getexif() is not None:
for k, v in img._getexif().items() exifraw = img.info.get("exif")
if k in ExifTags.TAGS 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 # Generate a unique key for the image
unique_key = hashlib.sha256( unique_key = hashlib.sha256(
@ -489,25 +495,25 @@ def admin_upload():
# Generate thumbnails # Generate thumbnails
generate_thumbnails(filename) generate_thumbnails(filename)
# Get image dimensions # Handle exposure time with error handling
with Image.open(file_path) as img: try:
width, height = img.size 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"] # Create database entry with safe defaults
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
db_session = DBSession() db_session = DBSession()
new_photo = Photo( new_photo = Photo(
input_filename=filename, input_filename=filename,
thumbnail_filename=f"{os.path.splitext(filename)[0]}/256_{filename}", thumbnail_filename=f"{os.path.splitext(filename)[0]}/256_{filename}",
focal_length=str( 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, shutter_speed=exposure_fraction,
date_taken=datetime.strptime( date_taken=datetime.strptime(
str(exif.get("DateTime", "1970:01:01 00:00:00")), "%Y:%m:%d %H:%M:%S" 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 class="editable" data-field="iso">{{ photo.iso }}</td>
<td>{{ photo.width }}x{{ photo.height }}</td> <td>{{ photo.width }}x{{ photo.height }}</td>
<td> <td>
<button onclick="saveChanges(this)">Save</button> <button class="save-btn">Save</button>
<button onclick="deletePhoto(this)" class="delete-btn">Delete</button> <button class="delete-btn">Delete</button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -241,8 +241,11 @@
<input type="text" id="about.location" name="about.location" value="{{ config.about.location }}"> <input type="text" id="about.location" name="about.location" value="{{ config.about.location }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="about.profile_image">Profile Image Path:</label> <label for="about.profile_image">Profile Image:</label>
<input type="text" id="about.profile_image" name="about.profile_image" value="{{ config.about.profile_image }}"> <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" class="profile-image-upload" accept="image/jpeg,image/png" style="flex: 1;">
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="about.bio">Bio (Markdown):</label> <label for="about.bio">Bio (Markdown):</label>
@ -258,7 +261,7 @@
</div> </div>
</div> </div>
</div> </div>
<script nonce="{{ g.csp_nonce }}"> <script nonce="{{ nonce }}">
function makeEditable(element) { function makeEditable(element) {
const value = element.textContent; const value = element.textContent;
const input = document.createElement('input'); const input = document.createElement('input');
@ -281,7 +284,8 @@
}); });
}); });
function saveChanges(button) { function saveChanges(event) {
const button = event.target;
const row = button.closest('tr'); const row = button.closest('tr');
const photoId = row.dataset.id; const photoId = row.dataset.id;
const updatedData = {}; const updatedData = {};
@ -311,8 +315,9 @@
}); });
} }
function deletePhoto(button) { function deletePhoto(event) {
if (confirm('Are you sure you want to delete this photo?')) { if (confirm('Are you sure you want to delete this photo?')) {
const button = event.target;
const row = button.closest('tr'); const row = button.closest('tr');
const photoId = row.dataset.id; const photoId = row.dataset.id;
@ -335,11 +340,45 @@
} }
} }
document.querySelectorAll('.delete-btn').forEach(button => {
button.addEventListener('click', (event) => deletePhoto(event));
});
document.querySelectorAll('.save-btn').forEach(button => {
button.addEventListener('click', (event) => saveChanges(event));
});
document.querySelectorAll('.profile-image-upload').forEach(input => {
input.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) => { document.getElementById('configForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const formData = {}; const formData = {};
const inputs = e.target.querySelectorAll('input, textarea'); const inputs = e.target.querySelectorAll('input:not([type="file"]), textarea');
inputs.forEach(input => { inputs.forEach(input => {
formData[input.name] = input.value; formData[input.name] = input.value;