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 后,异步尝试写入每日登录埋点。
|
在用户认证成功并创建 refresh session / access token 后,异步尝试写入每日登录埋点。
|
||||||
@@ -58,6 +66,7 @@ record_daily_login_tracking_event_after_auth_success(
|
|||||||
- `cargo check -p spacetime-client`
|
- `cargo check -p spacetime-client`
|
||||||
- `npm run check:encoding`
|
- `npm run check:encoding`
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
|
- `npm run test -- AuthGate.test.tsx`
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ pub fn get_profile_task_center(
|
|||||||
ctx: &mut ProcedureContext,
|
ctx: &mut ProcedureContext,
|
||||||
input: RuntimeProfileTaskCenterGetInput,
|
input: RuntimeProfileTaskCenterGetInput,
|
||||||
) -> RuntimeProfileTaskCenterProcedureResult {
|
) -> 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(record) => RuntimeProfileTaskCenterProcedureResult {
|
||||||
ok: true,
|
ok: true,
|
||||||
record: Some(record),
|
record: Some(record),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const authMocks = vi.hoisted(() => ({
|
|||||||
authEntry: vi.fn(),
|
authEntry: vi.fn(),
|
||||||
changePassword: vi.fn(),
|
changePassword: vi.fn(),
|
||||||
ensureStoredAccessToken: vi.fn(),
|
ensureStoredAccessToken: vi.fn(),
|
||||||
|
refreshStoredAccessToken: vi.fn(),
|
||||||
getAuthLoginOptions: vi.fn(),
|
getAuthLoginOptions: vi.fn(),
|
||||||
getCurrentAuthUser: vi.fn(),
|
getCurrentAuthUser: vi.fn(),
|
||||||
loginWithPhoneCode: vi.fn(),
|
loginWithPhoneCode: vi.fn(),
|
||||||
@@ -28,6 +29,7 @@ const authMocks = vi.hoisted(() => ({
|
|||||||
vi.mock('../../services/apiClient', () => ({
|
vi.mock('../../services/apiClient', () => ({
|
||||||
AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
|
AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
|
||||||
ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
|
ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
|
||||||
|
refreshStoredAccessToken: authMocks.refreshStoredAccessToken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../services/authService', () => ({
|
vi.mock('../../services/authService', () => ({
|
||||||
@@ -94,6 +96,7 @@ beforeEach(() => {
|
|||||||
window.history.replaceState(null, '', '/');
|
window.history.replaceState(null, '', '/');
|
||||||
authMocks.consumeAuthCallbackResult.mockReturnValue(null);
|
authMocks.consumeAuthCallbackResult.mockReturnValue(null);
|
||||||
authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token');
|
authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token');
|
||||||
|
authMocks.refreshStoredAccessToken.mockResolvedValue('jwt-refreshed-token');
|
||||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||||
user: null,
|
user: null,
|
||||||
availableLoginMethods: ['phone'],
|
availableLoginMethods: ['phone'],
|
||||||
@@ -204,12 +207,12 @@ test('auth gate keeps platform content visible when phone login is available', a
|
|||||||
expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull();
|
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;
|
let resolveToken!: (token: string) => void;
|
||||||
const tokenPromise = new Promise<string>((resolve) => {
|
const tokenPromise = new Promise<string>((resolve) => {
|
||||||
resolveToken = resolve;
|
resolveToken = resolve;
|
||||||
});
|
});
|
||||||
authMocks.ensureStoredAccessToken.mockReturnValue(tokenPromise);
|
authMocks.refreshStoredAccessToken.mockReturnValue(tokenPromise);
|
||||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
availableLoginMethods: ['phone'],
|
availableLoginMethods: ['phone'],
|
||||||
@@ -224,10 +227,11 @@ test('auth gate waits for access token refresh before exposing restored user con
|
|||||||
expect(screen.getByText('正在校验登录状态...')).toBeTruthy();
|
expect(screen.getByText('正在校验登录状态...')).toBeTruthy();
|
||||||
expect(authMocks.getCurrentAuthUser).not.toHaveBeenCalled();
|
expect(authMocks.getCurrentAuthUser).not.toHaveBeenCalled();
|
||||||
|
|
||||||
resolveToken('jwt-restored-token');
|
resolveToken('jwt-refreshed-token');
|
||||||
|
|
||||||
expect(await screen.findByText('应用内容')).toBeTruthy();
|
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);
|
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) => {
|
const tokenPromise = new Promise<string>((resolve) => {
|
||||||
resolveToken = resolve;
|
resolveToken = resolve;
|
||||||
});
|
});
|
||||||
authMocks.ensureStoredAccessToken.mockReturnValueOnce(tokenPromise);
|
authMocks.refreshStoredAccessToken.mockReturnValueOnce(tokenPromise);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
window.dispatchEvent(new Event('genarrative-auth-state-changed'));
|
window.dispatchEvent(new Event('genarrative-auth-state-changed'));
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { useGameSettings } from '../../hooks/useGameSettings';
|
import { useGameSettings } from '../../hooks/useGameSettings';
|
||||||
import {
|
import {
|
||||||
AUTH_STATE_EVENT,
|
AUTH_STATE_EVENT,
|
||||||
ensureStoredAccessToken,
|
refreshStoredAccessToken,
|
||||||
} from '../../services/apiClient';
|
} from '../../services/apiClient';
|
||||||
import {
|
import {
|
||||||
type AuthAuditLogEntry,
|
type AuthAuditLogEntry,
|
||||||
@@ -311,7 +311,10 @@ export function AuthGate({ children }: AuthGateProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureStoredAccessToken();
|
// 中文注释:打开已登录页面也要主动轮换 refresh cookie。
|
||||||
|
// 后端只在 refresh/session 成功续期时写每日登录埋点;如果本地 access token 尚未过期,
|
||||||
|
// 仅调用 /auth/me 不会进入续期链路,导致“打开网页”没有登录埋点。
|
||||||
|
await refreshStoredAccessToken();
|
||||||
const nextSession = await getCurrentAuthUser();
|
const nextSession = await getCurrentAuthUser();
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -513,6 +513,10 @@ export async function ensureStoredAccessToken() {
|
|||||||
return refreshAccessToken();
|
return refreshAccessToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function refreshStoredAccessToken() {
|
||||||
|
return refreshAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchWithApiAuth(
|
export async function fetchWithApiAuth(
|
||||||
input: string,
|
input: string,
|
||||||
init: RequestInit = {},
|
init: RequestInit = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user