Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid
import json
import time
import datetime
from flask import Flask, render_template, request, jsonify, send_from_directory
from urllib.parse import urlparse
import requests
Expand All @@ -21,7 +22,13 @@
app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
app.config['RESULTS_DIR'] = os.path.join(os.getcwd(), 'scan_results')
app.config['FEEDBACK_FILE'] = os.path.join(os.getcwd(), 'feedback', 'reviews.json')
app.config['DATA_DIR'] = os.path.join(os.getcwd(), 'data')
app.config['FEEDBACK_FILE'] = os.path.join(app.config['DATA_DIR'], 'feedback.json')
app.config['SUBSCRIBERS_FILE'] = os.path.join(app.config['DATA_DIR'], 'subscribers.json')

# Create directories if they don't exist
os.makedirs(app.config['RESULTS_DIR'], exist_ok=True)
os.makedirs(app.config['DATA_DIR'], exist_ok=True)
app.config['SLACK_WEBHOOK_URL'] = os.getenv('SLACK_WEBHOOK_URL', '')

# Cache busting - changes on each deployment/restart
Expand Down Expand Up @@ -317,13 +324,13 @@ def save_results():

return jsonify({'id': result_id})

@app.route('/api/results/<result_id>', methods=['GET'])
def get_results(result_id):
# Basic validation of result_id to prevent path traversal
if not result_id.replace('-', '').isalnum():
return jsonify({'error': 'Invalid result ID'}), 400
@app.route('/api/results/<scan_id>', methods=['GET'])
def get_results(scan_id):
# Security: basic path traversal protection
if '..' in scan_id or '/' in scan_id or '\\' in scan_id or not scan_id.replace('-', '').isalnum():
return jsonify({'error': 'Invalid scan ID'}), 400

file_path = os.path.join(app.config['RESULTS_DIR'], f"{result_id}.json")
file_path = os.path.join(app.config['RESULTS_DIR'], f"{scan_id}.json")

if not os.path.exists(file_path):
return jsonify({'error': 'Results not found'}), 404
Expand Down Expand Up @@ -439,5 +446,47 @@ def submit_feedback():
print(f"Error saving feedback: {str(e)}")
return jsonify({'error': f"Failed to save feedback: {str(e)}"}), 500

@app.route('/api/subscribe', methods=['POST'])
def subscribe_newsletter():
data = request.get_json()
if not data or not data.get('email'):
return jsonify({'error': 'Email is required'}), 400

email = data.get('email').strip()

subscriber_node = {
'id': str(uuid.uuid4()),
'email': email,
'timestamp': datetime.datetime.utcnow().isoformat() + 'Z',
'subscribed_via': 'web-modal'
}

try:
subscribers = []
if os.path.exists(app.config['SUBSCRIBERS_FILE']):
with open(app.config['SUBSCRIBERS_FILE'], 'r') as f:
try:
subscribers = json.load(f)
except json.JSONDecodeError:
subscribers = []

# Check if email already exists
if any(s['email'] == email for s in subscribers):
return jsonify({'message': 'Already subscribed!'}), 200

subscribers.append(subscriber_node)

with open(app.config['SUBSCRIBERS_FILE'], 'w') as f:
json.dump(subscribers, f, indent=4)

# Send Slack notification if configured
if app.config['SLACK_WEBHOOK_URL']:
send_slack_notification(f"✉️ New Newsletter Subscriber: *{email}*")

return jsonify({'message': 'Subscribed successfully'}), 200
except Exception as e:
print(f"Error in subscription: {str(e)}")
return jsonify({'error': 'Failed to complete subscription'}), 500

if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ services:
- CLEANUP_SCANNED_IMAGES=${CLEANUP_SCANNED_IMAGES:-true}
volumes:
- ./static:/opt/infrascan/static
- ./feedback:/opt/infrascan/feedback
- ./data:/opt/infrascan/data
- ./scan_results:/opt/infrascan/scan_results
- /var/run/docker.sock:/var/run/docker.sock # Docker socket for Docker Scout scanning
networks:
Expand Down
67 changes: 67 additions & 0 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ document.addEventListener('DOMContentLoaded', () => {
const feedbackContact = document.getElementById('feedback-contact');
let selectedRating = 0;

// Newsletter Elements
const newsletterModal = document.getElementById('newsletter-modal');
const closeNewsletterBtn = document.getElementById('close-newsletter-btn');
const subscribeBtn = document.getElementById('subscribe-btn');
const newsletterEmail = document.getElementById('newsletter-email');
const newsletterConsent = document.getElementById('newsletter-consent');

let currentResults = null;
let currentSummary = null;
let currentMetadata = null;
Expand Down Expand Up @@ -69,6 +76,13 @@ document.addEventListener('DOMContentLoaded', () => {
checkScannerStatus();
loadSharedResults();

// Trigger Newsletter Modal after 3 seconds if not already closed
if (!localStorage.getItem('newsletter_closed') && !window.location.search.includes('scan_id')) {
setTimeout(() => {
if (newsletterModal) newsletterModal.classList.remove('hidden');
}, 3000);
}

async function checkScannerStatus() {
try {
const response = await fetch('/api/scanner/status');
Expand Down Expand Up @@ -1220,4 +1234,57 @@ document.addEventListener('DOMContentLoaded', () => {
icon.textContent = '▼';
}
}

// Newsletter Event Listeners
if (closeNewsletterBtn) {
closeNewsletterBtn.onclick = () => {
newsletterModal.classList.add('hidden');
localStorage.setItem('newsletter_closed', 'true');
};
}

if (subscribeBtn) {
subscribeBtn.onclick = async () => {
const email = newsletterEmail.value.trim();
const consent = newsletterConsent.checked;

if (!email || !email.includes('@')) {
showToast('Please enter a valid email address');
return;
}

if (!consent) {
showToast('Please agree to the consent terms');
return;
}

subscribeBtn.disabled = true;
subscribeBtn.textContent = 'Subscribing...';

try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: email })
});

const data = await response.json();

if (response.ok) {
showToast(data.message || 'Thank you for subscribing!', 'success');
newsletterModal.classList.add('hidden');
localStorage.setItem('newsletter_closed', 'true');
} else {
throw new Error(data.error || 'Subscription failed');
}
} catch (e) {
showToast(e.message || 'Subscription failed. Please try again.');
} finally {
subscribeBtn.disabled = false;
subscribeBtn.innerHTML = '<span>✉️</span> Subscribe Now';
}
};
}
});
116 changes: 115 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@ header {
/* Back to center for better overall feel */
justify-content: center;
gap: 0.75rem;
margin-bottom: 0.15rem;
margin-bottom: 1.25rem;
}

