Skip to content

Commit 64f6b5f

Browse files
Showchen168claude
andcommitted
refactor: 代碼完整性修復與安全性優化
Phase 1 - 代碼清理: - 移除重複定義的 isPlaceholderFirebaseConfig() 函數(3處合併為1處) - 移除重複定義的 setAllRecords() 函數(3處合併為1處) - 修正版本驗證邏輯,移除 PATCH 0-9 限制以符合 SemVer 標準 - 更新測試案例以驗證新的版本格式規則 Phase 2 - 安全性修復: - 移除硬編碼的 Firebase API 密鑰,改為環境變數注入方式 - 本地認證密碼改用 SHA-256 雜湊存儲,不再明文保存 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d618c57 commit 64f6b5f

3 files changed

Lines changed: 47 additions & 51 deletions

File tree

app.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@
1010

1111

1212
def validate_version(version: str) -> None:
13-
"""檢查版本字串格式與 PATCH 範圍。"""
13+
"""檢查版本字串格式(符合 SemVer 標準)。"""
1414
if not version.startswith("v"):
1515
raise ValueError("版本格式錯誤:缺少 v 前綴。")
1616
parts = version[1:].split(".")
1717
if len(parts) != 3 or not all(p.isdigit() for p in parts):
1818
raise ValueError("版本格式錯誤:需為 vMAJOR.MINOR.PATCH。")
1919
major, minor, patch = (int(p) for p in parts)
20-
if patch < 0 or patch > 9:
21-
raise ValueError("版本格式錯誤:PATCH 必須為 0-9。")
22-
if major < 0 or minor < 0:
20+
if major < 0 or minor < 0 or patch < 0:
2321
raise ValueError("版本格式錯誤:版號不得為負數。")
2422

2523

index.html

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,15 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
998998
localStorage.setItem(AUTH_USERS_KEY, JSON.stringify(users));
999999
}
10001000

1001+
// 密碼雜湊函數(使用 Web Crypto API)
1002+
async function hashPassword(password) {
1003+
const encoder = new TextEncoder();
1004+
const data = encoder.encode(password);
1005+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
1006+
const hashArray = Array.from(new Uint8Array(hashBuffer));
1007+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
1008+
}
1009+
10011010
function getCurrentUserEmail() {
10021011
return localStorage.getItem(AUTH_CURRENT_USER_KEY);
10031012
}
@@ -1215,7 +1224,8 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
12151224
return;
12161225
}
12171226
const users = getStoredUsers();
1218-
const match = users.find(user => user.email === email && user.password === password);
1227+
const hashedPassword = await hashPassword(password);
1228+
const match = users.find(user => user.email === email && user.passwordHash === hashedPassword);
12191229
if (!match) {
12201230
setAuthError('auth-login-error', '帳號或密碼錯誤,請重新輸入。');
12211231
return;
@@ -1266,7 +1276,8 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
12661276
setAuthError('auth-register-error', '此郵箱已註冊,請直接登入。');
12671277
return;
12681278
}
1269-
users.push({ email, password });
1279+
const passwordHash = await hashPassword(password);
1280+
users.push({ email, passwordHash });
12701281
saveStoredUsers(users);
12711282
setCurrentUserEmail(email);
12721283
setCachedAuthEmail(email);
@@ -1353,17 +1364,6 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
13531364
switchTab(initialTab);
13541365
}
13551366

1356-
function isPlaceholderFirebaseConfig(cfg) {
1357-
return !cfg.apiKey || cfg.apiKey.startsWith('YOUR_') || !cfg.projectId || cfg.projectId.includes('YOUR_PROJECT_ID');
1358-
}
1359-
function setAllRecords(newRecords) {
1360-
window.allRecords = (newRecords || []).sort((a, b) => new Date(b.date) - new Date(a.date));
1361-
if(window.renderRecordsTable) window.renderRecordsTable();
1362-
if(window.renderAdminTable) window.renderAdminTable();
1363-
renderAnalyticsYearOptions();
1364-
if(window.updateCharts) window.updateCharts();
1365-
}
1366-
13671367
function parseSalaryGrowth(value){
13681368
if(value === undefined || value === null) return null;
13691369
const num = parseFloat(String(value).replace('%','').trim());
@@ -1653,16 +1653,6 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
16531653
renderSalaryGrowthChart(stats);
16541654
};
16551655

