Spaces:
Running
Running
David Ko
commited on
Commit
ยท
630e9fb
1
Parent(s):
b4662d6
Redirect-on-expiry: fresh required on /api/status and injected heartbeat in index.html responses to auto-redirect to /login when session expires
Browse files
api.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
import os
|
4 |
os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
|
5 |
|
6 |
-
from flask import Flask, request, jsonify, send_from_directory, redirect, url_for, session, render_template_string
|
7 |
from datetime import timedelta
|
8 |
import torch
|
9 |
from PIL import Image
|
@@ -18,7 +18,16 @@ import time
|
|
18 |
from flask_cors import CORS
|
19 |
import json
|
20 |
import sys
|
21 |
-
from flask_login import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
# Fix for SQLite3 version compatibility with ChromaDB
|
24 |
try:
|
@@ -33,8 +42,8 @@ from chromadb.utils import embedding_functions
|
|
33 |
app = Flask(__name__, static_folder='static')
|
34 |
app.secret_key = 'your_secret_key_here' # ์ธ์
์ํธํ๋ฅผ ์ํ ๋น๋ฐ ํค
|
35 |
app.config['CORS_HEADERS'] = 'Content-Type'
|
36 |
-
# Remember cookie (Flask-Login)
|
37 |
-
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(
|
38 |
app.config['REMEMBER_COOKIE_SECURE'] = True # Spaces uses HTTPS
|
39 |
app.config['REMEMBER_COOKIE_HTTPONLY'] = True
|
40 |
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None'
|
@@ -1208,7 +1217,19 @@ def logout():
|
|
1208 |
session.clear()
|
1209 |
except Exception as e:
|
1210 |
print(f"[DEBUG] Error clearing session on logout: {e}")
|
1211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1212 |
|
1213 |
# ์ ์ ํ์ผ ์๋น์ ์ํ ๋ผ์ฐํธ (๋ก๊ทธ์ธ ๋ถํ์)
|
1214 |
@app.route('/static/<path:filename>')
|
@@ -1224,8 +1245,8 @@ def serve_index_html():
|
|
1224 |
print(f"Request to /index.html - Cookies: {request.cookies}")
|
1225 |
print(f"Request to /index.html - User authenticated: {current_user.is_authenticated}")
|
1226 |
|
1227 |
-
# ์ธ์ฆ ํ์ธ
|
1228 |
-
if not current_user.is_authenticated:
|
1229 |
print("User not authenticated, redirecting to login")
|
1230 |
return redirect(url_for('login'))
|
1231 |
|
@@ -1238,7 +1259,45 @@ def serve_index_html():
|
|
1238 |
session['username'] = current_user.username
|
1239 |
session.modified = True
|
1240 |
|
1241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1242 |
# Prevent sensitive pages from being cached
|
1243 |
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
1244 |
resp.headers['Pragma'] = 'no-cache'
|
@@ -1248,7 +1307,7 @@ def serve_index_html():
|
|
1248 |
# ๊ธฐ๋ณธ ๊ฒฝ๋ก ๋ฐ ๊ธฐํ ๊ฒฝ๋ก ์ฒ๋ฆฌ (๋ก๊ทธ์ธ ํ์)
|
1249 |
@app.route('/', defaults={'path': ''}, methods=['GET'])
|
1250 |
@app.route('/<path:path>', methods=['GET'])
|
1251 |
-
@
|
1252 |
def serve_react(path):
|
1253 |
"""Serve React frontend"""
|
1254 |
print(f"Serving React frontend for path: {path}, user: {current_user.username if current_user.is_authenticated else 'not authenticated'}")
|
@@ -1260,15 +1319,58 @@ def serve_react(path):
|
|
1260 |
resp.headers['Expires'] = '0'
|
1261 |
return resp
|
1262 |
else:
|
1263 |
-
# React ์ฑ์ index.html ์๋น
|
1264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1265 |
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
1266 |
resp.headers['Pragma'] = 'no-cache'
|
1267 |
resp.headers['Expires'] = '0'
|
1268 |
return resp
|
1269 |
|
1270 |
@app.route('/similar-images', methods=['GET'])
|
1271 |
-
@
|
1272 |
def similar_images_page():
|
1273 |
"""Serve similar images search page"""
|
1274 |
resp = send_from_directory(app.static_folder, 'similar-images.html')
|
@@ -1278,7 +1380,7 @@ def similar_images_page():
|
|
1278 |
return resp
|
1279 |
|
1280 |
@app.route('/object-detection-search', methods=['GET'])
|
1281 |
-
@
|
1282 |
def object_detection_search_page():
|
1283 |
"""Serve object detection search page"""
|
1284 |
resp = send_from_directory(app.static_folder, 'object-detection-search.html')
|
@@ -1288,7 +1390,7 @@ def object_detection_search_page():
|
|
1288 |
return resp
|
1289 |
|
1290 |
@app.route('/model-vector-db', methods=['GET'])
|
1291 |
-
@
|
1292 |
def model_vector_db_page():
|
1293 |
"""Serve model vector DB UI page"""
|
1294 |
resp = send_from_directory(app.static_folder, 'model-vector-db.html')
|
@@ -1298,7 +1400,7 @@ def model_vector_db_page():
|
|
1298 |
return resp
|
1299 |
|
1300 |
@app.route('/api/status', methods=['GET'])
|
1301 |
-
@
|
1302 |
def status():
|
1303 |
return jsonify({
|
1304 |
"status": "online",
|
|
|
3 |
import os
|
4 |
os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
|
5 |
|
6 |
+
from flask import Flask, request, jsonify, send_from_directory, redirect, url_for, session, render_template_string, make_response
|
7 |
from datetime import timedelta
|
8 |
import torch
|
9 |
from PIL import Image
|
|
|
18 |
from flask_cors import CORS
|
19 |
import json
|
20 |
import sys
|
21 |
+
from flask_login import (
|
22 |
+
LoginManager,
|
23 |
+
UserMixin,
|
24 |
+
login_user,
|
25 |
+
logout_user,
|
26 |
+
login_required,
|
27 |
+
current_user,
|
28 |
+
fresh_login_required,
|
29 |
+
login_fresh,
|
30 |
+
)
|
31 |
|
32 |
# Fix for SQLite3 version compatibility with ChromaDB
|
33 |
try:
|
|
|
42 |
app = Flask(__name__, static_folder='static')
|
43 |
app.secret_key = 'your_secret_key_here' # ์ธ์
์ํธํ๋ฅผ ์ํ ๋น๋ฐ ํค
|
44 |
app.config['CORS_HEADERS'] = 'Content-Type'
|
45 |
+
# Remember cookie (Flask-Login) โ minimize duration to prevent auto re-login
|
46 |
+
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(seconds=1)
|
47 |
app.config['REMEMBER_COOKIE_SECURE'] = True # Spaces uses HTTPS
|
48 |
app.config['REMEMBER_COOKIE_HTTPONLY'] = True
|
49 |
app.config['REMEMBER_COOKIE_SAMESITE'] = 'None'
|
|
|
1217 |
session.clear()
|
1218 |
except Exception as e:
|
1219 |
print(f"[DEBUG] Error clearing session on logout: {e}")
|
1220 |
+
# Ensure remember cookie is removed by setting an expired cookie
|
1221 |
+
resp = redirect(url_for('login'))
|
1222 |
+
try:
|
1223 |
+
resp.delete_cookie(
|
1224 |
+
key='remember_token',
|
1225 |
+
path='/',
|
1226 |
+
samesite='None',
|
1227 |
+
secure=True,
|
1228 |
+
httponly=True,
|
1229 |
+
)
|
1230 |
+
except Exception as e:
|
1231 |
+
print(f"[DEBUG] Error deleting remember_token cookie: {e}")
|
1232 |
+
return resp
|
1233 |
|
1234 |
# ์ ์ ํ์ผ ์๋น์ ์ํ ๋ผ์ฐํธ (๋ก๊ทธ์ธ ๋ถํ์)
|
1235 |
@app.route('/static/<path:filename>')
|
|
|
1245 |
print(f"Request to /index.html - Cookies: {request.cookies}")
|
1246 |
print(f"Request to /index.html - User authenticated: {current_user.is_authenticated}")
|
1247 |
|
1248 |
+
# ์ธ์ฆ ํ์ธ (fresh session only)
|
1249 |
+
if not current_user.is_authenticated or not login_fresh():
|
1250 |
print("User not authenticated, redirecting to login")
|
1251 |
return redirect(url_for('login'))
|
1252 |
|
|
|
1259 |
session['username'] = current_user.username
|
1260 |
session.modified = True
|
1261 |
|
1262 |
+
# index.html์ ์ฝ์ด ํํธ๋นํธ ์คํฌ๋ฆฝํธ๋ฅผ ์ฃผ์
|
1263 |
+
index_path = os.path.join(app.static_folder, 'index.html')
|
1264 |
+
try:
|
1265 |
+
with open(index_path, 'r', encoding='utf-8') as f:
|
1266 |
+
html = f.read()
|
1267 |
+
except Exception as e:
|
1268 |
+
print(f"[DEBUG] Failed to read index.html for injection: {e}")
|
1269 |
+
return send_from_directory(app.static_folder, 'index.html')
|
1270 |
+
|
1271 |
+
heartbeat_script = """
|
1272 |
+
<script>
|
1273 |
+
(function(){
|
1274 |
+
function checkSession(){
|
1275 |
+
fetch('/api/status', {credentials: 'include'}).then(function(res){
|
1276 |
+
if(res.status !== 200){
|
1277 |
+
window.location.href = '/login';
|
1278 |
+
}
|
1279 |
+
}).catch(function(){
|
1280 |
+
// ๋คํธ์ํฌ ์ค๋ฅ ๋ฑ๋ ๋ก๊ทธ์ธ์ผ๋ก ์ ๋
|
1281 |
+
window.location.href = '/login';
|
1282 |
+
});
|
1283 |
+
}
|
1284 |
+
// ์ฒซ ์ฒดํฌ + ์ฃผ๊ธฐ์ ์ฒดํฌ(30์ด)
|
1285 |
+
checkSession();
|
1286 |
+
setInterval(checkSession, 30000);
|
1287 |
+
})();
|
1288 |
+
</script>
|
1289 |
+
"""
|
1290 |
+
|
1291 |
+
try:
|
1292 |
+
if '</body>' in html:
|
1293 |
+
html = html.replace('</body>', heartbeat_script + '</body>')
|
1294 |
+
else:
|
1295 |
+
html = html + heartbeat_script
|
1296 |
+
except Exception as e:
|
1297 |
+
print(f"[DEBUG] Failed to inject heartbeat script: {e}")
|
1298 |
+
return send_from_directory(app.static_folder, 'index.html')
|
1299 |
+
|
1300 |
+
resp = make_response(html)
|
1301 |
# Prevent sensitive pages from being cached
|
1302 |
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
1303 |
resp.headers['Pragma'] = 'no-cache'
|
|
|
1307 |
# ๊ธฐ๋ณธ ๊ฒฝ๋ก ๋ฐ ๊ธฐํ ๊ฒฝ๋ก ์ฒ๋ฆฌ (๋ก๊ทธ์ธ ํ์)
|
1308 |
@app.route('/', defaults={'path': ''}, methods=['GET'])
|
1309 |
@app.route('/<path:path>', methods=['GET'])
|
1310 |
+
@fresh_login_required
|
1311 |
def serve_react(path):
|
1312 |
"""Serve React frontend"""
|
1313 |
print(f"Serving React frontend for path: {path}, user: {current_user.username if current_user.is_authenticated else 'not authenticated'}")
|
|
|
1319 |
resp.headers['Expires'] = '0'
|
1320 |
return resp
|
1321 |
else:
|
1322 |
+
# React ์ฑ์ index.html ์๋น (ํํธ๋น๏ฟฝ๏ฟฝ ์คํฌ๋ฆฝํธ ์ฃผ์
)
|
1323 |
+
index_path = os.path.join(app.static_folder, 'index.html')
|
1324 |
+
try:
|
1325 |
+
with open(index_path, 'r', encoding='utf-8') as f:
|
1326 |
+
html = f.read()
|
1327 |
+
except Exception as e:
|
1328 |
+
print(f"[DEBUG] Failed to read index.html for injection (serve_react): {e}")
|
1329 |
+
resp = send_from_directory(app.static_folder, 'index.html')
|
1330 |
+
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
1331 |
+
resp.headers['Pragma'] = 'no-cache'
|
1332 |
+
resp.headers['Expires'] = '0'
|
1333 |
+
return resp
|
1334 |
+
|
1335 |
+
heartbeat_script = """
|
1336 |
+
<script>
|
1337 |
+
(function(){
|
1338 |
+
function checkSession(){
|
1339 |
+
fetch('/api/status', {credentials: 'include'}).then(function(res){
|
1340 |
+
if(res.status !== 200){
|
1341 |
+
window.location.href = '/login';
|
1342 |
+
}
|
1343 |
+
}).catch(function(){
|
1344 |
+
window.location.href = '/login';
|
1345 |
+
});
|
1346 |
+
}
|
1347 |
+
checkSession();
|
1348 |
+
setInterval(checkSession, 30000);
|
1349 |
+
})();
|
1350 |
+
</script>
|
1351 |
+
"""
|
1352 |
+
|
1353 |
+
try:
|
1354 |
+
if '</body>' in html:
|
1355 |
+
html = html.replace('</body>', heartbeat_script + '</body>')
|
1356 |
+
else:
|
1357 |
+
html = html + heartbeat_script
|
1358 |
+
except Exception as e:
|
1359 |
+
print(f"[DEBUG] Failed to inject heartbeat script (serve_react): {e}")
|
1360 |
+
resp = send_from_directory(app.static_folder, 'index.html')
|
1361 |
+
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
1362 |
+
resp.headers['Pragma'] = 'no-cache'
|
1363 |
+
resp.headers['Expires'] = '0'
|
1364 |
+
return resp
|
1365 |
+
|
1366 |
+
resp = make_response(html)
|
1367 |
resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
1368 |
resp.headers['Pragma'] = 'no-cache'
|
1369 |
resp.headers['Expires'] = '0'
|
1370 |
return resp
|
1371 |
|
1372 |
@app.route('/similar-images', methods=['GET'])
|
1373 |
+
@fresh_login_required
|
1374 |
def similar_images_page():
|
1375 |
"""Serve similar images search page"""
|
1376 |
resp = send_from_directory(app.static_folder, 'similar-images.html')
|
|
|
1380 |
return resp
|
1381 |
|
1382 |
@app.route('/object-detection-search', methods=['GET'])
|
1383 |
+
@fresh_login_required
|
1384 |
def object_detection_search_page():
|
1385 |
"""Serve object detection search page"""
|
1386 |
resp = send_from_directory(app.static_folder, 'object-detection-search.html')
|
|
|
1390 |
return resp
|
1391 |
|
1392 |
@app.route('/model-vector-db', methods=['GET'])
|
1393 |
+
@fresh_login_required
|
1394 |
def model_vector_db_page():
|
1395 |
"""Serve model vector DB UI page"""
|
1396 |
resp = send_from_directory(app.static_folder, 'model-vector-db.html')
|
|
|
1400 |
return resp
|
1401 |
|
1402 |
@app.route('/api/status', methods=['GET'])
|
1403 |
+
@fresh_login_required
|
1404 |
def status():
|
1405 |
return jsonify({
|
1406 |
"status": "online",
|