feat(auth): P0 登录与会话(迁移 v16、argon2id、HttpOnly Cookie)

- 迁移机制:schema::migrate 支持 15→16,新增 admin/session/setting 表
- 接口:session/setup/login/logout/password;SameSite=Strict + Origin 校验 + 失败限流
- 会话只存 token 的 sha256;改密清空全部会话
- 前端:/login 独立壳(首次=设置密码),页头登录/退出切换
- 测试:37 passed(迁移 + 登录/会话 4 例)
- 文档:README 登录说明、docs/ADMIN-PLAN.md 方案与 P0 记录
This commit is contained in:
stash
2026-10-29 12:41:03 +08:00
parent 1aac8bcbe7
commit dea7781b86
14 changed files with 1222 additions and 46 deletions
+21
View File
@@ -28,6 +28,7 @@ import {
import { useProjects } from './useProjects'
import { useCatalog } from './useCatalog'
import { useViewRoute } from './useViewRoute'
import { fetchSession, logout as logoutRequest, type SessionState } from './auth'
import { parseViewPath, viewPath, type View } from './data/view'
import './App.css'
@@ -38,9 +39,21 @@ export default function App() {
const [keyword, setKeyword] = useState('')
// 当前视图由地址栏决定:刷新 / 收藏 / 前进后退都回到同一页(见 useViewRoute.ts)
const { view, go } = useViewRoute()
const [session, setSession] = useState<SessionState | null>(null)
const [scrolled, setScrolled] = useState(false)
const searchRef = useRef<HTMLInputElement>(null)
// 登录态:只用来切换页头按钮与后续的编辑入口
useEffect(() => {
let alive = true
fetchSession().then((next) => {
if (alive) setSession(next)
})
return () => {
alive = false
}
}, [])
const categories = useMemo(
() => withLocalCategory(projects, catalog.categories),
[projects, catalog.categories],
@@ -244,6 +257,10 @@ export default function App() {
)
}
const handleLogout = () => {
void logoutRequest().then(() => window.location.assign('/'))
}
return (
<>
{/* 主题的顶部背景图:固定 65vh,底部渐隐到页面底色 */}
@@ -270,7 +287,11 @@ export default function App() {
projectCount={projects.length}
sampleProject={projects[0]?.name}
live={live}
authenticated={session?.authenticated === true}
username={session?.username ?? undefined}
onFocusSearch={() => searchRef.current?.focus()}
onLogin={() => window.location.assign('/login')}
onLogout={handleLogout}
/>
}
sider={