diff --git a/Client/src/App.tsx b/Client/src/App.tsx index ce9a42f..781f676 100644 --- a/Client/src/App.tsx +++ b/Client/src/App.tsx @@ -4,49 +4,51 @@ import { LoginPage } from './components/LoginPage' import { BlogPage } from './components/BlogPage' import { StartPage } from './components/StartPage' import { useAuth } from './hooks/useAuth' +import { checkGate } from './api/gate' function App() { // 登录态从 localStorage/sessionStorage 恢复:已登录直接进博客页。 const auth = useAuth() - // 未登录默认进导航页;点齿轮切到登录页。 + // 是否显示登录页。入口 hash 是否合法由后端判定,故初值为 false,等校验通过再开。 const [showLogin, setShowLogin] = useState(false) const signOut = auth.signOut - // 进登录页:往浏览器历史压一条记录,便于返回键退回。 - const openLogin = () => { - window.history.pushState({ page: 'login' }, '') - setShowLogin(true) - } - - // “返回等于登出”:退回导航页时清掉登录态。 - // 浏览器返回键 / 登录页返回 / 博客页登出 都汇到这里。 + // 监听地址栏 hash:把 `#` 后那串发给后端校验,命中合法开发者才放行到登录页。 + // 合法 hash 列表只存在后端,前端不写死,扒 JS 也看不到合法值。 useEffect(() => { - const onPop = () => { - signOut() - setShowLogin(false) + let alive = true + const check = async () => { + const hash = window.location.hash.replace(/^#/, '') + const ok = hash ? await checkGate(hash) : false + // 防竞态:异步返回时若组件已卸载或 hash 又变了,丢弃这次结果。 + if (alive && hash === window.location.hash.replace(/^#/, '')) { + setShowLogin(ok) + } } - window.addEventListener('popstate', onPop) - return () => window.removeEventListener('popstate', onPop) - }, [signOut]) + check() + window.addEventListener('hashchange', check) + return () => { + alive = false + window.removeEventListener('hashchange', check) + } + }, []) - // 博客页登出:若是从登录流程进来的(历史里有 login 记录),走 history.back() - // 让浏览器历史保持干净;否则(如记住登录直接进博客)直接回首页。 - const signOutFromBlog = () => { - if (window.history.state?.page === 'login') { - window.history.back() - } else { - signOut() - setShowLogin(false) + // 回到导航页:清掉登录态,并抹掉地址栏里的密钥(防止留在历史里被看到)。 + const goStart = () => { + signOut() + setShowLogin(false) + if (window.location.hash) { + history.replaceState(null, '', window.location.pathname + window.location.search) } } let view if (auth.username) { - view = + view = } else if (showLogin) { - view = window.history.back()} /> + view = } else { - view = + view = } return ( diff --git a/Client/src/api/gate.ts b/Client/src/api/gate.ts new file mode 100644 index 0000000..69fefbf --- /dev/null +++ b/Client/src/api/gate.ts @@ -0,0 +1,24 @@ +// 入口门校验。把 URL `#` 后那串 hash 发给后端,问“是不是合法开发者的入口”。 +// 后端基址默认本机 8080,可用 VITE_API_BASE 覆盖(与 auth/useSystemStats 一致)。 +const API_BASE = + (import.meta.env.VITE_API_BASE as string | undefined) ?? 'http://localhost:8080' + +/** + * 校验入口 hash。命中合法开发者返回 true,否则 false。 + * 后端不可达 / 非 200 一律按“不放行”处理(返回 false),不暴露登录页。 + */ +export async function checkGate(hash: string): Promise { + if (!hash) return false + try { + const res = await fetch(`${API_BASE}/api/web/gate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hash }), + }) + if (!res.ok) return false + const data = (await res.json()) as { ok?: boolean } + return data.ok === true + } catch { + return false + } +} diff --git a/Client/src/components/StartPage.tsx b/Client/src/components/StartPage.tsx index c4142e6..509f141 100644 --- a/Client/src/components/StartPage.tsx +++ b/Client/src/components/StartPage.tsx @@ -5,7 +5,6 @@ import { useWeather } from '../hooks/useWeather' import { Greeting } from './start/Greeting' import { SearchBar } from './start/SearchBar' import { QuickLinks } from './start/QuickLinks' -import { SettingsGear } from './start/SettingsGear' import { WeatherFx } from './start/WeatherFx' // 背景图:放 src/assets/startpage/ 下。按昼夜选 light / dark; @@ -53,12 +52,7 @@ function isThunder(icon?: string): boolean { return icon ? ['302', '303', '304'].includes(icon) : false } -interface StartPageProps { - /** 点右下齿轮:切到登录页 */ - onOpenSettings: () => void -} - -export function StartPage({ onOpenSettings }: StartPageProps) { +export function StartPage() { const wx = useWeather() // 每分钟一拍,让昼夜随时间自动翻(即便天气没刷新)。 @@ -132,8 +126,6 @@ export function StartPage({ onOpenSettings }: StartPageProps) { - - ) } diff --git a/Client/src/components/start/SettingsGear.tsx b/Client/src/components/start/SettingsGear.tsx deleted file mode 100644 index b26c568..0000000 --- a/Client/src/components/start/SettingsGear.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { motion } from 'framer-motion' - -interface SettingsGearProps { - onClick: () => void -} - -// 右下角齿轮:悬停放大并旋转,点击跳转登录页。 -export function SettingsGear({ onClick }: SettingsGearProps) { - return ( - - - - - - - ) -} diff --git a/Server/.gitignore b/Server/.gitignore index ea8c4bf..8069243 100644 --- a/Server/.gitignore +++ b/Server/.gitignore @@ -1 +1,4 @@ /target + +# 开发者入口 hash 列表(秘密,单独部署,勿提交) +/dev_gate.txt