Skip to content

Commit 4b98bce

Browse files
fix4
1 parent 768a510 commit 4b98bce

6 files changed

Lines changed: 96 additions & 40 deletions

File tree

.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
TUNNEL_TOKEN=
22
SECRET_KEY=your_random_secret_string_here
3-
TURNSTILE_SITE_KEY=your_site_key_here
4-
TURNSTILE_SECRET_KEY=your_secret_key_here

DEPLOYMENT_GUIDE.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ cd school_grades/school_grades
4848
```env
4949
GHCR_IMAGE=您的GitHub帳號/school_grades
5050
SECRET_KEY=您的Flask_Secret_Key
51-
TURNSTILE_SITE_KEY=您的Cloudflare_Turnstile_SiteKey
52-
TURNSTILE_SECRET_KEY=您的Cloudflare_Turnstile_Secret
5351
TUNNEL_TOKEN=您的Cloudflare_Tunnel_Token
5452
```
5553

docker-compose.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ services:
66
environment:
77
- PYTHONUNBUFFERED=1
88
- SECRET_KEY=${SECRET_KEY}
9-
- TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}
10-
- TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY}
119
- TZ=Asia/Taipei
1210

1311
deploy:

grades_dashboard.js

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,55 @@ function setupSyncFeature() {
767767

768768
const API_BASE = '/api';
769769
let availableStructure = {}; // Store the loaded structure
770+
let turnstileSiteKey = '';
771+
let turnstileWidgetId = null;
772+
773+
// 獲取 Turnstile site key
774+
async function fetchTurnstileSiteKey() {
775+
if (turnstileSiteKey) return turnstileSiteKey;
776+
try {
777+
const res = await fetch(`${API_BASE}/turnstile-site-key`);
778+
const data = await res.json();
779+
turnstileSiteKey = data.siteKey || '';
780+
} catch (e) {
781+
console.warn('Failed to fetch Turnstile site key:', e);
782+
}
783+
return turnstileSiteKey;
784+
}
785+
786+
// 渲染 Turnstile widget
787+
async function renderTurnstile() {
788+
const container = document.getElementById('turnstileContainer');
789+
if (!container) return;
790+
791+
// 先重置舊的 widget
792+
if (turnstileWidgetId !== null && typeof turnstile !== 'undefined') {
793+
try { turnstile.remove(turnstileWidgetId); } catch (e) { /* ignore */ }
794+
turnstileWidgetId = null;
795+
}
796+
container.innerHTML = '';
797+
798+
const siteKey = await fetchTurnstileSiteKey();
799+
if (!siteKey) return;
800+
801+
// 等待 Turnstile API 載入
802+
const waitForTurnstile = () => new Promise((resolve) => {
803+
if (typeof turnstile !== 'undefined') return resolve();
804+
const interval = setInterval(() => {
805+
if (typeof turnstile !== 'undefined') {
806+
clearInterval(interval);
807+
resolve();
808+
}
809+
}, 100);
810+
});
811+
812+
await waitForTurnstile();
813+
814+
turnstileWidgetId = turnstile.render(container, {
815+
sitekey: siteKey,
816+
theme: 'dark',
817+
});
818+
}
770819

771820
// Helper to toggle modal
772821
const toggleModal = (modal, show) => {
@@ -812,16 +861,20 @@ function setupSyncFeature() {
812861
const handleLogin = async () => {
813862
const username = usernameInput.value.trim();
814863
const password = passwordInput.value.trim();
815-
const turnstileResponse = typeof turnstile !== 'undefined' ? turnstile.getResponse() : null;
816864

817865
if (!username || !password) {
818866
showStatus(loginStatus, '請輸入帳號密碼', 'error');
819867
return;
820868
}
821869

822-
if (!turnstileResponse) {
823-
showStatus(loginStatus, '請完成驗證', 'error');
824-
return;
870+
// 取得 Turnstile token
871+
let turnstileResponse = '';
872+
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) {
873+
turnstileResponse = turnstile.getResponse(turnstileWidgetId) || '';
874+
if (!turnstileResponse) {
875+
showStatus(loginStatus, '請完成人機驗證', 'error');
876+
return;
877+
}
825878
}
826879

827880
showStatus(loginStatus, '登入中...', 'normal');
@@ -832,7 +885,7 @@ function setupSyncFeature() {
832885
method: 'POST',
833886
headers: { 'Content-Type': 'application/json' },
834887
credentials: 'include',
835-
body: JSON.stringify({ username, password, turnstile_response: turnstileResponse })
888+
body: JSON.stringify({ username, password, 'cf-turnstile-response': turnstileResponse })
836889
});
837890
const data = await res.json();
838891

@@ -846,11 +899,11 @@ function setupSyncFeature() {
846899
}, 500);
847900
} else {
848901
showStatus(loginStatus, data.message || '登入失敗', 'error');
849-
if (typeof turnstile !== 'undefined') turnstile.reset();
902+
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) turnstile.reset(turnstileWidgetId);
850903
}
851904
} catch (error) {
852905
showStatus(loginStatus, '連線錯誤: ' + error.message, 'error');
853-
if (typeof turnstile !== 'undefined') turnstile.reset();
906+
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) turnstile.reset(turnstileWidgetId);
854907
} finally {
855908
confirmLogin.disabled = false;
856909
}
@@ -885,6 +938,7 @@ function setupSyncFeature() {
885938
toggleModal(selectExamModal, false);
886939
toggleModal(loginModal, true);
887940
usernameInput.focus();
941+
renderTurnstile();
888942
return;
889943
}
890944

