Files
danyang-match/miniapp/stores/authStore.js
T

160 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AuthStore(规范 §131)
*
* 职责:
* - 微信登录 / 绑定手机号 / 刷新 Token / 登出
* - Token 持久化委托 utils/api.js 完成;本层只维护 isLoggedIn 等状态
* - 401 Session 失效时单飞刷新,刷新失败再触发 onSessionExpired
* - 注入 onSessionExpired 回调到 apiClient
*
* 不变量(AGENTS.md 第 3 条):未登录用户不可访问任何需要权限的页面,
* 路由守卫根据 AuthStore.isLoggedIn 决定是否重定向到登录页。
*/
const BaseStore = require('./baseStore');
const api = require('../utils/api');
const { authService } = require('../services');
const { STORAGE_KEYS, REQUEST_ERROR } = require('../constants');
class AuthStore extends BaseStore {
constructor() {
super({
isLoggedIn: false,
/** @type {string|null} */
userId: null,
/** @type {string|null} */
phoneBound: false,
/** @type {'idle'|'logging_in'|'refreshing'|'logging_out'|'expired'} */
authStatus: 'idle',
lastError: null,
});
this._refreshPromise = null;
// 注入 Session 失效回调(apiClient 在 401 时调用)
api.onSessionExpired = () => this.handleSessionExpired();
}
/** 冷启动从 storage 还原登录态 */
hydrate() {
const snapshot = BaseStore.hydrate(STORAGE_KEYS.IS_LOGGED_IN);
if (snapshot && typeof snapshot === 'object') {
this.setState({
isLoggedIn: !!snapshot.isLoggedIn,
userId: snapshot.userId || null,
phoneBound: !!snapshot.phoneBound,
});
} else if (api.getToken()) {
// 兼容旧版仅存了 token 的场景
this.setState({ isLoggedIn: true });
}
}
/**
* 微信登录
* @param {string} code wx.login() 拿到的 code
* @returns {Promise<{ needPhone: boolean, needProfile: boolean }>}
*/
async loginWithWechat(code) {
this.setState({ authStatus: 'logging_in', lastError: null });
try {
const result = await authService.wechatLogin({ code });
api.saveToken(result.token, result.refreshToken);
const patch = {
isLoggedIn: true,
userId: result.userId || null,
phoneBound: !result.needPhone,
authStatus: 'idle',
};
this.setState(patch);
this.persist(
{ isLoggedIn: true, userId: patch.userId, phoneBound: patch.phoneBound },
STORAGE_KEYS.IS_LOGGED_IN,
);
return { needPhone: !!result.needPhone, needProfile: !!result.needProfile };
} catch (err) {
this.setState({ authStatus: 'idle', lastError: err });
throw err;
}
}
/**
* 绑定手机号
* @param {{ phoneCode?: string, encryptedData?: string, iv?: string }} payload
*/
async bindPhone(payload) {
const result = await authService.bindPhone(payload);
this.setState({ phoneBound: true });
// 同步持久化
const snap = BaseStore.hydrate(STORAGE_KEYS.IS_LOGGED_IN) || {};
this.persist({ ...snap, phoneBound: true }, STORAGE_KEYS.IS_LOGGED_IN);
return result;
}
/**
* 刷新 Token(单飞:并发 401 只触发一次刷新)
* @returns {Promise<boolean>} 是否刷新成功
*/
async refresh() {
if (this._refreshPromise) return this._refreshPromise;
this._refreshPromise = (async () => {
this.setState({ authStatus: 'refreshing' });
try {
const result = await authService.refresh();
api.saveToken(result.token, result.refreshToken);
this.setState({ authStatus: 'idle', isLoggedIn: true });
return true;
} catch (err) {
// 刷新失败 → 真正的 Session 失效
this._handleExpired();
return false;
} finally {
this._refreshPromise = null;
}
})();
return this._refreshPromise;
}
/** 401 时由 apiClient 调用:先尝试刷新,刷新失败再走 onSessionExpired */
async handleSessionExpired() {
// 已在登录流程中则不打断
if (this.state.authStatus === 'logging_in') return;
const ok = await this.refresh();
if (!ok) {
this._handleExpired();
}
}
/** 真正的 Session 失效:清 token、清状态、由路由守卫跳登录 */
_handleExpired() {
api.clearToken();
try { wx.removeStorageSync(STORAGE_KEYS.IS_LOGGED_IN); } catch (e) {}
this.setState({
isLoggedIn: false,
userId: null,
phoneBound: false,
authStatus: 'expired',
lastError: { code: REQUEST_ERROR.UNAUTHORIZED, message: '登录状态已过期,请重新登录' },
});
}
/** 主动登出 */
async logout() {
this.setState({ authStatus: 'logging_out' });
try {
await authService.logout();
} catch (e) {
// 后端登出失败不阻塞本地登出
}
api.clearToken();
try { wx.removeStorageSync(STORAGE_KEYS.IS_LOGGED_IN); } catch (e) {}
this.setState({
isLoggedIn: false,
userId: null,
phoneBound: false,
authStatus: 'idle',
lastError: null,
});
}
}
const authStore = new AuthStore();
module.exports = authStore;