Files

138 lines
3.9 KiB
TypeScript

const AUTH_SESSION_KEY = 'teaching-feedback-auth-session-v1'
const EXPIRY_SKEW_MS = 60_000
interface RawLoginResponse {
access_token: string
expires_at: string
user_id: string
}
interface ErrorBody {
error?: string
}
export interface AuthSession {
apiBaseUrl: string
accessToken: string
expiresAt: number
userId: string
}
export class AuthenticationError extends Error {
constructor(message: string) {
super(message)
this.name = 'AuthenticationError'
}
}
let pendingLogin: { apiBaseUrl: string; promise: Promise<AuthSession> } | null = null
export function getStoredAuthSession(apiBaseUrl: string): AuthSession | null {
const value = wx.getStorageSync(AUTH_SESSION_KEY) as Partial<AuthSession> | string
if (!value || typeof value !== 'object') return null
if (
value.apiBaseUrl !== apiBaseUrl ||
typeof value.accessToken !== 'string' ||
!value.accessToken ||
typeof value.expiresAt !== 'number' ||
typeof value.userId !== 'string' ||
!value.userId
) {
return null
}
return value as AuthSession
}
export function clearAuthSession(): void {
wx.removeStorageSync(AUTH_SESSION_KEY)
}
export async function getAccessToken(apiBaseUrl: string, forceRefresh = false): Promise<string> {
if (!forceRefresh) {
const stored = getStoredAuthSession(apiBaseUrl)
if (stored && stored.expiresAt > Date.now() + EXPIRY_SKEW_MS) return stored.accessToken
}
return (await login(apiBaseUrl)).accessToken
}
export async function ensureAuthSession(apiBaseUrl: string): Promise<AuthSession> {
const accessToken = await getAccessToken(apiBaseUrl)
const session = getStoredAuthSession(apiBaseUrl)
if (!session || session.accessToken !== accessToken) {
throw new AuthenticationError('登录状态保存失败,请重试')
}
return session
}
async function login(apiBaseUrl: string): Promise<AuthSession> {
if (pendingLogin?.apiBaseUrl === apiBaseUrl) return pendingLogin.promise
const promise = loginWithWechat(apiBaseUrl).finally(() => {
if (pendingLogin?.promise === promise) pendingLogin = null
})
pendingLogin = { apiBaseUrl, promise }
return promise
}
async function loginWithWechat(apiBaseUrl: string): Promise<AuthSession> {
const code = await getWechatLoginCode()
const response = await exchangeCode(apiBaseUrl, code)
const expiresAt = Date.parse(response.expires_at)
if (
!response.access_token ||
!response.user_id ||
Number.isNaN(expiresAt) ||
expiresAt <= Date.now()
) {
throw new AuthenticationError('登录服务返回了无效会话')
}
const session: AuthSession = {
apiBaseUrl,
accessToken: response.access_token,
expiresAt,
userId: response.user_id
}
wx.setStorageSync(AUTH_SESSION_KEY, session)
return session
}
function getWechatLoginCode(): Promise<string> {
return new Promise((resolve, reject) => {
wx.login({
success(result) {
if (result.code) {
resolve(result.code)
return
}
reject(new AuthenticationError('微信未返回登录凭证'))
},
fail(error) {
reject(new AuthenticationError(`微信登录失败:${error.errMsg}`))
}
})
})
}
function exchangeCode(apiBaseUrl: string, code: string): Promise<RawLoginResponse> {
return new Promise((resolve, reject) => {
wx.request({
url: `${apiBaseUrl}/api/v1/auth/wechat/login`,
method: 'POST',
data: { code },
timeout: 15_000,
header: { 'Content-Type': 'application/json' },
success(response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(response.data as RawLoginResponse)
return
}
const body = response.data as ErrorBody
reject(new AuthenticationError(body?.error || `登录服务返回 HTTP ${response.statusCode}`))
},
fail(error) {
reject(new AuthenticationError(`无法连接登录服务:${error.errMsg}`))
}
})
})
}