feat: Add delete button for uploaded CSV files in admin panel
- Added delete button (trash icon) next to each uploaded file in the import section
- Implemented DELETE endpoint at /admin/delete-file/{filename} with authentication and validation
- Added JavaScript function to handle file deletion with confirmation dialog
- Includes security checks for directory traversal and file existence
- Logs file deletion actions with username for audit trail
- UI automatically refreshes after successful deletion
This commit is contained in:
55
app/main.py
55
app/main.py
@@ -1803,6 +1803,61 @@ async def admin_sync_data(
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/admin/delete-file/{filename}")
|
||||||
|
async def admin_delete_file(
|
||||||
|
request: Request,
|
||||||
|
filename: str,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete an uploaded CSV file from the data-import directory.
|
||||||
|
|
||||||
|
Requires authentication and validates that the file exists.
|
||||||
|
"""
|
||||||
|
# Check authentication
|
||||||
|
user = get_current_user_from_session(request.session)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
# Validate filename to prevent directory traversal
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
# Construct file path
|
||||||
|
import_dir = "data-import"
|
||||||
|
file_path = os.path.join(import_dir, filename)
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
# Check if it's actually a file (not a directory)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
raise HTTPException(status_code=400, detail="Not a file")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Delete the file
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
# Log the deletion
|
||||||
|
logger.info(
|
||||||
|
"admin_delete_file",
|
||||||
|
filename=filename,
|
||||||
|
username=user.username,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": True, "message": f"File '{filename}' deleted successfully"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"admin_delete_file_error",
|
||||||
|
filename=filename,
|
||||||
|
username=user.username,
|
||||||
|
error=str(e)
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error deleting file: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/admin")
|
@app.get("/admin")
|
||||||
async def admin_panel(request: Request, db: Session = Depends(get_db)):
|
async def admin_panel(request: Request, db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -475,13 +475,18 @@
|
|||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for file in files %}
|
{% for file in files %}
|
||||||
<label class="list-group-item d-flex justify-content-between align-items-center">
|
<label class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div class="flex-grow-1">
|
||||||
<input class="form-check-input me-2 file-checkbox" type="checkbox"
|
<input class="form-check-input me-2 file-checkbox" type="checkbox"
|
||||||
name="selected_files" value="{{ file.filename }}" id="{{ file.filename }}">
|
name="selected_files" value="{{ file.filename }}" id="{{ file.filename }}">
|
||||||
<small class="text-muted">{{ file.filename }}</small>
|
<small class="text-muted">{{ file.filename }}</small>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">{{ file.size }} bytes • {{ file.modified.strftime('%Y-%m-%d %H:%M') }}</small>
|
<small class="text-muted">{{ file.size }} bytes • {{ file.modified.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger delete-file-btn"
|
||||||
|
data-filename="{{ file.filename }}"
|
||||||
|
onclick="deleteFile('{{ file.filename }}', event)">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -747,5 +752,36 @@ function confirmSync() {
|
|||||||
document.getElementById('syncForm').submit();
|
document.getElementById('syncForm').submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete file function
|
||||||
|
async function deleteFile(filename, event) {
|
||||||
|
// Prevent label click from triggering checkbox
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to delete "${filename}"?\n\nThis action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/admin/delete-file/${encodeURIComponent(filename)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Reload the page to refresh the file list
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`Error deleting file: ${error.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting file:', error);
|
||||||
|
alert(`Error deleting file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user