fix(account): route forum login through ipc

This commit is contained in:
2026-05-18 22:55:21 +08:00
parent 62081fb0ad
commit c2e8b9a1b4
6 changed files with 242 additions and 6 deletions
+36 -1
View File
@@ -62,7 +62,29 @@
@select-category="selectSubCategory"
/>
<div class="px-4 py-6 lg:px-10">
<template v-if="activeTab === 'home'">
<section
v-if="currentView === 'account'"
class="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">
用户管理
</h1>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
账号资料与安全设置功能即将开放
</p>
</section>
<section
v-else-if="currentView === 'favorites'"
class="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">
我的收藏
</h1>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
收藏应用列表功能即将开放
</p>
</section>
<template v-else-if="activeTab === 'home'">
<HomeView
:links="homeLinks"
:lists="homeLists"
@@ -307,6 +329,7 @@ const isDarkTheme = computed(() => {
const categories: Ref<Record<string, CategoryInfo>> = ref({});
const apps: Ref<App[]> = ref([]);
const activeTab = ref("home");
const currentView = ref<"default" | "account" | "favorites">("default");
const selectedCategory = ref("all");
const searchQuery = ref("");
const isSidebarOpen = ref(false);
@@ -456,6 +479,7 @@ const toggleTheme = () => {
};
const selectTab = (tab: string) => {
currentView.value = "default";
activeTab.value = tab;
selectedCategory.value = "all";
isSidebarOpen.value = false;
@@ -470,6 +494,7 @@ const selectTab = (tab: string) => {
};
const selectSubCategory = (category: string) => {
currentView.value = "default";
selectedCategory.value = category;
window.scrollTo({ top: 0, behavior: "smooth" });
};
@@ -532,6 +557,7 @@ const fetchAppFromStore = async (
};
const openDetail = async (app: App | Record<string, unknown>) => {
currentView.value = "default";
// 提取 pkgname 和 category(必须存在)
const pkgname = (app as Record<string, unknown>).pkgname as string;
const category =
@@ -1142,11 +1168,17 @@ const handleFlarumLogin = async (payload: FlarumLoginPayload) => {
const openUserManagement = () => {
if (!requireLogin("请登录后查看和管理账号信息。")) return;
currentView.value = "account";
activeTab.value = "account";
isSidebarOpen.value = false;
showLoginPrompt.value = false;
};
const openFavoriteManagement = () => {
if (!requireLogin("请登录后查看我的收藏。")) return;
currentView.value = "favorites";
activeTab.value = "favorites";
isSidebarOpen.value = false;
showLoginPrompt.value = false;
};
@@ -1390,10 +1422,12 @@ const loadApps = async (onFirstBatch?: () => void) => {
};
const handleSearchInput = (value: string) => {
currentView.value = "default";
searchQuery.value = value;
};
const handleSearchFocus = () => {
currentView.value = "default";
if (activeTab.value === "home") activeTab.value = "all";
};
@@ -1517,6 +1551,7 @@ onMounted(async () => {
// 根据包名直接打开应用详情
const tryOpen = () => {
// 先切换到"全部应用"分类
currentView.value = "default";
activeTab.value = "all";
// 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息
const target = apps.value.find((a) => a.pkgname === data.pkgname);
@@ -0,0 +1,117 @@
import { fireEvent, render, screen } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue";
import { setAuthSession } from "@/global/authState";
const invoke = vi.fn();
vi.mock("axios", () => {
const get = vi.fn(async (url: string) => {
if (url.includes("categories.json")) return { data: {} };
return { data: [] };
});
return {
default: {
create: () => ({ get }),
},
};
});
vi.mock("@/modules/updateCenter", () => ({
createUpdateCenterStore: () => ({
isOpen: { value: false },
showCloseConfirm: { value: false },
showMigrationConfirm: { value: false },
searchQuery: { value: "" },
selectedTaskKeys: { value: new Set<string>() },
snapshot: {
value: { items: [], tasks: [], warnings: [], hasRunningTasks: false },
},
filteredItems: { value: [] },
allSelected: { value: false },
someSelected: { value: false },
bind: vi.fn(),
unbind: vi.fn(),
open: vi.fn(),
refresh: vi.fn(),
ignoreItem: vi.fn(),
unignoreItem: vi.fn(),
toggleSelection: vi.fn(),
toggleSelectAll: vi.fn(),
getSelectedItems: vi.fn(() => []),
closeNow: vi.fn(),
startSelected: vi.fn(),
requestClose: vi.fn(),
}),
}));
describe("App account placeholders", () => {
beforeEach(() => {
invoke.mockReset();
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
return [];
});
Object.assign(window.ipcRenderer, {
invoke,
on: vi.fn(),
off: vi.fn(),
send: vi.fn(),
});
window.apm_store.arch = "amd64";
localStorage.clear();
setAuthSession({
accessToken: "backend-token",
tokenType: "bearer",
user: {
id: 1,
flarumUserId: "42",
username: "momen",
displayName: "Momen",
avatarUrl: "https://bbs.spark-app.store/avatar.png",
forumLevel: "管理员",
forumGroups: ["管理员"],
},
});
vi.stubGlobal(
"matchMedia",
vi.fn(() => ({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
);
});
it("shows the user management placeholder from the logged-in quick menu", async () => {
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
expect(
await screen.findByRole("heading", { name: "用户管理" }),
).toBeTruthy();
expect(screen.queryByText("请登录后查看和管理账号信息。")).toBeNull();
});
it("shows the favorites placeholder from the logged-in quick menu", async () => {
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(screen.queryByText("请登录后查看我的收藏。")).toBeNull();
});
});
+35
View File
@@ -0,0 +1,35 @@
import axios from "axios";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { requestFlarumToken } from "@/modules/flarumAuth";
vi.mock("axios", () => ({
default: {
post: vi.fn(),
},
}));
describe("requestFlarumToken", () => {
beforeEach(() => {
vi.mocked(window.ipcRenderer.invoke).mockReset();
vi.mocked(axios.post).mockReset();
});
it("requests the Flarum token through main-process IPC", async () => {
vi.mocked(window.ipcRenderer.invoke).mockResolvedValue({
token: "forum-token",
user_id: 42,
});
const payload = { identification: "user@example.com", password: "secret" };
const token = await requestFlarumToken(payload);
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith(
"request-flarum-token",
payload,
);
expect(axios.post).not.toHaveBeenCalled();
expect(token).toEqual({ token: "forum-token", userId: "42" });
});
});
+3 -5
View File
@@ -1,6 +1,3 @@
import axios from "axios";
import { FLARUM_BASE_URL } from "@/global/storeConfig";
import type { FlarumLoginPayload } from "@/global/typedefinition";
type FlarumTokenResponse = {
@@ -18,8 +15,9 @@ const asRecord = (value: unknown): Record<string, unknown> => {
export const requestFlarumToken = async (
payload: FlarumLoginPayload,
): Promise<FlarumTokenResponse> => {
const response = await axios.post(`${FLARUM_BASE_URL}/api/token`, payload);
const data = asRecord(response.data);
const data = asRecord(
await window.ipcRenderer.invoke("request-flarum-token", payload),
);
return {
token: String(data.token || ""),
+4
View File
@@ -34,6 +34,10 @@ interface IpcRendererFacade {
// IPC channel type definitions
declare interface IpcChannels {
"get-app-version": () => string;
"request-flarum-token": (payload: {
identification: string;
password: string;
}) => Promise<{ token: string; userId: string }>;
}
declare const __APP_VERSION__: string;