export function normalizeUsername(value) { return value.trim().toLowerCase() } export function validPassword(value, minimum = 10) { const bytes = new TextEncoder().encode(value).length return bytes >= minimum && bytes <= 72 } export function validUsername(value) { return /^[a-z][a-z0-9_.-]{2,31}$/.test(normalizeUsername(value)) } export function createSession({ fetch, storage, changed = () => {} }) { const key = 'lexgo-admin-token' const state = { token: storage.getItem(key) || '', user: null, accounts: [], generation: 0 } function clear() { state.generation++ state.token = '' state.user = null state.accounts = [] storage.removeItem(key) changed(state) } function assertCurrent(generation) { if (generation !== state.generation) throw new Error('会话已变化,请重新操作') } async function request(path, method = 'GET', body, token = state.token, generation = state.generation) { const result = await fetch('/api/v1' + path, { method, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), cache: 'no-store' }) const payload = await result.json() assertCurrent(generation) if (!result.ok) { if (result.status === 401 || result.status === 403) clear() throw new Error(result.status === 401 ? '登录已失效,请重新登录' : payload.msg || '请求失败') } return payload.data } async function revoke(token) { // Cleanup must never restore a previous identity or expose its response. if (!token) return let result try { result = await fetch('/api/v1/logout', { method: 'POST', headers: { Authorization: 'Bearer ' + token } }) } catch { throw new Error('服务器尚未确认退出,原会话可能仍有效') } if (!result.ok && result.status !== 401) throw new Error('服务器尚未确认退出,原会话可能仍有效') } async function requireAdmin(user, token) { if (user.role !== 'admin') { clear() try { await revoke(token) } catch (error) { throw new Error('此账号没有管理权限;' + error.message) } throw new Error('此账号没有管理权限') } } function authorized() { if (!state.user || state.user.role !== 'admin') throw new Error('请先登录管理员账号') } return { state, clear, async login(username, password) { const oldToken = state.token clear() const generation = state.generation if (oldToken) await revoke(oldToken) assertCurrent(generation) const data = await request('/login', 'POST', { username: normalizeUsername(username), password }, '', generation) assertCurrent(generation) await requireAdmin(data.user, data.token) assertCurrent(generation) state.token = data.token state.user = data.user storage.setItem(key, data.token) changed(state) }, async restore() { if (!state.token) return false const generation = state.generation try { const user = await request('/me') assertCurrent(generation) await requireAdmin(user, state.token) assertCurrent(generation) state.user = user changed(state) return true } catch (error) { if (generation === state.generation) clear(); throw error } }, async logout() { const token = state.token; clear(); await revoke(token) }, async loadAccounts() { authorized() const generation = state.generation const data = await request('/accounts') assertCurrent(generation) state.accounts = data.items changed(state) }, async createAccount(username, password) { authorized() if (!validUsername(username)) throw new Error('账号须为 3–32 位,以字母开头,仅含字母、数字、点、下划线或短横线') if (!validPassword(password, 6)) throw new Error('密码须为 6–72 字节') return request('/accounts', 'POST', { username: normalizeUsername(username), password }) }, async updateAccount(id, patch) { authorized() if (patch.password !== undefined && !validPassword(patch.password)) throw new Error('密码须为 10–72 字节') if (id === state.user.id && patch.disabled) throw new Error('不能停用当前管理员') const account = state.accounts.find(item => item.id === id) if (id === state.user.id || (account && account.role !== 'learner')) throw new Error('管理员账号不支持此操作') return request('/accounts/' + id, 'PATCH', patch) } } }