mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 14:13:49 +08:00
fix(auth): clarify flarum login failures
This commit is contained in:
@@ -14,6 +14,13 @@ dist-electron
|
|||||||
release
|
release
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Local secrets and databases
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
|
||||||
# Test coverage
|
# Test coverage
|
||||||
coverage
|
coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|||||||
+35
-12
@@ -150,20 +150,35 @@ ipcMain.handle("request-flarum-token", async (_event, payload: unknown) => {
|
|||||||
throw new Error("登录信息格式不正确,请重新输入。");
|
throw new Error("登录信息格式不正确,请重新输入。");
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(FLARUM_TOKEN_URL, {
|
logger.info({ endpoint: FLARUM_TOKEN_URL }, "Requesting Flarum login token");
|
||||||
method: "POST",
|
|
||||||
headers: {
|
let response: Response;
|
||||||
"Content-Type": "application/json",
|
try {
|
||||||
Accept: "application/json",
|
response = await fetch(FLARUM_TOKEN_URL, {
|
||||||
"User-Agent": getUserAgent(),
|
method: "POST",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
identification: credentials.identification,
|
Accept: "application/json",
|
||||||
password: credentials.password,
|
"User-Agent": getUserAgent(),
|
||||||
}),
|
},
|
||||||
});
|
body: JSON.stringify({
|
||||||
|
identification: credentials.identification,
|
||||||
|
password: credentials.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
{ err, endpoint: FLARUM_TOKEN_URL },
|
||||||
|
"Flarum token request failed before response",
|
||||||
|
);
|
||||||
|
throw new Error("无法连接星火论坛,请检查网络后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
logger.warn(
|
||||||
|
{ endpoint: FLARUM_TOKEN_URL, status: response.status },
|
||||||
|
"Flarum rejected login token request",
|
||||||
|
);
|
||||||
throw new Error("论坛登录失败,请检查账号和密码。");
|
throw new Error("论坛登录失败,请检查账号和密码。");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +189,14 @@ ipcMain.handle("request-flarum-token", async (_event, payload: unknown) => {
|
|||||||
userId === undefined ||
|
userId === undefined ||
|
||||||
userId === null
|
userId === null
|
||||||
) {
|
) {
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
endpoint: FLARUM_TOKEN_URL,
|
||||||
|
hasToken: typeof data.token === "string" && data.token.length > 0,
|
||||||
|
hasUserId: userId !== undefined && userId !== null,
|
||||||
|
},
|
||||||
|
"Flarum token response missing required fields",
|
||||||
|
);
|
||||||
throw new Error("论坛登录响应异常,请稍后重试。");
|
throw new Error("论坛登录响应异常,请稍后重试。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { exchangeFlarumToken } from "@/modules/backendApi";
|
||||||
|
|
||||||
|
const axiosMocks = vi.hoisted(() => {
|
||||||
|
const post = vi.fn();
|
||||||
|
return {
|
||||||
|
instance: {
|
||||||
|
defaults: { headers: { common: {} as Record<string, unknown> } },
|
||||||
|
get: vi.fn(),
|
||||||
|
post,
|
||||||
|
},
|
||||||
|
post,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const loggerMocks = vi.hoisted(() => ({
|
||||||
|
error: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("axios", () => ({
|
||||||
|
default: {
|
||||||
|
create: vi.fn(() => axiosMocks.instance),
|
||||||
|
isAxiosError: (error: unknown) =>
|
||||||
|
Boolean((error as { isAxiosError?: boolean }).isAxiosError),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("pino", () => ({
|
||||||
|
default: () => loggerMocks,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("backend API auth exchange", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(axios.create).mockClear();
|
||||||
|
axiosMocks.post.mockReset();
|
||||||
|
loggerMocks.error.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps backend connection failures to a user-actionable login error", async () => {
|
||||||
|
const error = Object.assign(new Error("Network Error"), {
|
||||||
|
code: "ERR_NETWORK",
|
||||||
|
isAxiosError: true,
|
||||||
|
request: {},
|
||||||
|
});
|
||||||
|
axiosMocks.post.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
exchangeFlarumToken({ flarumUserId: "42", flarumToken: "forum-token" }),
|
||||||
|
).rejects.toThrow("无法连接星火账号服务,请确认后端服务已启动或稍后重试。");
|
||||||
|
expect(loggerMocks.error).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
code: "ERR_NETWORK",
|
||||||
|
message: "Network Error",
|
||||||
|
status: undefined,
|
||||||
|
},
|
||||||
|
"Spark backend auth exchange failed",
|
||||||
|
);
|
||||||
|
expect(JSON.stringify(loggerMocks.error.mock.calls)).not.toContain("forum-token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps backend server failures to an update-required login error", async () => {
|
||||||
|
const error = Object.assign(new Error("Request failed with status code 500"), {
|
||||||
|
isAxiosError: true,
|
||||||
|
response: { status: 500 },
|
||||||
|
});
|
||||||
|
axiosMocks.post.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
exchangeFlarumToken({ flarumUserId: "42", flarumToken: "forum-token" }),
|
||||||
|
).rejects.toThrow("星火账号服务异常,请确认后端数据库迁移已执行后重试。");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -32,4 +32,29 @@ describe("requestFlarumToken", () => {
|
|||||||
expect(axios.post).not.toHaveBeenCalled();
|
expect(axios.post).not.toHaveBeenCalled();
|
||||||
expect(token).toEqual({ token: "forum-token", userId: "42" });
|
expect(token).toEqual({ token: "forum-token", userId: "42" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects malformed token responses from main-process IPC", async () => {
|
||||||
|
vi.mocked(window.ipcRenderer.invoke).mockResolvedValue({
|
||||||
|
token: "",
|
||||||
|
user_id: 42,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
requestFlarumToken({ identification: "momen", password: "secret" }),
|
||||||
|
).rejects.toThrow("论坛登录响应异常,请稍后重试。");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips Electron IPC wrapper text from known login errors", async () => {
|
||||||
|
vi.mocked(window.ipcRenderer.invoke).mockRejectedValue(
|
||||||
|
new Error(
|
||||||
|
"Error invoking remote method 'request-flarum-token': Error: 无法连接星火论坛,请检查网络后重试。",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
requestFlarumToken({ identification: "momen", password: "secret" }),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
message: "无法连接星火论坛,请检查网络后重试。",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios, { type AxiosResponse } from "axios";
|
||||||
|
import pino from "pino";
|
||||||
|
|
||||||
import { SPARK_BACKEND_BASE_URL } from "@/global/storeConfig";
|
import { SPARK_BACKEND_BASE_URL } from "@/global/storeConfig";
|
||||||
import type {
|
import type {
|
||||||
@@ -19,9 +20,42 @@ const backend = axios.create({
|
|||||||
baseURL: SPARK_BACKEND_BASE_URL,
|
baseURL: SPARK_BACKEND_BASE_URL,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
const logger = pino({ name: "backendApi" });
|
||||||
|
|
||||||
type ApiRecord = Record<string, unknown>;
|
type ApiRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
const normalizeBackendAuthError = (error: unknown): Error => {
|
||||||
|
if (!axios.isAxiosError(error)) {
|
||||||
|
return error instanceof Error ? error : new Error("登录失败,请稍后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
{
|
||||||
|
code: error.code,
|
||||||
|
message: error.message,
|
||||||
|
status: error.response?.status,
|
||||||
|
},
|
||||||
|
"Spark backend auth exchange failed",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!error.response) {
|
||||||
|
return new Error("无法连接星火账号服务,请确认后端服务已启动或稍后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = error.response.status;
|
||||||
|
if (status === 401) {
|
||||||
|
return new Error("论坛登录失败,请检查账号和密码。");
|
||||||
|
}
|
||||||
|
if (status === 503) {
|
||||||
|
return new Error("星火账号服务暂时无法连接论坛,请稍后重试。");
|
||||||
|
}
|
||||||
|
if (status === 500) {
|
||||||
|
return new Error("星火账号服务异常,请确认后端数据库迁移已执行后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error(`星火账号服务返回异常 (${status}),请稍后重试。`);
|
||||||
|
};
|
||||||
|
|
||||||
const asApiRecord = (value: unknown): ApiRecord => {
|
const asApiRecord = (value: unknown): ApiRecord => {
|
||||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
return value as ApiRecord;
|
return value as ApiRecord;
|
||||||
@@ -145,10 +179,15 @@ export const exchangeFlarumToken = async (payload: {
|
|||||||
flarumUserId: string;
|
flarumUserId: string;
|
||||||
flarumToken: string;
|
flarumToken: string;
|
||||||
}): Promise<AuthSession> => {
|
}): Promise<AuthSession> => {
|
||||||
const response = await backend.post("/auth/flarum", {
|
let response: AxiosResponse;
|
||||||
flarum_user_id: payload.flarumUserId,
|
try {
|
||||||
flarum_token: payload.flarumToken,
|
response = await backend.post("/auth/flarum", {
|
||||||
});
|
flarum_user_id: payload.flarumUserId,
|
||||||
|
flarum_token: payload.flarumToken,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw normalizeBackendAuthError(error);
|
||||||
|
}
|
||||||
const data = asApiRecord(response.data);
|
const data = asApiRecord(response.data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,15 +12,49 @@ const asRecord = (value: unknown): Record<string, unknown> => {
|
|||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const knownLoginErrorMessages = [
|
||||||
|
"无法连接星火论坛,请检查网络后重试。",
|
||||||
|
"论坛登录失败,请检查账号和密码。",
|
||||||
|
"论坛登录响应异常,请稍后重试。",
|
||||||
|
"登录信息格式不正确,请重新输入。",
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeIpcError = (error: unknown): Error => {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return new Error("登录失败,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownMessage = knownLoginErrorMessages.find((message) =>
|
||||||
|
error.message.includes(message),
|
||||||
|
);
|
||||||
|
return knownMessage ? new Error(knownMessage) : error;
|
||||||
|
};
|
||||||
|
|
||||||
export const requestFlarumToken = async (
|
export const requestFlarumToken = async (
|
||||||
payload: FlarumLoginPayload,
|
payload: FlarumLoginPayload,
|
||||||
): Promise<FlarumTokenResponse> => {
|
): Promise<FlarumTokenResponse> => {
|
||||||
const data = asRecord(
|
let data: Record<string, unknown>;
|
||||||
await window.ipcRenderer.invoke("request-flarum-token", payload),
|
try {
|
||||||
);
|
data = asRecord(
|
||||||
|
await window.ipcRenderer.invoke("request-flarum-token", payload),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw normalizeIpcError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = data.token;
|
||||||
|
const userId = data.userId ?? data.user_id;
|
||||||
|
if (
|
||||||
|
typeof token !== "string" ||
|
||||||
|
!token ||
|
||||||
|
userId === undefined ||
|
||||||
|
userId === null
|
||||||
|
) {
|
||||||
|
throw new Error("论坛登录响应异常,请稍后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: String(data.token || ""),
|
token,
|
||||||
userId: String(data.userId || data.user_id || ""),
|
userId: String(userId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user