.github-stars-badge {
display: flex;
align-items: center;
transition: transform 0.2s ease;
margin-left: 0.5rem;
}

.github-stars-badge:hover {
transform: scale(1.05);
}

.soldevelo-brand {
Expand Down Expand Up @@ -1087,6 +1098,17 @@ footer {
margin-bottom: 0.5rem;
}

.github-stars-badge-footer {
display: inline-block;
vertical-align: middle;
margin-left: 0.5rem;
transition: transform 0.2s ease;
}

.github-stars-badge-footer:hover {
transform: scale(1.1) rotate(2deg);
}

footer a {
color: var(--text-muted);
text-decoration: none;
Expand Down Expand Up @@ -1423,6 +1445,98 @@ select option:disabled {
transform: scale(1.1);
}

/* Newsletter Checkbox styling */
.newsletter-form {
margin-top: 1.5rem;
}

.checkbox-group {
margin-top: 1.5rem;
background: rgba(255, 255, 255, 0.03);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}

.checkbox-group:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(99, 102, 241, 0.2);
transform: translateY(-1px);
}

.custom-checkbox {
display: flex;
align-items: flex-start;
gap: 12px;
cursor: pointer;
user-select: none;
color: var(--text-muted);
}

.custom-checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}

.checkmark {
position: relative;
height: 20px;
width: 20px;
min-width: 20px;
background-color: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
flex-shrink: 0;
margin-top: 3px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
}

.custom-checkbox:hover input~.checkmark {
border-color: rgba(99, 102, 241, 0.5);
background-color: rgba(99, 102, 241, 0.05);
}

.custom-checkbox input:checked~.checkmark {
background-color: var(--primary);
border-color: var(--primary);
box-shadow: 0 0 10px rgba(99, 102, 241, 0.4);
}

.checkmark:after {
content: "";
position: absolute;
display: none;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg);
}

.custom-checkbox input:checked~.checkmark:after {
display: block;
}

.checkbox-text {
line-height: 1.5;
font-size: 0.875rem;
color: var(--text-muted);
transition: color 0.2s;
}

.custom-checkbox:hover .checkbox-text {
color: var(--text-main);
}

textarea {
width: 100%;
padding: 0.875rem 1.25rem;
Expand Down
39 changes: 38 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<a href="{{ url_for('index') }}" class="logo-link">
<h1>InfraScan</h1>
</a>
<a href="https://github.com/SolDevelo/InfraScan" target="_blank" class="github-stars-badge">
<img src="https://img.shields.io/github/stars/SolDevelo/InfraScan?style=social" alt="GitHub stars">
</a>
<div class="soldevelo-brand">
<span class="brand-divider"></span>
<a href="https://soldevelo.com?utm_source=infrascan&utm_medium=referral&utm_campaign=app_transition&utm_content=header-logo"
Expand Down Expand Up @@ -352,7 +355,12 @@ <h3>Need help implementing these fixes?</h3>
<p>InfraScan v1.0.3 &copy; 2026 SolDevelo. Advanced Infrastructure Auditor.</p>
<p style="font-size: 0.9rem; opacity: 0.8; margin-top:4px;">This tool is <a
href="https://github.com/SolDevelo/InfraScan" target="_blank"><strong>Open Source</strong></a> –
contributions are welcome!</p>
contributions are welcome!
<a href="https://github.com/SolDevelo/InfraScan" target="_blank" class="github-stars-badge-footer">
<img src="https://img.shields.io/github/stars/SolDevelo/InfraScan?style=social&label=Star"
alt="GitHub stars">
</a>
</p>

<p>Made with &hearts; by <a
href="https://soldevelo.com?utm_source=infrascan&utm_medium=referral&utm_campaign=app_transition&utm_content=footer-logo"
Expand All @@ -374,6 +382,35 @@ <h3>Need help implementing these fixes?</h3>
</button>
</div>

<!-- Newsletter Modal -->
<div id="newsletter-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h2>Stay Updated</h2>
<button class="close-btn" id="close-newsletter-btn">&times;</button>
</div>
<p class="modal-subtitle">Would you like to subscribe to our newsletter?</p>
<div class="newsletter-form">
<div class="input-group">
<label for="newsletter-email">Email Address</label>
<input type="email" id="newsletter-email" placeholder="your@email.com">
</div>

<div class="checkbox-group">
<label class="custom-checkbox">
<input type="checkbox" id="newsletter-consent">
<span class="checkmark"></span>
<span class="checkbox-text">I agree to receive the newsletter and be contacted.</span>
</label>
</div>

<button class="action-btn" id="subscribe-btn">
<span>✉️</span> Subscribe Now
</button>
</div>
</div>
</div>

<!-- Feedback Modal -->
<div id="feedback-modal" class="modal hidden">
<div class="modal-content">
Expand Down