feat: add WeChat session authentication
This commit is contained in:
137
utils/auth.ts
Normal file
137
utils/auth.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user