diff --git a/index.html b/index.html index 805bc98..4b2dfec 100644 --- a/index.html +++ b/index.html @@ -434,6 +434,74 @@ gap: 8px; margin-top: 12px; } + .auth-gate { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + z-index: 100; + } + .auth-gate.hidden { + opacity: 0; + pointer-events: none; + } + .auth-gate-card { + width: min(520px, 100%); + border-radius: 24px; + border: 1px solid var(--line); + background: var(--panel); + padding: 30px; + text-align: center; + box-shadow: var(--shadow); + } + .auth-gate-card h2 { + margin: 0 0 10px; + font-size: 28px; + } + .auth-gate-card p { + margin: 0 0 18px; + color: var(--muted); + line-height: 1.7; + } + .auth-gate-card form { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 14px; + } + .auth-gate-card input { + border-radius: 12px; + border: 1px solid var(--line); + padding: 12px 14px; + background: transparent; + color: var(--text); + font-size: 14px; + } + .auth-gate-card button { + border: 0; + border-radius: 14px; + padding: 12px 18px; + font-weight: 700; + background: linear-gradient(135deg, var(--brand), #37a1ff); + color: #fff; + cursor: pointer; + font-size: 14px; + } + .overview-nav { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + margin-bottom: 16px; + } + .overview-nav .nav-card { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.18); + } .variant { padding: 10px 12px; border-radius: 14px; @@ -486,6 +554,7 @@ Privacy + @@ -535,29 +604,29 @@
Checking course and sandbox alignment...
-
-
登录培训入口
-

登录并解锁专属实验

-
-
-
- 未登录 -

登录后可访问加速练习、保存进度、查看历史。

-
- 公开访问 -
-
- - - -
- -
-
-
+
+ + + +
@@ -590,17 +659,17 @@
-
Stage map
-

阶段路线清单

-

按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。

+
Stage map
+

阶段路线清单

+

按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。

-
Command categories
-

命令家族可视化

-

了解各类命令在当前课程中的分布,以及建议的学习顺序。

+
Command categories
+

命令家族可视化

+

了解各类命令在当前课程中的分布,以及建议的学习顺序。

@@ -1343,6 +1412,7 @@ document.getElementById('practiceText').textContent = text.practiceText; document.getElementById('prevBtn').textContent = text.prevBtn; document.getElementById('nextBtn').textContent = text.nextBtn; + renderAuthControls(); if (!state.overview) { document.getElementById('courseTitle').textContent = text.courseTitle; @@ -1389,6 +1459,7 @@ renderStaticText(); document.getElementById('themeBtn').addEventListener('click', () => setTheme(state.theme === 'light' ? 'dark' : 'light')); document.getElementById('languageBtn').addEventListener('click', () => setLanguage(state.language === 'zh' ? 'en' : 'zh')); + document.getElementById('logoutBtn').addEventListener('click', logoutLab); document.getElementById('searchInput').addEventListener('keydown', (event) => { if (event.key === 'Enter') runSearch(); }); @@ -1399,8 +1470,12 @@ if (state.lesson?.next_lesson) openLesson(state.lesson.next_lesson.id); }); document.getElementById('masteryBtn').addEventListener('click', toggleMastery); - await loadOverview(); - renderAuthStatus(); + if (state.auth.loggedIn) { + hideAuthGate(); + await loadOverview(); + } else { + showAuthGate(); + } }); function saveProgress() { @@ -1434,24 +1509,55 @@ localStorage.setItem('linux_lab_auth', JSON.stringify(state.auth)); } - function renderAuthStatus() { - const badge = document.getElementById('authStatusBadge'); - const modeLabel = document.getElementById('authModeLabel'); - const hint = document.getElementById('authHint'); - const form = document.getElementById('loginForm'); + function renderAuthControls() { + const zh = state.language === 'zh'; const logoutBtn = document.getElementById('logoutBtn'); - badge.textContent = state.auth.loggedIn ? '已登录' : '公开访问'; - modeLabel.textContent = state.auth.loggedIn ? `当前登录:${state.auth.user}` : '未登录'; - hint.textContent = state.auth.loggedIn - ? '您已解锁全部实验和进度保存功能。' - : '登录后可解锁任务追踪、保存进度和补全排障建议提示。'; - form.style.display = state.auth.loggedIn ? 'none' : 'flex'; logoutBtn.style.display = state.auth.loggedIn ? 'inline-flex' : 'none'; - badge.classList.toggle('auth-status', !!state.auth.loggedIn); + logoutBtn.textContent = zh ? '退出登录' : 'Log out'; + + document.getElementById('navEyebrow').textContent = zh ? '总览菜单' : 'Overview menu'; + document.getElementById('navTitle').textContent = zh ? '课程总览' : 'Course overview'; + document.getElementById('navText').textContent = zh ? '按模块、阶段或练习快速跳转,不再只能上一课下一课。' : 'Jump by module, stage, or exercise instead of relying only on previous and next.'; + document.getElementById('navStageCountLabel').textContent = zh ? '已设置阶段' : 'Mapped stages'; + document.getElementById('navExerciseCountLabel').textContent = zh ? '练习总数' : 'Exercises'; + document.getElementById('navLessonEmpty').textContent = zh ? '登录后即可选择模块导航。' : 'Sign in to unlock module navigation.'; + document.getElementById('navStageEyebrow').textContent = zh ? '阶段地图' : 'Stage map'; + document.getElementById('navCategoryEyebrow').textContent = zh ? '命令分类' : 'Command categories'; + document.getElementById('stageMapEyebrow').textContent = zh ? '阶段地图' : 'Stage map'; + document.getElementById('stageMapTitle').textContent = zh ? '阶段路线清单' : 'Stage route checklist'; + document.getElementById('stageMapText').textContent = zh ? '按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。' : 'Study by stages so commands connect into a complete operations path.'; + document.getElementById('categoryEyebrow').textContent = zh ? '命令分类' : 'Command categories'; + document.getElementById('categoryTitle').textContent = zh ? '命令家族可视化' : 'Command family map'; + document.getElementById('categoryText').textContent = zh ? '了解各类命令在当前课程中的分布,以及建议的学习顺序。' : 'See how command families are distributed and which order makes sense.'; + document.getElementById('gateTitle').textContent = zh ? '请先登录' : 'Sign in first'; + document.getElementById('gateDescription').textContent = zh ? '登录后解锁课程总览、阶段地图、命令分类和练习仪表盘。' : 'Sign in to unlock the overview, stage map, command categories, and practice dashboard.'; + document.getElementById('gateUser').setAttribute('placeholder', zh ? '用户名' : 'Username'); + document.getElementById('gatePassword').setAttribute('placeholder', zh ? '密码' : 'Password'); + document.getElementById('gateSubmit').textContent = zh ? '登录并进入总览' : 'Login and enter overview'; + if (!state.auth.loggedIn) { + document.getElementById('gateMessage').textContent = zh ? '将自动保存进度与阶段状态,方便继续学习。' : 'Progress and stage state will be saved automatically so you can resume later.'; + } + } + + async function apiFetch(path, options = {}) { + const headers = new Headers(options.headers || {}); + if (state.auth.token) { + headers.set('Authorization', `Bearer ${state.auth.token}`); + headers.set('X-Token', state.auth.token); + } + return fetch(path, Object.assign({}, options, { headers })); + } + + function showAuthGate() { + document.getElementById('authGate').classList.remove('hidden'); + } + + function hideAuthGate() { + document.getElementById('authGate').classList.add('hidden'); } async function loadOverview() { - const response = await fetch('/api/overview'); + const response = await apiFetch('/api/overview'); state.overview = await response.json(); renderOverview(); const firstLesson = state.overview.modules?.[0]?.lessons?.[0]; @@ -1482,6 +1588,7 @@ renderModules(); renderStageGrid(); renderModuleSummaryGrid(); + renderVisualModules(); } function renderRuntime(runtime) { @@ -1570,7 +1677,7 @@ } async function openLesson(lessonId, focusExerciseId = '') { - const response = await fetch('/api/lesson?id=' + encodeURIComponent(lessonId)); + const response = await apiFetch('/api/lesson?id=' + encodeURIComponent(lessonId)); const payload = await response.json(); state.lesson = payload.lesson; renderModules(); @@ -1750,11 +1857,11 @@ feedback.className = 'feedback'; feedback.textContent = ''; try { - const runResponse = await fetch('/api/run?cmd=' + encodeURIComponent(cmd)); + const runResponse = await apiFetch('/api/run?cmd=' + encodeURIComponent(cmd)); const runData = await runResponse.json(); output.textContent = `$ ${cmd}\n\n${runData.output || runData.message || text.noOutput}`; renderRuntime({ cwd: runData.cwd || '/', user: state.overview?.runtime?.user || 'sandbox_user' }); - const checkResponse = await fetch('/api/check?exercise_id=' + encodeURIComponent(exerciseId) + '&last_cmd=' + encodeURIComponent(cmd) + '&output=' + encodeURIComponent(runData.output || '')); + const checkResponse = await apiFetch('/api/check?exercise_id=' + encodeURIComponent(exerciseId) + '&last_cmd=' + encodeURIComponent(cmd) + '&output=' + encodeURIComponent(runData.output || '')); const checkData = await checkResponse.json(); feedback.className = 'feedback show ' + (checkData.success ? 'success' : 'warn'); feedback.textContent = checkData.message + (checkData.next_suggestion ? '\n' + checkData.next_suggestion : ''); @@ -1767,7 +1874,7 @@ async function resetSandbox() { const text = uiText(); - await fetch('/api/reset', { method: 'POST' }); + await apiFetch('/api/reset', { method: 'POST' }); renderRuntime({ cwd: '/', user: 'sandbox_user' }); alert(text.sandboxResetComplete); } @@ -1782,7 +1889,7 @@ } container.innerHTML = `
${escapeHtml(text.searchLoading)}
`; try { - const response = await fetch('/api/course/search?q=' + encodeURIComponent(query)); + const response = await apiFetch('/api/course/search?q=' + encodeURIComponent(query)); const payload = await response.json(); const results = payload.results || []; if (!results.length) { @@ -1846,6 +1953,19 @@ const percent = totalLessons ? Math.round((masteredLessons / totalLessons) * 100) : 0; return `${escapeHtml(localizedModuleTitle(module))}: ${percent}%`; }).join(""); + const navStageMap = document.getElementById("navStageMap"); + const navStageCount = document.getElementById("navStageCount"); + if (navStageMap) { + navStageMap.innerHTML = modules.map((module, index) => ` +
+ ${index + 1}. ${escapeHtml(localizedModuleTitle(module))} + ${(module.lesson_count || 0)} lessons +
+ `).join(""); + } + if (navStageCount) { + navStageCount.textContent = modules.length; + } } function renderCategoryGrid() { @@ -1873,13 +1993,38 @@ `).join(""); } + function renderNavLessons() { + const container = document.getElementById("navLessonResults"); + const modules = state.overview?.modules || []; + const exerciseCount = modules.reduce((sum, module) => sum + (module.exercise_count || 0), 0); + const lessonCountEl = document.getElementById("navLessonCount"); + const exerciseCountEl = document.getElementById("navExerciseCount"); + if (lessonCountEl) { + lessonCountEl.textContent = modules.length; + } + if (exerciseCountEl) { + exerciseCountEl.textContent = exerciseCount; + } + if (!modules.length) { + container.innerHTML = `
${escapeHtml(uiText().noModulesFound)}
`; + return; + } + container.innerHTML = modules.map((module) => ` + + `).join(""); + } + async function loginToLab(event) { event.preventDefault(); - const username = document.getElementById("authUser").value.trim(); - const password = document.getElementById("authPassword").value.trim(); - const messageNode = document.getElementById("authMessage"); + const zh = state.language === 'zh'; + const username = document.getElementById("gateUser").value.trim(); + const password = document.getElementById("gatePassword").value.trim(); + const messageNode = document.getElementById("gateMessage"); if (!username || !password) { - messageNode.textContent = "请输入用户名和密码"; + messageNode.textContent = zh ? "请输入用户名和密码" : "Please enter a username and password."; return; } try { @@ -1891,41 +2036,52 @@ const payload = await response.json(); if (payload.success) { state.auth = { loggedIn: true, user: username, token: payload.token }; - messageNode.textContent = payload.message || "登录成功"; - document.getElementById("authStatusBadge").textContent = "已登录"; - document.getElementById("authModeLabel").textContent = `当前登录:${username}`; - document.getElementById("logoutBtn").style.display = "inline-flex"; - document.getElementById("loginForm").style.display = "none"; + persistAuth(); + hideAuthGate(); + messageNode.textContent = payload.message || (zh ? "登录成功" : "Login succeeded."); + renderAuthControls(); + await loadOverview(); return; } - messageNode.textContent = payload.error || "登录失败"; + messageNode.textContent = payload.error || (zh ? "登录失败" : "Login failed."); } catch (error) { - messageNode.textContent = "网络异常,请稍后再试"; + messageNode.textContent = zh ? "网络异常,请稍后再试" : "Network error. Please try again later."; } } function logoutLab() { + const zh = state.language === 'zh'; state.auth = { loggedIn: false, user: "", token: "" }; - document.getElementById("authMessage").textContent = "已退出"; - document.getElementById("authStatusBadge").textContent = "仅公开"; - document.getElementById("authModeLabel").textContent = "未登录"; - document.getElementById("logoutBtn").style.display = "none"; - document.getElementById("loginForm").style.display = "flex"; + persistAuth(); + showAuthGate(); + renderAuthControls(); + document.getElementById("gateMessage").textContent = zh ? "已退出,重新登录即可继续。" : "Logged out. Sign in again to continue."; } function renderVisualModules() { renderStageTimeline(); renderCategoryGrid(); + renderNavLessons(); } - document.getElementById("loginForm").addEventListener("submit", loginToLab); - document.getElementById("logoutBtn").addEventListener("click", logoutLab); + document.getElementById("gateLoginForm").addEventListener("submit", loginToLab); window.openLesson = openLesson; window.runExercise = runExercise; window.runSearch = runSearch; window.resetSandbox = resetSandbox; - renderVisualModules(); +
+
+

请先登录

+

登录后解锁课程总览、阶段地图、命令分类和练习仪表盘。

+
+ + + +
+

将自动保存进度与阶段状态,方便继续学习。

+
+
diff --git a/server.py b/server.py index 7093e30..ad87c26 100755 --- a/server.py +++ b/server.py @@ -26,15 +26,11 @@ PUBLIC_GET_PATHS = { "/", "/privacy", "/privacy.html", - "/api/course", - "/api/course/search", - "/api/diagnostics", "/api/health", - "/api/lesson", - "/api/overview", } PUBLIC_POST_PATHS = { "/api/login", + "/api/logout", } SAFE_REMOTE_HOST = "xiaoxiaoluohao.indevs.in" @@ -1009,8 +1005,6 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler): return False def check_auth(self, auth_header: str, token: str) -> bool: - if self.client_address[0] == "127.0.0.1": - return True if token == "safe_linux_2026": return True if auth_header.startswith("Bearer ") and auth_header[7:] == "safe_linux_2026": @@ -1020,10 +1014,9 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler): def require_auth_if_needed(self, path: str, method: str) -> bool: if self.is_public_path(path, method): return True - host = self.headers.get("Host", "") auth_header = self.headers.get("Authorization", "") token = self.headers.get("X-Token", "") - if SAFE_REMOTE_HOST in host and not self.check_auth(auth_header, token): + if not self.check_auth(auth_header, token): self.send_json({"error": "Authentication required"}, 401) return False return True