index.html

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
<link rel="preconnect" href="https://fonts.googleapis.com">
99
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1010
<meta http-equiv="Content-Security-Policy"
11-
content="default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net https://challenges.cloudflare.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://challenges.cloudflare.com https://cdn.jsdelivr.net; frame-src 'self' https://challenges.cloudflare.com; img-src 'self' data:;">
11+
content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://cdn.jsdelivr.net https://challenges.cloudflare.com; img-src 'self' data:; frame-src https://challenges.cloudflare.com;">
1212
<link
1313
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Noto+Sans+TC:wght@400;500;700&display=swap"
1414
rel="stylesheet">
1515
<link
1616
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
1717
rel="stylesheet">
1818
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
19-
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
19+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
2020
<link rel="stylesheet" href="/grades_dashboard.css">
2121
<link rel="icon" type="image/x-icon" href="/favicon.ico">
2222
</head>
@@ -163,10 +163,9 @@ <h3>登入系統</h3>
163163
</button>
164164
</div>
165165
</div>
166-
<div class="form-group" style="display: flex; justify-content: center; margin-top: 15px;">
167-
<div class="cf-turnstile" data-sitekey="0x4AAAAAAChO97Yocoqp2iiB"></div>
168-
</div>
169166
<div id="loginStatus" class="status-msg"></div>
167+
<div id="turnstileContainer" style="display: flex; justify-content: center; margin-top: 12px;">
168+
</div>
170169
</div>
171170
<div class="modal-footer">
172171
<button class="modal-btn cancel" id="cancelLogin">取消</button>

server.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
from werkzeug.middleware.proxy_fix import ProxyFix
44
import json
55
import os
6+
import requests as http_requests
67
import logging
78
from logging.handlers import RotatingFileHandler
89
from grade_fetcher import GradeFetcher
910
import time
1011
import threading
1112
import secrets
1213
import string
13-
import requests
14-
1514
SHARED_FOLDER = 'shared_grades'
1615
CLEANUP_INTERVAL = 600 # 10 minutes
1716
FILE_LIFETIME = 7200 # 2 hours
@@ -53,6 +52,10 @@
5352
# CORS 設定 - 允許 credentials
5453
CORS(app, supports_credentials=True)
5554

55+
# Turnstile 設定
56+
TURNSTILE_SECRET_KEY = os.environ.get('TURNSTILE_SECRET_KEY', '')
57+
TURNSTILE_SITE_KEY = os.environ.get('TURNSTILE_SITE_KEY', '')
58+
5659

5760
if not os.path.exists(SHARED_FOLDER):
5861
os.makedirs(SHARED_FOLDER)
@@ -103,6 +106,10 @@ def static_files(filename):
103106
return send_from_directory('.', filename)
104107

105108

109+
@app.route('/api/turnstile-site-key')
110+
def get_turnstile_site_key():
111+
return jsonify({'siteKey': TURNSTILE_SITE_KEY})
112+
106113
@app.route('/api/check_login')
107114
def check_login():
108115
# Only check if we have tokens in session
@@ -119,28 +126,30 @@ def login():
119126

120127
if not username or not data.get('password'):
121128
return jsonify({'success': False, 'message': '請輸入帳號密碼'}), 400
122-
123-
turnstile_response = data.get('turnstile_response')
124-
secret_key = os.environ.get('TURNSTILE_SECRET_KEY') or '1x0000000000000000000000000000000AA'
125129

126-
if not turnstile_response:
127-
return jsonify({'success': False, 'message': '請完成驗證'}), 400
130+
# Turnstile 驗證
131+
turnstile_token = data.get('cf-turnstile-response', '')
132+
if TURNSTILE_SECRET_KEY:
133+
if not turnstile_token:
134+
return jsonify({'success': False, 'message': '請完成人機驗證'}), 400
135+
try:
136+
verify_res = http_requests.post(
137+
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
138+
data={
139+
'secret': TURNSTILE_SECRET_KEY,
140+
'response': turnstile_token,
141+
'remoteip': request.remote_addr
142+
},
143+
timeout=10
144+
)
145+
verify_data = verify_res.json()
146+
if not verify_data.get('success'):
147+
logger.warning(f"Turnstile verification failed for user {username}: {verify_data}")
148+
return jsonify({'success': False, 'message': '人機驗證失敗,請重試'}), 403
149+
except Exception as e:
150+
logger.error(f"Turnstile verification error: {e}")
151+
return jsonify({'success': False, 'message': '驗證服務暫時無法使用,請稍後再試'}), 500
128152

129-
try:
130-
cf_url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
131-
cf_data = {
132-
'secret': secret_key,
133-
'response': turnstile_response
134-
}
135-
cf_res = requests.post(cf_url, data=cf_data, timeout=10)
136-
cf_json = cf_res.json()
137-
if not cf_json.get('success'):
138-
logger.warning(f"Turnstile verification failed for user {username}: {cf_json}")
139-
return jsonify({'success': False, 'message': '驗證失敗,請重試'}), 400
140-
except Exception as e:
141-
logger.error(f"Turnstile API error: {e}")
142-
return jsonify({'success': False, 'message': '驗證連線失敗,請稍後再試'}), 500
143-
144153
try:
145154
success, message, cookies, student_no, token = GradeFetcher.login_and_get_tokens(username, data.get('password'))
146155

0 commit comments

Comments
 (0)