feat: 接入两端用户名登录和独立学习空间 (#2)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
export function normalizeUsername(value) { return value.trim().toLowerCase() }
|
||||
export function validPassword(value) {
|
||||
const bytes = new TextEncoder().encode(value).length
|
||||
return bytes >= 10 && 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)) throw new Error('密码须为 10–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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user