Update app.py
Browse files
app.py
CHANGED
@@ -6,68 +6,525 @@ import uuid
|
|
6 |
import os
|
7 |
import io
|
8 |
import base64
|
9 |
-
from PIL import Image
|
10 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
app = Flask(__name__)
|
13 |
CORS(app)
|
14 |
|
15 |
-
# In-memory
|
16 |
-
SECRETS = {}
|
17 |
-
|
|
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
if
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
sid = str(uuid.uuid4())
|
42 |
-
SECRETS[sid] = {
|
43 |
-
"data": data,
|
44 |
-
"image": image_data,
|
45 |
-
"expire_at": time.time() + ttl,
|
46 |
-
"view_once": view_once
|
47 |
-
}
|
48 |
-
return jsonify({"id": sid})
|
49 |
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
if not secret:
|
54 |
-
return jsonify({"error": "Not found"}), 404
|
55 |
-
if time.time() > secret["expire_at"]:
|
56 |
-
del SECRETS[sid]
|
57 |
-
return jsonify({"error": "Expired"}), 410
|
58 |
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
62 |
|
63 |
-
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
|
68 |
@app.route("/")
|
69 |
def index():
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
72 |
if __name__ == "__main__":
|
73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
import os
|
7 |
import io
|
8 |
import base64
|
|
|
9 |
import time
|
10 |
+
import json
|
11 |
+
import hashlib
|
12 |
+
import qrcode
|
13 |
+
from PIL import Image
|
14 |
+
import requests
|
15 |
+
import random
|
16 |
+
import string
|
17 |
|
18 |
app = Flask(__name__)
|
19 |
CORS(app)
|
20 |
|
21 |
+
# In-memory storage
|
22 |
+
SECRETS = {} # { id: { data, file_data, file_type, expire_at, view_once, theme, analytics, etc. } }
|
23 |
+
SHORT_LINKS = {} # { short_id: full_id }
|
24 |
+
ANALYTICS = {} # { secret_id: [analytics_entries] }
|
25 |
|
26 |
+
# Configuration
|
27 |
+
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
28 |
+
ALLOWED_EXTENSIONS = {
|
29 |
+
'image': ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],
|
30 |
+
'video': ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'],
|
31 |
+
'audio': ['mp3', 'wav', 'ogg', 'aac', 'm4a'],
|
32 |
+
'document': ['pdf', 'txt', 'doc', 'docx', 'rtf', 'odt']
|
33 |
+
}
|
34 |
+
|
35 |
+
def get_file_type(filename):
|
36 |
+
"""Determine file type based on extension"""
|
37 |
+
if not filename:
|
38 |
+
return 'unknown'
|
39 |
+
|
40 |
+
ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
41 |
+
|
42 |
+
for file_type, extensions in ALLOWED_EXTENSIONS.items():
|
43 |
+
if ext in extensions:
|
44 |
+
return file_type
|
45 |
+
|
46 |
+
return 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
+
def generate_short_id():
|
49 |
+
"""Generate a short, unique ID"""
|
50 |
+
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
+
def get_client_ip(request):
|
53 |
+
"""Get client IP address"""
|
54 |
+
if request.headers.get('X-Forwarded-For'):
|
55 |
+
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
56 |
+
elif request.headers.get('X-Real-IP'):
|
57 |
+
return request.headers.get('X-Real-IP')
|
58 |
+
else:
|
59 |
+
return request.remote_addr
|
60 |
|
61 |
+
def get_location_info(ip):
|
62 |
+
"""Get location information from IP (mock implementation)"""
|
63 |
+
# In production, use a real geolocation service like ipapi.co, ipstack.com, etc.
|
64 |
+
try:
|
65 |
+
# Mock data - replace with real API call
|
66 |
+
if ip == '127.0.0.1' or ip.startswith('192.168.'):
|
67 |
+
return {
|
68 |
+
'country': 'Local',
|
69 |
+
'city': 'Local',
|
70 |
+
'region': 'Local',
|
71 |
+
'timezone': 'Local'
|
72 |
+
}
|
73 |
+
|
74 |
+
# Example with ipapi.co (uncomment for production)
|
75 |
+
# response = requests.get(f'https://ipapi.co/{ip}/json/', timeout=5)
|
76 |
+
# if response.status_code == 200:
|
77 |
+
# data = response.json()
|
78 |
+
# return {
|
79 |
+
# 'country': data.get('country_name', 'Unknown'),
|
80 |
+
# 'city': data.get('city', 'Unknown'),
|
81 |
+
# 'region': data.get('region', 'Unknown'),
|
82 |
+
# 'timezone': data.get('timezone', 'Unknown')
|
83 |
+
# }
|
84 |
+
|
85 |
+
return {
|
86 |
+
'country': 'Unknown',
|
87 |
+
'city': 'Unknown',
|
88 |
+
'region': 'Unknown',
|
89 |
+
'timezone': 'Unknown'
|
90 |
+
}
|
91 |
+
except:
|
92 |
+
return {
|
93 |
+
'country': 'Unknown',
|
94 |
+
'city': 'Unknown',
|
95 |
+
'region': 'Unknown',
|
96 |
+
'timezone': 'Unknown'
|
97 |
+
}
|
98 |
|
99 |
+
def generate_qr_code(data):
|
100 |
+
"""Generate QR code for the given data"""
|
101 |
+
qr = qrcode.QRCode(
|
102 |
+
version=1,
|
103 |
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
104 |
+
box_size=10,
|
105 |
+
border=4,
|
106 |
+
)
|
107 |
+
qr.add_data(data)
|
108 |
+
qr.make(fit=True)
|
109 |
+
|
110 |
+
img = qr.make_image(fill_color="black", back_color="white")
|
111 |
+
|
112 |
+
# Convert to base64
|
113 |
+
buffer = io.BytesIO()
|
114 |
+
img.save(buffer, format='PNG')
|
115 |
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
116 |
+
|
117 |
+
return f"data:image/png;base64,{img_str}"
|
118 |
+
|
119 |
+
def record_access(secret_id, request):
|
120 |
+
"""Record access analytics"""
|
121 |
+
ip = get_client_ip(request)
|
122 |
+
location = get_location_info(ip)
|
123 |
+
user_agent = request.headers.get('User-Agent', '')
|
124 |
+
|
125 |
+
# Determine device type
|
126 |
+
device_type = 'desktop'
|
127 |
+
if any(mobile in user_agent.lower() for mobile in ['mobile', 'android', 'iphone', 'ipad']):
|
128 |
+
device_type = 'mobile'
|
129 |
+
|
130 |
+
analytics_entry = {
|
131 |
+
'timestamp': time.time(),
|
132 |
+
'ip': ip,
|
133 |
+
'location': location,
|
134 |
+
'user_agent': user_agent,
|
135 |
+
'device_type': device_type,
|
136 |
+
'referer': request.headers.get('Referer', ''),
|
137 |
+
'accept_language': request.headers.get('Accept-Language', '')
|
138 |
+
}
|
139 |
+
|
140 |
+
if secret_id not in ANALYTICS:
|
141 |
+
ANALYTICS[secret_id] = []
|
142 |
+
|
143 |
+
ANALYTICS[secret_id].append(analytics_entry)
|
144 |
+
|
145 |
+
return analytics_entry
|
146 |
|
147 |
@app.route("/")
|
148 |
def index():
|
149 |
+
"""Health check endpoint"""
|
150 |
+
return jsonify({
|
151 |
+
"status": "running",
|
152 |
+
"service": "Sharelock Backend",
|
153 |
+
"version": "2.0.0",
|
154 |
+
"features": [
|
155 |
+
"End-to-end encryption",
|
156 |
+
"File uploads (5MB max)",
|
157 |
+
"QR code generation",
|
158 |
+
"Analytics tracking",
|
159 |
+
"Short URLs",
|
160 |
+
"Self-destruct messages"
|
161 |
+
]
|
162 |
+
})
|
163 |
+
|
164 |
+
@app.route("/api/store", methods=["POST"])
|
165 |
+
def store():
|
166 |
+
"""Store encrypted secret with enhanced features"""
|
167 |
+
try:
|
168 |
+
form = request.form
|
169 |
+
data = form.get("data")
|
170 |
+
|
171 |
+
if not data:
|
172 |
+
return jsonify({"error": "Data is required"}), 400
|
173 |
+
|
174 |
+
# Parse parameters
|
175 |
+
ttl = int(form.get("ttl", 300))
|
176 |
+
view_once = form.get("view_once", "false").lower() == "true"
|
177 |
+
delay_seconds = int(form.get("delay_seconds", 0))
|
178 |
+
theme = form.get("theme", "default")
|
179 |
+
password_hint = form.get("password_hint", "")
|
180 |
+
|
181 |
+
# Handle file upload
|
182 |
+
file_data = None
|
183 |
+
file_type = None
|
184 |
+
file_name = None
|
185 |
+
|
186 |
+
if 'file' in request.files:
|
187 |
+
file = request.files['file']
|
188 |
+
if file and file.filename:
|
189 |
+
# Check file size
|
190 |
+
file.seek(0, os.SEEK_END)
|
191 |
+
file_size = file.tell()
|
192 |
+
file.seek(0)
|
193 |
+
|
194 |
+
if file_size > MAX_FILE_SIZE:
|
195 |
+
return jsonify({"error": f"File too large. Max size: {MAX_FILE_SIZE/1024/1024:.1f}MB"}), 400
|
196 |
+
|
197 |
+
# Process file
|
198 |
+
file_name = secure_filename(file.filename)
|
199 |
+
file_type = get_file_type(file_name)
|
200 |
+
|
201 |
+
if file_type == 'unknown':
|
202 |
+
return jsonify({"error": "File type not supported"}), 400
|
203 |
+
|
204 |
+
# Read and encode file
|
205 |
+
file_content = file.read()
|
206 |
+
file_data = base64.b64encode(file_content).decode('utf-8')
|
207 |
+
|
208 |
+
# Generate IDs
|
209 |
+
secret_id = str(uuid.uuid4())
|
210 |
+
short_id = generate_short_id()
|
211 |
+
|
212 |
+
# Ensure short_id is unique
|
213 |
+
while short_id in SHORT_LINKS:
|
214 |
+
short_id = generate_short_id()
|
215 |
+
|
216 |
+
# Store secret
|
217 |
+
SECRETS[secret_id] = {
|
218 |
+
"data": data,
|
219 |
+
"file_data": file_data,
|
220 |
+
"file_type": file_type,
|
221 |
+
"file_name": file_name,
|
222 |
+
"expire_at": time.time() + ttl,
|
223 |
+
"view_once": view_once,
|
224 |
+
"delay_seconds": delay_seconds,
|
225 |
+
"theme": theme,
|
226 |
+
"password_hint": password_hint,
|
227 |
+
"created_at": time.time(),
|
228 |
+
"creator_ip": get_client_ip(request),
|
229 |
+
"access_count": 0
|
230 |
+
}
|
231 |
+
|
232 |
+
# Store short link mapping
|
233 |
+
SHORT_LINKS[short_id] = secret_id
|
234 |
+
|
235 |
+
# Generate QR code
|
236 |
+
base_url = request.host_url.rstrip('/')
|
237 |
+
secret_url = f"{base_url}/tools/sharelock?id={secret_id}"
|
238 |
+
qr_code = generate_qr_code(secret_url)
|
239 |
+
|
240 |
+
return jsonify({
|
241 |
+
"id": secret_id,
|
242 |
+
"short_id": short_id,
|
243 |
+
"short_url": f"{base_url}/s/{short_id}",
|
244 |
+
"qr_code": qr_code,
|
245 |
+
"expires_at": SECRETS[secret_id]["expire_at"],
|
246 |
+
"has_file": file_data is not None
|
247 |
+
})
|
248 |
+
|
249 |
+
except Exception as e:
|
250 |
+
return jsonify({"error": str(e)}), 500
|
251 |
+
|
252 |
+
@app.route("/api/fetch/<secret_id>")
|
253 |
+
def fetch(secret_id):
|
254 |
+
"""Fetch and decrypt secret with analytics"""
|
255 |
+
try:
|
256 |
+
# Check if it's a short link
|
257 |
+
if secret_id in SHORT_LINKS:
|
258 |
+
secret_id = SHORT_LINKS[secret_id]
|
259 |
+
|
260 |
+
secret = SECRETS.get(secret_id)
|
261 |
+
if not secret:
|
262 |
+
return jsonify({"error": "Secret not found"}), 404
|
263 |
+
|
264 |
+
# Check expiration
|
265 |
+
if time.time() > secret["expire_at"]:
|
266 |
+
# Clean up expired secret
|
267 |
+
if secret_id in SECRETS:
|
268 |
+
del SECRETS[secret_id]
|
269 |
+
# Clean up short link
|
270 |
+
for short_id, full_id in list(SHORT_LINKS.items()):
|
271 |
+
if full_id == secret_id:
|
272 |
+
del SHORT_LINKS[short_id]
|
273 |
+
return jsonify({"error": "Secret has expired"}), 410
|
274 |
+
|
275 |
+
# Record access analytics
|
276 |
+
analytics_entry = record_access(secret_id, request)
|
277 |
+
|
278 |
+
# Increment access count
|
279 |
+
secret["access_count"] += 1
|
280 |
+
|
281 |
+
# Prepare response
|
282 |
+
response = {
|
283 |
+
"data": secret["data"],
|
284 |
+
"theme": secret.get("theme", "default"),
|
285 |
+
"delay_seconds": secret.get("delay_seconds", 0),
|
286 |
+
"password_hint": secret.get("password_hint", ""),
|
287 |
+
"access_count": secret["access_count"]
|
288 |
+
}
|
289 |
+
|
290 |
+
# Include file data if present
|
291 |
+
if secret.get("file_data"):
|
292 |
+
response["file_data"] = secret["file_data"]
|
293 |
+
response["file_type"] = secret.get("file_type", "unknown")
|
294 |
+
response["file_name"] = secret.get("file_name", "unknown")
|
295 |
+
|
296 |
+
# Handle view-once deletion
|
297 |
+
if secret["view_once"]:
|
298 |
+
# Schedule deletion after delay
|
299 |
+
delay = secret.get("delay_seconds", 0)
|
300 |
+
if delay > 0:
|
301 |
+
# In a production environment, you'd use a task queue like Celery
|
302 |
+
# For now, we'll delete immediately after response
|
303 |
+
pass
|
304 |
+
|
305 |
+
# Delete the secret
|
306 |
+
del SECRETS[secret_id]
|
307 |
+
|
308 |
+
# Clean up short link
|
309 |
+
for short_id, full_id in list(SHORT_LINKS.items()):
|
310 |
+
if full_id == secret_id:
|
311 |
+
del SHORT_LINKS[short_id]
|
312 |
+
break
|
313 |
+
|
314 |
+
return jsonify(response)
|
315 |
+
|
316 |
+
except Exception as e:
|
317 |
+
return jsonify({"error": str(e)}), 500
|
318 |
+
|
319 |
+
@app.route("/api/analytics/<secret_id>")
|
320 |
+
def get_analytics(secret_id):
|
321 |
+
"""Get analytics for a specific secret"""
|
322 |
+
try:
|
323 |
+
# Verify secret exists or existed
|
324 |
+
if secret_id not in SECRETS and secret_id not in ANALYTICS:
|
325 |
+
return jsonify({"error": "Secret not found"}), 404
|
326 |
+
|
327 |
+
analytics_data = ANALYTICS.get(secret_id, [])
|
328 |
+
|
329 |
+
# Format analytics for frontend
|
330 |
+
formatted_analytics = []
|
331 |
+
for entry in analytics_data:
|
332 |
+
formatted_analytics.append({
|
333 |
+
"timestamp": entry["timestamp"],
|
334 |
+
"datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["timestamp"])),
|
335 |
+
"ip": entry["ip"],
|
336 |
+
"location": entry["location"],
|
337 |
+
"device_type": entry["device_type"],
|
338 |
+
"user_agent": entry["user_agent"][:100] + "..." if len(entry["user_agent"]) > 100 else entry["user_agent"]
|
339 |
+
})
|
340 |
+
|
341 |
+
return jsonify({
|
342 |
+
"secret_id": secret_id,
|
343 |
+
"total_accesses": len(formatted_analytics),
|
344 |
+
"analytics": formatted_analytics
|
345 |
+
})
|
346 |
+
|
347 |
+
except Exception as e:
|
348 |
+
return jsonify({"error": str(e)}), 500
|
349 |
+
|
350 |
+
@app.route("/api/secrets")
|
351 |
+
def list_secrets():
|
352 |
+
"""List all active secrets (for dashboard)"""
|
353 |
+
try:
|
354 |
+
current_time = time.time()
|
355 |
+
active_secrets = []
|
356 |
+
|
357 |
+
for secret_id, secret in SECRETS.items():
|
358 |
+
if current_time <= secret["expire_at"]:
|
359 |
+
# Find short link
|
360 |
+
short_id = None
|
361 |
+
for s_id, full_id in SHORT_LINKS.items():
|
362 |
+
if full_id == secret_id:
|
363 |
+
short_id = s_id
|
364 |
+
break
|
365 |
+
|
366 |
+
active_secrets.append({
|
367 |
+
"id": secret_id,
|
368 |
+
"short_id": short_id,
|
369 |
+
"created_at": secret["created_at"],
|
370 |
+
"expires_at": secret["expire_at"],
|
371 |
+
"view_once": secret["view_once"],
|
372 |
+
"has_file": secret.get("file_data") is not None,
|
373 |
+
"file_type": secret.get("file_type"),
|
374 |
+
"theme": secret.get("theme", "default"),
|
375 |
+
"access_count": secret.get("access_count", 0),
|
376 |
+
"preview": secret["data"][:100] + "..." if len(secret["data"]) > 100 else secret["data"]
|
377 |
+
})
|
378 |
+
|
379 |
+
return jsonify({
|
380 |
+
"secrets": active_secrets,
|
381 |
+
"total": len(active_secrets)
|
382 |
+
})
|
383 |
+
|
384 |
+
except Exception as e:
|
385 |
+
return jsonify({"error": str(e)}), 500
|
386 |
+
|
387 |
+
@app.route("/api/delete/<secret_id>", methods=["DELETE"])
|
388 |
+
def delete_secret(secret_id):
|
389 |
+
"""Manually delete a secret"""
|
390 |
+
try:
|
391 |
+
if secret_id not in SECRETS:
|
392 |
+
return jsonify({"error": "Secret not found"}), 404
|
393 |
+
|
394 |
+
# Delete secret
|
395 |
+
del SECRETS[secret_id]
|
396 |
+
|
397 |
+
# Clean up short link
|
398 |
+
for short_id, full_id in list(SHORT_LINKS.items()):
|
399 |
+
if full_id == secret_id:
|
400 |
+
del SHORT_LINKS[short_id]
|
401 |
+
break
|
402 |
+
|
403 |
+
return jsonify({"message": "Secret deleted successfully"})
|
404 |
+
|
405 |
+
except Exception as e:
|
406 |
+
return jsonify({"error": str(e)}), 500
|
407 |
+
|
408 |
+
@app.route("/s/<short_id>")
|
409 |
+
def redirect_short_link(short_id):
|
410 |
+
"""Redirect short link to full URL"""
|
411 |
+
if short_id not in SHORT_LINKS:
|
412 |
+
return jsonify({"error": "Short link not found"}), 404
|
413 |
+
|
414 |
+
secret_id = SHORT_LINKS[short_id]
|
415 |
+
base_url = request.host_url.rstrip('/')
|
416 |
+
return f"""
|
417 |
+
<!DOCTYPE html>
|
418 |
+
<html>
|
419 |
+
<head>
|
420 |
+
<title>Sharelock - Redirecting...</title>
|
421 |
+
<meta http-equiv="refresh" content="0;url={base_url}/tools/sharelock?id={secret_id}">
|
422 |
+
</head>
|
423 |
+
<body>
|
424 |
+
<p>Redirecting to secure message...</p>
|
425 |
+
<p>If you are not redirected automatically, <a href="{base_url}/tools/sharelock?id={secret_id}">click here</a>.</p>
|
426 |
+
</body>
|
427 |
+
</html>
|
428 |
+
"""
|
429 |
+
|
430 |
+
@app.route("/api/qr/<secret_id>")
|
431 |
+
def get_qr_code(secret_id):
|
432 |
+
"""Generate QR code for a secret"""
|
433 |
+
try:
|
434 |
+
if secret_id not in SECRETS:
|
435 |
+
return jsonify({"error": "Secret not found"}), 404
|
436 |
+
|
437 |
+
base_url = request.host_url.rstrip('/')
|
438 |
+
secret_url = f"{base_url}/tools/sharelock?id={secret_id}"
|
439 |
+
qr_code = generate_qr_code(secret_url)
|
440 |
+
|
441 |
+
return jsonify({"qr_code": qr_code})
|
442 |
+
|
443 |
+
except Exception as e:
|
444 |
+
return jsonify({"error": str(e)}), 500
|
445 |
+
|
446 |
+
@app.route("/api/stats")
|
447 |
+
def get_stats():
|
448 |
+
"""Get overall statistics"""
|
449 |
+
try:
|
450 |
+
total_secrets = len(SECRETS)
|
451 |
+
total_accesses = sum(len(analytics) for analytics in ANALYTICS.values())
|
452 |
+
|
453 |
+
# Count by file type
|
454 |
+
file_types = {}
|
455 |
+
for secret in SECRETS.values():
|
456 |
+
file_type = secret.get("file_type", "text")
|
457 |
+
file_types[file_type] = file_types.get(file_type, 0) + 1
|
458 |
+
|
459 |
+
# Count by theme
|
460 |
+
themes = {}
|
461 |
+
for secret in SECRETS.values():
|
462 |
+
theme = secret.get("theme", "default")
|
463 |
+
themes[theme] = themes.get(theme, 0) + 1
|
464 |
+
|
465 |
+
return jsonify({
|
466 |
+
"total_secrets": total_secrets,
|
467 |
+
"total_accesses": total_accesses,
|
468 |
+
"file_types": file_types,
|
469 |
+
"themes": themes,
|
470 |
+
"active_short_links": len(SHORT_LINKS)
|
471 |
+
})
|
472 |
+
|
473 |
+
except Exception as e:
|
474 |
+
return jsonify({"error": str(e)}), 500
|
475 |
+
|
476 |
+
@app.route("/api/cleanup", methods=["POST"])
|
477 |
+
def cleanup_expired():
|
478 |
+
"""Clean up expired secrets"""
|
479 |
+
try:
|
480 |
+
current_time = time.time()
|
481 |
+
expired_count = 0
|
482 |
+
|
483 |
+
# Find expired secrets
|
484 |
+
expired_secrets = []
|
485 |
+
for secret_id, secret in SECRETS.items():
|
486 |
+
if current_time > secret["expire_at"]:
|
487 |
+
expired_secrets.append(secret_id)
|
488 |
+
|
489 |
+
# Delete expired secrets
|
490 |
+
for secret_id in expired_secrets:
|
491 |
+
del SECRETS[secret_id]
|
492 |
+
expired_count += 1
|
493 |
+
|
494 |
+
# Clean up short links
|
495 |
+
for short_id, full_id in list(SHORT_LINKS.items()):
|
496 |
+
if full_id == secret_id:
|
497 |
+
del SHORT_LINKS[short_id]
|
498 |
+
break
|
499 |
+
|
500 |
+
return jsonify({
|
501 |
+
"message": f"Cleaned up {expired_count} expired secrets",
|
502 |
+
"expired_count": expired_count
|
503 |
+
})
|
504 |
+
|
505 |
+
except Exception as e:
|
506 |
+
return jsonify({"error": str(e)}), 500
|
507 |
+
|
508 |
+
# Error handlers
|
509 |
+
@app.errorhandler(404)
|
510 |
+
def not_found(error):
|
511 |
+
return jsonify({"error": "Endpoint not found"}), 404
|
512 |
+
|
513 |
+
@app.errorhandler(500)
|
514 |
+
def internal_error(error):
|
515 |
+
return jsonify({"error": "Internal server error"}), 500
|
516 |
|
517 |
if __name__ == "__main__":
|
518 |
+
print("π Sharelock Backend Starting...")
|
519 |
+
print("π Features enabled:")
|
520 |
+
print(" β
End-to-end encryption")
|
521 |
+
print(" β
File uploads (5MB max)")
|
522 |
+
print(" β
QR code generation")
|
523 |
+
print(" β
Analytics tracking")
|
524 |
+
print(" β
Short URLs")
|
525 |
+
print(" β
Self-destruct messages")
|
526 |
+
print(" β
Multiple themes")
|
527 |
+
print(" β
Password hints")
|
528 |
+
print("π Server running on http://0.0.0.0:7860")
|
529 |
+
|
530 |
+
app.run(host="0.0.0.0", port=7860, debug=True)
|