fix(auth): tighten refresh session revocation
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
authEntry,
|
||||
bindWechatPhone,
|
||||
changePhoneNumber,
|
||||
changePassword,
|
||||
consumeAuthCallbackResult,
|
||||
getAuthAuditLogs,
|
||||
getAuthLoginOptions,
|
||||
@@ -33,6 +34,8 @@ import {
|
||||
loginWithPhoneCode,
|
||||
logoutAllAuthSessions,
|
||||
redeemRegistrationInviteCode,
|
||||
revokeAuthSession,
|
||||
revokeAuthSessions,
|
||||
sendPhoneLoginCode,
|
||||
startWechatLogin,
|
||||
updateAuthProfile,
|
||||
@@ -154,6 +157,44 @@ describe('authService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('change password clears local auth session after backend success', async () => {
|
||||
window.localStorage.setItem(
|
||||
'genarrative:access-token',
|
||||
'jwt-before-password-change',
|
||||
);
|
||||
apiClientMocks.requestJson.mockResolvedValue({
|
||||
user: {
|
||||
id: 'user_1',
|
||||
publicUserCode: 'SY-00000001',
|
||||
username: 'phone_00000001',
|
||||
displayName: '旅人甲',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await changePassword(' old-password ', ' new-password ');
|
||||
|
||||
expect(user.id).toBe('user_1');
|
||||
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
|
||||
'/api/auth/password/change',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
currentPassword: 'old-password',
|
||||
newPassword: 'new-password',
|
||||
}),
|
||||
}),
|
||||
'修改密码失败',
|
||||
);
|
||||
expect(getStoredAccessToken()).toBe('');
|
||||
expect(apiClientMocks.emitAuthStateChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sends phone login code through the auth endpoint', async () => {
|
||||
apiClientMocks.requestJson.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -475,8 +516,15 @@ describe('authService', () => {
|
||||
sessions: [
|
||||
{
|
||||
sessionId: 'usess_1',
|
||||
sessionIds: ['usess_1', 'usess_2'],
|
||||
sessionCount: 2,
|
||||
clientType: 'browser',
|
||||
clientRuntime: 'chrome',
|
||||
clientPlatform: 'windows',
|
||||
clientLabel: '网页端浏览器',
|
||||
deviceDisplayName: 'Windows / Chrome',
|
||||
miniProgramAppId: null,
|
||||
miniProgramEnv: null,
|
||||
userAgent: 'Mozilla/5.0',
|
||||
ipMasked: '127.0.*.*',
|
||||
isCurrent: true,
|
||||
@@ -490,6 +538,46 @@ describe('authService', () => {
|
||||
const sessions = await getAuthSessions();
|
||||
|
||||
expect(sessions).toHaveLength(1);
|
||||
expect(sessions[0].sessionIds).toEqual(['usess_1', 'usess_2']);
|
||||
expect(sessions[0].sessionCount).toBe(2);
|
||||
});
|
||||
|
||||
it('revokes a single auth session by backend route', async () => {
|
||||
apiClientMocks.requestJson.mockResolvedValue({ ok: true });
|
||||
|
||||
await revokeAuthSession('usess_1');
|
||||
|
||||
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
|
||||
'/api/auth/sessions/usess_1/revoke',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
'移除登录设备失败',
|
||||
);
|
||||
});
|
||||
|
||||
it('revokes grouped auth sessions once per unique session id', async () => {
|
||||
apiClientMocks.requestJson.mockResolvedValue({ ok: true });
|
||||
|
||||
await revokeAuthSessions([' usess_1 ', 'usess_2', 'usess_1', '']);
|
||||
|
||||
expect(apiClientMocks.requestJson).toHaveBeenCalledTimes(2);
|
||||
expect(apiClientMocks.requestJson).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/auth/sessions/usess_1/revoke',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
'移除登录设备失败',
|
||||
);
|
||||
expect(apiClientMocks.requestJson).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/auth/sessions/usess_2/revoke',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
'移除登录设备失败',
|
||||
);
|
||||
});
|
||||
|
||||
it('loads recent auth audit logs', async () => {
|
||||
|
||||
@@ -289,6 +289,7 @@ export async function changePassword(
|
||||
'修改密码失败',
|
||||
);
|
||||
|
||||
clearAuthSession();
|
||||
return response.user;
|
||||
}
|
||||
|
||||
@@ -441,6 +442,16 @@ export async function revokeAuthSession(sessionId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function revokeAuthSessions(sessionIds: string[]) {
|
||||
const uniqueSessionIds = Array.from(
|
||||
new Set(sessionIds.map((sessionId) => sessionId.trim()).filter(Boolean)),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
uniqueSessionIds.map((sessionId) => revokeAuthSession(sessionId)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAuthAuditLogs() {
|
||||
const response = await requestJson<AuthAuditLogsResponse>(
|
||||
'/api/auth/audit-logs',
|
||||
|
||||
Reference in New Issue
Block a user