Skip to content

Commit 8b260a6

Browse files
加入turnstile
1 parent ddbfd02 commit 8b260a6

4 files changed

Lines changed: 58 additions & 6 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
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

grades_dashboard.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
let radarChartInstance = null;
77
let barChartInstance = null;
88

9+
// HTML 跳脫輔助函數,防範 XSS
10+
function escapeHTML(str) {
11+
if (str === null || str === undefined) return '';
12+
const text = String(str);
13+
const map = {
14+
'&': '&',
15+
'<': '&lt;',
16+
'>': '&gt;',
17+
'"': '&quot;',
18+
"'": '&#039;'
19+
};
20+
return text.replace(/[&<>"']/g, function (m) { return map[m]; });
21+
}
22+
923
document.addEventListener('DOMContentLoaded', () => {
1024
loadGradesData();
1125
setupFileImport();
@@ -377,13 +391,13 @@ function generateScoreCards(subjects) {
377391
card.className = 'score-card';
378392
card.innerHTML = `
379393
<div class="score-header">
380-
<span class="subject-name">${subject.SubjectName}</span>
381-
<span class="score-badge ${scoreClass}">${subject.ScoreDisplay ?? scoreValue}</span>
394+
<span class="subject-name">${escapeHTML(subject.SubjectName)}</span>
395+
<span class="score-badge ${scoreClass}">${escapeHTML(subject.ScoreDisplay ?? scoreValue)}</span>
382396
</div>
383397
<div class="score-details">
384398
<div class="score-row">
385399
<span class="score-label">班級平均</span>
386-
<span class="score-value">${subject.ClassAVGScoreDisplay ?? classAvgValue.toFixed(2)}</span>
400+
<span class="score-value">${escapeHTML(subject.ClassAVGScoreDisplay ?? classAvgValue.toFixed(2))}</span>
387401
</div>
388402
<div class="score-row">
389403
<span class="score-label">與班平均差距</span>
@@ -626,7 +640,7 @@ function generateStandardsTable(subjects, standards) {
626640

627641
const row = document.createElement('tr');
628642
row.innerHTML = `
629-
<td>${shortenName(subject.SubjectName)}</td>
643+
<td>${escapeHTML(shortenName(subject.SubjectName))}</td>
630644
<td class="top-mark">${std["頂標"].toFixed(2)}</td>
631645
<td class="front-mark">${std["前標"].toFixed(2)}</td>
632646
<td class="avg-mark">${std["均標"].toFixed(2)}</td>
@@ -682,7 +696,7 @@ function generateDistributionCards(subjects, standards) {
682696
const card = document.createElement('div');
683697
card.className = 'distribution-card';
684698
card.innerHTML = `
685-
<h4>${subject.SubjectName}</h4>
699+
<h4>${escapeHTML(subject.SubjectName)}</h4>
686700
<div class="distribution-bars">
687701
${ranges.map(r => {
688702
const percentage = r.count === 0 ? 0 : (r.count / total) * 100;
@@ -838,12 +852,18 @@ function setupSyncFeature() {
838852
const handleLogin = async () => {
839853
const username = usernameInput.value.trim();
840854
const password = passwordInput.value.trim();
855+
const turnstileResponse = typeof turnstile !== 'undefined' ? turnstile.getResponse() : null;
841856

842857
if (!username || !password) {
843858
showStatus(loginStatus, '請輸入帳號密碼', 'error');
844859
return;
845860
}
846861

862+
if (!turnstileResponse) {
863+
showStatus(loginStatus, '請完成驗證', 'error');
864+
return;
865+
}
866+
847867
showStatus(loginStatus, '載入中...', 'normal');
848868
confirmLogin.disabled = true;
849869

@@ -852,7 +872,7 @@ function setupSyncFeature() {
852872
method: 'POST',
853873
headers: { 'Content-Type': 'application/json' },
854874
credentials: 'include',
855-
body: JSON.stringify({ username, password })
875+
body: JSON.stringify({ username, password, turnstile_response: turnstileResponse })
856876
});
857877
const data = await res.json();
858878

@@ -866,9 +886,11 @@ function setupSyncFeature() {
866886
}, 500);
867887
} else {
868888
showStatus(loginStatus, data.message || '登入失敗', 'error');
889+
if (typeof turnstile !== 'undefined') turnstile.reset();
869890
}
870891
} catch (error) {
871892
showStatus(loginStatus, '連線錯誤: ' + error.message, 'error');
893+
if (typeof turnstile !== 'undefined') turnstile.reset();
872894
} finally {
873895
confirmLogin.disabled = false;
874896
}

index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
77
<title>成績分析平台 - 中大壢中</title>
8+
<link rel="preconnect" href="https://fonts.googleapis.com">
9+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
810
<link
911
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Noto+Sans+TC:wght@400;500;700&display=swap"
1012
rel="stylesheet">
1113
<link
1214
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
1315
rel="stylesheet">
1416
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
17+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
1518
<link rel="stylesheet" href="/grades_dashboard.css">
1619
<link rel="icon" type="image/x-icon" href="/favicon.ico">
1720
</head>
@@ -158,6 +161,9 @@ <h3>登入系統</h3>
158161
</button>
159162
</div>
160163
</div>
164+
<div class="form-group" style="display: flex; justify-content: center; margin-top: 15px;">
165+
<div class="cf-turnstile" data-sitekey="0x4AAAAAAChO97Yocoqp2iiB"></div>
166+
</div>
161167
<div id="loginStatus" class="status-msg"></div>
162168
</div>
163169
<div class="modal-footer">

server.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import threading
1111
import secrets
1212
import string
13+
import requests
1314

1415
SHARED_FOLDER = 'shared_grades'
1516
CLEANUP_INTERVAL = 600 # 10 minutes
@@ -119,6 +120,27 @@ def login():
119120
if not username or not data.get('password'):
120121
return jsonify({'success': False, 'message': '請輸入帳號密碼'}), 400
121122

123+
turnstile_response = data.get('turnstile_response')
124+
secret_key = os.environ.get('TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA')
125+
126+
if not turnstile_response:
127+
return jsonify({'success': False, 'message': '請完成驗證'}), 400
128+
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+
122144
try:
123145
success, message, cookies, student_no, token = GradeFetcher.login_and_get_tokens(username, data.get('password'))
124146

0 commit comments

Comments
 (0)