feat: 安全设置展示有效登录会话,支持剔除其它设备

按设备指纹去重并直出 IP;非当前会话可踢下线,对方需重新输入密码。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 01:48:52 +08:00
parent 2ccfb5f330
commit 3a7bfe59df
16 changed files with 808 additions and 28 deletions

View File

@@ -1346,6 +1346,47 @@ export async function apiChangePassword(oldPassword: string, newPassword: string
return res.json();
}
/** 当前用户仍有效的登录会话(安全设置 SSR / 客户端) */
export interface LoginDevice {
id: number;
ip: string;
device_kind: "computer" | "mobile" | "tablet";
device_label: string;
browser: string;
current: boolean;
created_at: string;
}
export interface LoginDevicesResponse {
devices: LoginDevice[];
}
export async function fetchLoginDevices(
cookieHeader?: string,
viewer?: { userAgent?: string; clientIP?: string }
): Promise<LoginDevice[]> {
const headers: Record<string, string> = {};
if (cookieHeader) headers.Cookie = cookieHeader;
if (viewer?.userAgent) headers["X-J13-Client-UA"] = viewer.userAgent;
if (viewer?.clientIP) headers["X-J13-Client-IP"] = viewer.clientIP;
const res = await fetch(`${API_BASE}/api/me/login-devices`, ssrInit(headers));
if (res.status === 401) return [];
if (!res.ok) throw new Error("获取登录设备失败");
const data = (await res.json()) as LoginDevicesResponse;
return data.devices ?? [];
}
/** 剔除其它登录设备,该设备需重新输入密码登录 */
export async function apiRevokeLoginDevice(
id: number
): Promise<{ ok?: boolean; error?: string }> {
const res = await fetchWithRefresh(`/api/me/login-devices/${id}`, {
method: "DELETE",
headers: clientHeaders(),
});
return res.json();
}
// 更新当前用户资料(昵称、头像、邮箱、签名)
export async function apiUpdateProfile(data: {
nickname: string;