From cee341c34c82b4f758873fbd0d3f00f93e1c585b Mon Sep 17 00:00:00 2001 From: Zeecka Date: Fri, 9 Jan 2026 17:35:46 +0100 Subject: [PATCH] Password removal working --- aperisolve/app.py | 55 +++++++++++ aperisolve/static/css/aperisolve.css | 17 ++++ aperisolve/static/js/aperisolve.js | 134 +++++++++++++++++++++++++-- 3 files changed, 200 insertions(+), 6 deletions(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index 0bc4fba..92852ef 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -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/", 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/") @app.route("/image//") def get_image(hash_val: Optional[str] = None, img_name: Optional[str] = None) -> Response: diff --git a/aperisolve/static/css/aperisolve.css b/aperisolve/static/css/aperisolve.css index d46d0c1..ccee6e4 100644 --- a/aperisolve/static/css/aperisolve.css +++ b/aperisolve/static/css/aperisolve.css @@ -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, diff --git a/aperisolve/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js index 5623d43..af973c7 100644 --- a/aperisolve/static/js/aperisolve.js +++ b/aperisolve/static/js/aperisolve.js @@ -224,25 +224,147 @@ async function fetchImageInfo(submission_hash) { infoData.size )}`; tableInfos.innerHTML += ` Upload count:${infoData.upload_count}`; - if (Array.isArray(infoData.passwords)) { - const passwordList = infoData.passwords - .map((pwd) => `${escapeHtml(pwd)}`) - .join(", "); - tableInfos.innerHTML += ` Common password(s):${passwordList}`; + 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 = ' 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 = `${escapeHtml(pwd)}`; + + // 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 = "";