Password removal working
This commit is contained in:
@@ -225,6 +225,7 @@ def create_app() -> Flask:
|
||||
"upload_count": image.upload_count,
|
||||
"passwords": passwords,
|
||||
"removal_min_age_seconds": REMOVAL_MIN_AGE_SECONDS,
|
||||
"submission_date": submission.date,
|
||||
}
|
||||
) # type: ignore
|
||||
|
||||
@@ -345,6 +346,60 @@ def create_app() -> Flask:
|
||||
|
||||
return jsonify({"message": "Image successfully removed"}), 200
|
||||
|
||||
@app.route("/remove_password/<hash_val>", methods=["POST"])
|
||||
def remove_password(hash_val: str) -> tuple[Response, int]:
|
||||
"""Remove a password from a submission if criteria are met."""
|
||||
|
||||
submission = Submission.query.get_or_404(hash_val) # type: ignore
|
||||
|
||||
if not submission.password:
|
||||
return jsonify({"error": "No password to remove"}), 400
|
||||
|
||||
# Check if submission is at least 5 minutes old
|
||||
current_time = time.time()
|
||||
age_seconds = current_time - submission.date
|
||||
if age_seconds < REMOVAL_MIN_AGE_SECONDS:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"error": f"Submission must be at least 5 minutes old. "
|
||||
f"Current age: {int(age_seconds)}s"
|
||||
}
|
||||
),
|
||||
403,
|
||||
)
|
||||
|
||||
# Get all IPs that uploaded this specific submission (password combination)
|
||||
upload_logs = UploadLog.query.filter_by(submission_hash=submission.hash).all()
|
||||
unique_ips = set()
|
||||
for log in upload_logs:
|
||||
if log.ip_address:
|
||||
unique_ips.add(log.ip_address)
|
||||
|
||||
# Check if only one IP has uploaded this submission
|
||||
if len(unique_ips) > 1:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"error": "Submission uploaded from multiple IP addresses. "
|
||||
"Password removal is not allowed.",
|
||||
"ip_count": len(unique_ips),
|
||||
}
|
||||
),
|
||||
403,
|
||||
)
|
||||
|
||||
# Remove the password
|
||||
try:
|
||||
submission.password = None
|
||||
db.session.commit() # pylint: disable=no-member
|
||||
except Exception as e:
|
||||
db.session.rollback() # pylint: disable=no-member
|
||||
sentry_sdk.capture_exception(e)
|
||||
return jsonify({"error": "Failed to remove password"}), 500
|
||||
|
||||
return jsonify({"message": "Password successfully removed"}), 200
|
||||
|
||||
@app.route("/image/<img_name>")
|
||||
@app.route("/image/<hash_val>/<img_name>")
|
||||
def get_image(hash_val: Optional[str] = None, img_name: Optional[str] = None) -> Response:
|
||||
|
||||
@@ -191,6 +191,23 @@ form {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Password removal styling */
|
||||
.password-item {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-delete-icon {
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.password-delete-icon:hover {
|
||||
color: #ff0000 !important;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* ======== Forms & Inputs ======== */
|
||||
input,
|
||||
textarea,
|
||||
|
||||
@@ -224,25 +224,147 @@ async function fetchImageInfo(submission_hash) {
|
||||
infoData.size
|
||||
)}</td></tr>`;
|
||||
tableInfos.innerHTML += `<tr><td><i class="fa fa-upload"></i> Upload count:</td><td>${infoData.upload_count}</td></tr>`;
|
||||
if (Array.isArray(infoData.passwords)) {
|
||||
const passwordList = infoData.passwords
|
||||
.map((pwd) => `<code>${escapeHtml(pwd)}</code>`)
|
||||
.join(", ");
|
||||
tableInfos.innerHTML += `<tr><td><i class="fa fa-key"></i> Common password(s):</td><td>${passwordList}</td></tr>`;
|
||||
if (Array.isArray(infoData.passwords) && infoData.passwords.length > 0) {
|
||||
await displayPasswordsWithRemoval(tableInfos, infoData.passwords, submission_hash, infoData);
|
||||
}
|
||||
|
||||
// Fetch removal status and update removal UI
|
||||
await updateRemovalUI(submission_hash, infoData);
|
||||
}
|
||||
|
||||
async function displayPasswordsWithRemoval(tableInfos, passwords, submission_hash, infoData) {
|
||||
const tr = document.createElement("tr");
|
||||
const tdLabel = document.createElement("td");
|
||||
const tdContent = document.createElement("td");
|
||||
|
||||
tdLabel.innerHTML = '<i class="fa fa-key"></i> Common password(s):';
|
||||
tr.appendChild(tdLabel);
|
||||
tr.appendChild(tdContent);
|
||||
tableInfos.appendChild(tr);
|
||||
|
||||
// Clear existing password timer if any
|
||||
if (passwordTimerIntervals[submission_hash]) {
|
||||
clearInterval(passwordTimerIntervals[submission_hash]);
|
||||
delete passwordTimerIntervals[submission_hash];
|
||||
}
|
||||
|
||||
const uploadTime = infoData.submission_date * 1000;
|
||||
const currentTime = new Date().getTime();
|
||||
const ageSeconds = Math.floor((currentTime - uploadTime) / 1000);
|
||||
const REMOVAL_MIN_AGE_SECONDS = infoData.removal_min_age_seconds || 300;
|
||||
|
||||
for (const pwd of passwords) {
|
||||
const passwordSpan = document.createElement("span");
|
||||
passwordSpan.className = "password-item";
|
||||
passwordSpan.innerHTML = `<code>${escapeHtml(pwd)}</code>`;
|
||||
|
||||
// Check if this password can be removed
|
||||
const canRemove = ageSeconds >= REMOVAL_MIN_AGE_SECONDS;
|
||||
|
||||
if (canRemove) {
|
||||
const deleteIcon = document.createElement("i");
|
||||
deleteIcon.className = "fas fa-times password-delete-icon";
|
||||
deleteIcon.title = "Remove password";
|
||||
deleteIcon.dataset.submissionHash = submission_hash;
|
||||
deleteIcon.dataset.password = pwd;
|
||||
deleteIcon.style.cursor = "pointer";
|
||||
deleteIcon.style.marginLeft = "5px";
|
||||
deleteIcon.style.color = "#ff4d4d";
|
||||
|
||||
deleteIcon.addEventListener("click", async function() {
|
||||
await handlePasswordRemoval(submission_hash, deleteIcon);
|
||||
});
|
||||
|
||||
passwordSpan.appendChild(deleteIcon);
|
||||
}
|
||||
|
||||
tdContent.appendChild(passwordSpan);
|
||||
tdContent.appendChild(document.createTextNode(" "));
|
||||
}
|
||||
|
||||
// Set up timer to refresh when passwords become eligible for removal
|
||||
if (ageSeconds < REMOVAL_MIN_AGE_SECONDS) {
|
||||
const remainingSeconds = REMOVAL_MIN_AGE_SECONDS - ageSeconds;
|
||||
passwordTimerIntervals[submission_hash] = setInterval(async () => {
|
||||
const newUploadTime = infoData.submission_date * 1000;
|
||||
const newCurrentTime = new Date().getTime();
|
||||
const newAgeSeconds = Math.floor((newCurrentTime - newUploadTime) / 1000);
|
||||
|
||||
if (newAgeSeconds >= REMOVAL_MIN_AGE_SECONDS) {
|
||||
// Time has elapsed, refresh the password display
|
||||
clearInterval(passwordTimerIntervals[submission_hash]);
|
||||
delete passwordTimerIntervals[submission_hash];
|
||||
|
||||
// Re-fetch info and refresh display
|
||||
const infoResp = await fetch(`/infos/${submission_hash}`);
|
||||
const newInfoData = await infoResp.json();
|
||||
|
||||
// Clear and rebuild password row
|
||||
tr.remove();
|
||||
await displayPasswordsWithRemoval(tableInfos, newInfoData.passwords, submission_hash, newInfoData);
|
||||
}
|
||||
}, Math.min(remainingSeconds * 1000, 1000)); // Check every second or at the exact time
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPasswordRemovalEligibility(submission_hash, password, infoData) {
|
||||
try {
|
||||
// Check age constraint using submission date, not first_submission_date
|
||||
const uploadTime = infoData.submission_date * 1000; // Convert to milliseconds
|
||||
const currentTime = new Date().getTime();
|
||||
const ageSeconds = Math.floor((currentTime - uploadTime) / 1000);
|
||||
const REMOVAL_MIN_AGE_SECONDS = infoData.removal_min_age_seconds || 300;
|
||||
|
||||
if (ageSeconds < REMOVAL_MIN_AGE_SECONDS) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error checking password removal eligibility:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordRemoval(submission_hash, iconElement) {
|
||||
try {
|
||||
iconElement.style.pointerEvents = "none";
|
||||
iconElement.style.opacity = "0.5";
|
||||
|
||||
const response = await fetch(`/remove_password/${submission_hash}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Remove the password item from display
|
||||
const passwordSpan = iconElement.closest(".password-item");
|
||||
if (passwordSpan) {
|
||||
passwordSpan.style.transition = "opacity 0.3s";
|
||||
passwordSpan.style.opacity = "0";
|
||||
setTimeout(() => passwordSpan.remove(), 300);
|
||||
}
|
||||
} else {
|
||||
// Silently fail - just re-enable the icon
|
||||
iconElement.style.pointerEvents = "auto";
|
||||
iconElement.style.opacity = "1";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Password removal error:", error);
|
||||
iconElement.style.pointerEvents = "auto";
|
||||
iconElement.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Image removal functionality
|
||||
*/
|
||||
|
||||
let removalTimerIntervals = {};
|
||||
let passwordTimerIntervals = {};
|
||||
|
||||
async function updateRemovalUI(submission_hash, infoData) {
|
||||
console.log("Updating removal UI...");
|
||||
const resultRemovalDiv = document.getElementById("result-removal");
|
||||
resultRemovalDiv.innerHTML = "";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user