fix: trigger login tracking on session restore
This commit is contained in:
@@ -9,6 +9,14 @@
|
||||
|
||||
但认证成功链路还没有调用该方法,因此当前只完成了“任务中心读取不污染登录埋点”,没有完成“用户真实登录写入每日登录埋点”。
|
||||
|
||||
## 现象
|
||||
|
||||
用户已经登录、cookie 未过期时,直接打开网页并不会触发每日登录埋点。原因是前端恢复登录态只读取 `/api/auth/me`,这条链路不会主动走 refresh cookie 续期,因此后端新的埋点写入点不会被触发。
|
||||
|
||||
## 修复思路
|
||||
|
||||
在 `AuthGate` 恢复已登录会话时,先主动调用一次 refresh 接口轮换 refresh cookie,再调用 `/api/auth/me` 读取当前会话。这样无论本地 access token 是否仍然有效,打开页面都会进入 refresh 续期链路,从而触发后端的 `daily_login` 埋点写入。
|
||||
|
||||
## 目标
|
||||
|
||||
在用户认证成功并创建 refresh session / access token 后,异步尝试写入每日登录埋点。
|
||||
@@ -58,6 +66,7 @@ record_daily_login_tracking_event_after_auth_success(
|
||||
- `cargo check -p spacetime-client`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
- `npm run test -- AuthGate.test.tsx`
|
||||
|
||||
## 注意
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ pub fn get_profile_task_center(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileTaskCenterGetInput,
|
||||
) -> RuntimeProfileTaskCenterProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_profile_task_center_snapshot(tx, input.clone(), true)) {
|
||||
match ctx.try_with_tx(|tx| get_profile_task_center_snapshot(tx, input.clone(), false)) {
|
||||
Ok(record) => RuntimeProfileTaskCenterProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
|
||||
@@ -13,6 +13,7 @@ const authMocks = vi.hoisted(() => ({
|
||||
authEntry: vi.fn(),
|
||||
changePassword: vi.fn(),
|
||||
ensureStoredAccessToken: vi.fn(),
|
||||
refreshStoredAccessToken: vi.fn(),
|
||||
getAuthLoginOptions: vi.fn(),
|
||||
getCurrentAuthUser: vi.fn(),
|
||||
loginWithPhoneCode: vi.fn(),
|
||||
@@ -28,6 +29,7 @@ const authMocks = vi.hoisted(() => ({
|
||||
vi.mock('../../services/apiClient', () => ({
|
||||
AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
|
||||
ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
|
||||
refreshStoredAccessToken: authMocks.refreshStoredAccessToken,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/authService', () => ({
|
||||
@@ -94,6 +96,7 @@ beforeEach(() => {
|
||||
window.history.replaceState(null, '', '/');
|
||||
authMocks.consumeAuthCallbackResult.mockReturnValue(null);
|
||||
authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token');
|
||||
authMocks.refreshStoredAccessToken.mockResolvedValue('jwt-refreshed-token');
|
||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||
user: null,
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -204,12 +207,12 @@ test('auth gate keeps platform content visible when phone login is available', a
|
||||
expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull();
|
||||
});
|
||||
|
||||
test('auth gate waits for access token refresh before exposing restored user content', async () => {
|
||||
test('auth gate waits for refresh cookie rotation before exposing restored user content', async () => {
|
||||
let resolveToken!: (token: string) => void;
|
||||
const tokenPromise = new Promise<string>((resolve) => {
|
||||
resolveToken = resolve;
|
||||
});
|
||||
authMocks.ensureStoredAccessToken.mockReturnValue(tokenPromise);
|
||||
authMocks.refreshStoredAccessToken.mockReturnValue(tokenPromise);
|
||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||
user: mockUser,
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -224,10 +227,11 @@ test('auth gate waits for access token refresh before exposing restored user con
|
||||
expect(screen.getByText('正在校验登录状态...')).toBeTruthy();
|
||||
expect(authMocks.getCurrentAuthUser).not.toHaveBeenCalled();
|
||||
|
||||
resolveToken('jwt-restored-token');
|
||||
resolveToken('jwt-refreshed-token');
|
||||
|
||||
expect(await screen.findByText('应用内容')).toBeTruthy();
|
||||
expect(authMocks.ensureStoredAccessToken).toHaveBeenCalledTimes(1);
|
||||
expect(authMocks.refreshStoredAccessToken).toHaveBeenCalledTimes(1);
|
||||
expect(authMocks.ensureStoredAccessToken).not.toHaveBeenCalled();
|
||||
expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -440,7 +444,7 @@ test('auth state refresh keeps mounted platform content and local tab state', as
|
||||
const tokenPromise = new Promise<string>((resolve) => {
|
||||
resolveToken = resolve;
|
||||
});
|
||||
authMocks.ensureStoredAccessToken.mockReturnValueOnce(tokenPromise);
|
||||
authMocks.refreshStoredAccessToken.mockReturnValueOnce(tokenPromise);
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event('genarrative-auth-state-changed'));
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { useGameSettings } from '../../hooks/useGameSettings';
|
||||
import {
|
||||
AUTH_STATE_EVENT,
|
||||
ensureStoredAccessToken,
|
||||
refreshStoredAccessToken,
|
||||
} from '../../services/apiClient';
|
||||
import {
|
||||
type AuthAuditLogEntry,
|
||||
@@ -311,7 +311,10 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureStoredAccessToken();
|
||||
// 中文注释:打开已登录页面也要主动轮换 refresh cookie。
|
||||
// 后端只在 refresh/session 成功续期时写每日登录埋点;如果本地 access token 尚未过期,
|
||||
// 仅调用 /auth/me 不会进入续期链路,导致“打开网页”没有登录埋点。
|
||||
await refreshStoredAccessToken();
|
||||
const nextSession = await getCurrentAuthUser();
|
||||
if (!isActive) {
|
||||
return;
|
||||
|
||||
@@ -513,6 +513,10 @@ export async function ensureStoredAccessToken() {
|
||||
return refreshAccessToken();
|
||||
}
|
||||
|
||||
export async function refreshStoredAccessToken() {
|
||||
return refreshAccessToken();
|
||||
}
|
||||
|
||||
export async function fetchWithApiAuth(
|
||||
input: string,
|
||||
init: RequestInit = {},
|
||||
|
||||
Reference in New Issue
Block a user