1656-
function isPlaceholderFirebaseConfig(cfg) {
1657-
return !cfg.apiKey || cfg.apiKey.startsWith('YOUR_') || !cfg.projectId || cfg.projectId.includes('YOUR_PROJECT_ID');
1658-
}
1659-
function setAllRecords(newRecords) {
1660-
window.allRecords = (newRecords || []).sort((a, b) => new Date(b.date) - new Date(a.date));
1661-
if(window.renderRecordsTable) window.renderRecordsTable();
1662-
if(window.renderAdminTable) window.renderAdminTable();
1663-
if(window.updateCharts) window.updateCharts();
1664-
}
1665-
16661656
// Google Drive (可透過 __google_drive_config 覆寫)
16671657
const defaultGoogleDriveConfig = {
16681658
apiKey: 'YOUR_GOOGLE_API_KEY',
@@ -1691,6 +1681,7 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
16911681
persistLocalRecords(window.allRecords);
16921682
if(window.renderRecordsTable) window.renderRecordsTable();
16931683
if(window.renderAdminTable) window.renderAdminTable();
1684+
renderAnalyticsYearOptions();
16941685
if(window.updateCharts) window.updateCharts();
16951686
if(window.updateDraftSelector) window.updateDraftSelector();
16961687
}
@@ -2523,29 +2514,27 @@ <h3 class="text-white font-bold">編輯紀錄</h3>
25232514
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, createUserWithEmailAndPassword, signInWithEmailAndPassword, sendPasswordResetEmail, signOut } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
25242515
import { getFirestore, collection, addDoc, onSnapshot, doc, updateDoc, deleteDoc, query, orderBy, setDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
25252516

2526-
const firebaseConfig = {
2527-
apiKey: "AIzaSyAGPH6HKTi8yikT9rG_VhoivwWycM5LO6Y",
2528-
authDomain: "resignation-system.firebaseapp.com",
2529-
projectId: "resignation-system",
2530-
storageBucket: "resignation-system.firebasestorage.app",
2531-
messagingSenderId: "20779725950",
2532-
appId: "1:20779725950:web:12a2d3b753d5cf3bc0d42f",
2533-
measurementId: "G-EH2FBKQTKD"
2534-
};
2535-
// 為了在沒有變數的情況下不報錯,這裡做一個防呆
2536-
// 實際部署時請務必填寫上方 config
2537-
2538-
// 注意:GitHub Pages 部署時,以下這行必須修改
2539-
// const firebaseConfig = JSON.parse(__firebase_config);
2540-
2541-
// 在此環境下為了模擬,我們還是會嘗試讀取變數,但會加上 try-catch
2542-
let finalConfig = firebaseConfig;
2517+
// Firebase 配置:優先從環境變數 __firebase_config 讀取
2518+
// 部署時請透過 script 標籤注入:<script>window.__firebase_config = '{"apiKey":"...","projectId":"..."}';</script>
2519+
const defaultFirebaseConfig = {
2520+
apiKey: "YOUR_API_KEY",
2521+
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
2522+
projectId: "YOUR_PROJECT_ID",
2523+
storageBucket: "YOUR_PROJECT_ID.appspot.com",
2524+
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
2525+
appId: "YOUR_APP_ID"
2526+
};
2527+
2528+
let finalConfig = defaultFirebaseConfig;
25432529
try {
2544-
if(typeof __firebase_config !== 'undefined') {
2545-
finalConfig = JSON.parse(__firebase_config);
2530+
if (typeof window.__firebase_config !== 'undefined' && window.__firebase_config) {
2531+
const injectedConfig = typeof window.__firebase_config === 'string'
2532+
? JSON.parse(window.__firebase_config)
2533+
: window.__firebase_config;
2534+
finalConfig = { ...defaultFirebaseConfig, ...injectedConfig };
25462535
}
25472536
} catch(e) {
2548-
console.log("Using placeholder config");
2537+
console.warn("Firebase 配置解析失敗,使用預設值", e);
25492538
}
25502539

25512540
if (isPlaceholderFirebaseConfig(finalConfig)) {

tests/test_version.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ def test_get_version_returns_current_version():
77
assert get_version() == APP_VERSION
88

99

10-
def test_validate_version_rejects_invalid_patch():
11-
invalid_version = f"v1.0.{9 + 1}"
12-
with pytest.raises(ValueError, match="PATCH 必須為 0-9"):
13-
validate_version(invalid_version)
10+
def test_validate_version_rejects_invalid_format():
11+
"""測試無效的版本格式會被拒絕。"""
12+
# 缺少 v 前綴
13+
with pytest.raises(ValueError, match="缺少 v 前綴"):
14+
validate_version("1.0.0")
15+
16+
# 格式錯誤
17+
with pytest.raises(ValueError, match="需為 vMAJOR.MINOR.PATCH"):
18+
validate_version("v1.0")
19+
20+
# 非數字
21+
with pytest.raises(ValueError, match="需為 vMAJOR.MINOR.PATCH"):
22+
validate_version("v1.0.beta")

0 commit comments

Comments
 (0)