mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 06:03:49 +08:00
fix(account): route forum login through ipc
This commit is contained in:
@@ -55,6 +55,7 @@ import "./backend/install-manager.js";
|
|||||||
import "./handle-url-scheme.js";
|
import "./handle-url-scheme.js";
|
||||||
|
|
||||||
const logger = pino({ name: "index.ts" });
|
const logger = pino({ name: "index.ts" });
|
||||||
|
const FLARUM_TOKEN_URL = "https://bbs.spark-app.store/api/token";
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
//
|
//
|
||||||
@@ -118,6 +119,52 @@ ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" =>
|
|||||||
|
|
||||||
ipcMain.handle("get-app-version", (): string => getAppVersion());
|
ipcMain.handle("get-app-version", (): string => getAppVersion());
|
||||||
|
|
||||||
|
ipcMain.handle("request-flarum-token", async (_event, payload: unknown) => {
|
||||||
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||||
|
throw new Error("登录信息格式不正确,请重新输入。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = payload as Record<string, unknown>;
|
||||||
|
if (
|
||||||
|
typeof credentials.identification !== "string" ||
|
||||||
|
typeof credentials.password !== "string"
|
||||||
|
) {
|
||||||
|
throw new Error("登录信息格式不正确,请重新输入。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(FLARUM_TOKEN_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
"User-Agent": getUserAgent(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
identification: credentials.identification,
|
||||||
|
password: credentials.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("论坛登录失败,请检查账号和密码。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as Record<string, unknown>;
|
||||||
|
const userId = data.userId ?? data.user_id;
|
||||||
|
if (
|
||||||
|
typeof data.token !== "string" ||
|
||||||
|
userId === undefined ||
|
||||||
|
userId === null
|
||||||
|
) {
|
||||||
|
throw new Error("论坛登录响应异常,请稍后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: data.token,
|
||||||
|
userId: String(userId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const getMainWindowCloseGuardState = (): MainWindowCloseGuardState => ({
|
const getMainWindowCloseGuardState = (): MainWindowCloseGuardState => ({
|
||||||
installTaskCount: tasks.size,
|
installTaskCount: tasks.size,
|
||||||
hasRunningUpdateCenterTasks:
|
hasRunningUpdateCenterTasks:
|
||||||
|
|||||||
+36
-1
@@ -62,7 +62,29 @@
|
|||||||
@select-category="selectSubCategory"
|
@select-category="selectSubCategory"
|
||||||
/>
|
/>
|
||||||
<div class="px-4 py-6 lg:px-10">
|
<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
|
<HomeView
|
||||||
:links="homeLinks"
|
:links="homeLinks"
|
||||||
:lists="homeLists"
|
:lists="homeLists"
|
||||||
@@ -307,6 +329,7 @@ const isDarkTheme = computed(() => {
|
|||||||
const categories: Ref<Record<string, CategoryInfo>> = ref({});
|
const categories: Ref<Record<string, CategoryInfo>> = ref({});
|
||||||
const apps: Ref<App[]> = ref([]);
|
const apps: Ref<App[]> = ref([]);
|
||||||
const activeTab = ref("home");
|
const activeTab = ref("home");
|
||||||
|
const currentView = ref<"default" | "account" | "favorites">("default");
|
||||||
const selectedCategory = ref("all");
|
const selectedCategory = ref("all");
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
const isSidebarOpen = ref(false);
|
const isSidebarOpen = ref(false);
|
||||||
@@ -456,6 +479,7 @@ const toggleTheme = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectTab = (tab: string) => {
|
const selectTab = (tab: string) => {
|
||||||
|
currentView.value = "default";
|
||||||
activeTab.value = tab;
|
activeTab.value = tab;
|
||||||
selectedCategory.value = "all";
|
selectedCategory.value = "all";
|
||||||
isSidebarOpen.value = false;
|
isSidebarOpen.value = false;
|
||||||
@@ -470,6 +494,7 @@ const selectTab = (tab: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectSubCategory = (category: string) => {
|
const selectSubCategory = (category: string) => {
|
||||||
|
currentView.value = "default";
|
||||||
selectedCategory.value = category;
|
selectedCategory.value = category;
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
@@ -532,6 +557,7 @@ const fetchAppFromStore = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openDetail = async (app: App | Record<string, unknown>) => {
|
const openDetail = async (app: App | Record<string, unknown>) => {
|
||||||
|
currentView.value = "default";
|
||||||
// 提取 pkgname 和 category(必须存在)
|
// 提取 pkgname 和 category(必须存在)
|
||||||
const pkgname = (app as Record<string, unknown>).pkgname as string;
|
const pkgname = (app as Record<string, unknown>).pkgname as string;
|
||||||
const category =
|
const category =
|
||||||
@@ -1142,11 +1168,17 @@ const handleFlarumLogin = async (payload: FlarumLoginPayload) => {
|
|||||||
|
|
||||||
const openUserManagement = () => {
|
const openUserManagement = () => {
|
||||||
if (!requireLogin("请登录后查看和管理账号信息。")) return;
|
if (!requireLogin("请登录后查看和管理账号信息。")) return;
|
||||||
|
currentView.value = "account";
|
||||||
|
activeTab.value = "account";
|
||||||
|
isSidebarOpen.value = false;
|
||||||
showLoginPrompt.value = false;
|
showLoginPrompt.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFavoriteManagement = () => {
|
const openFavoriteManagement = () => {
|
||||||
if (!requireLogin("请登录后查看我的收藏。")) return;
|
if (!requireLogin("请登录后查看我的收藏。")) return;
|
||||||
|
currentView.value = "favorites";
|
||||||
|
activeTab.value = "favorites";
|
||||||
|
isSidebarOpen.value = false;
|
||||||
showLoginPrompt.value = false;
|
showLoginPrompt.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1390,10 +1422,12 @@ const loadApps = async (onFirstBatch?: () => void) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchInput = (value: string) => {
|
const handleSearchInput = (value: string) => {
|
||||||
|
currentView.value = "default";
|
||||||
searchQuery.value = value;
|
searchQuery.value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchFocus = () => {
|
const handleSearchFocus = () => {
|
||||||
|
currentView.value = "default";
|
||||||
if (activeTab.value === "home") activeTab.value = "all";
|
if (activeTab.value === "home") activeTab.value = "all";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1517,6 +1551,7 @@ onMounted(async () => {
|
|||||||
// 根据包名直接打开应用详情
|
// 根据包名直接打开应用详情
|
||||||
const tryOpen = () => {
|
const tryOpen = () => {
|
||||||
// 先切换到"全部应用"分类
|
// 先切换到"全部应用"分类
|
||||||
|
currentView.value = "default";
|
||||||
activeTab.value = "all";
|
activeTab.value = "all";
|
||||||
// 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息
|
// 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息
|
||||||
const target = apps.value.find((a) => a.pkgname === data.pkgname);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { FLARUM_BASE_URL } from "@/global/storeConfig";
|
|
||||||
import type { FlarumLoginPayload } from "@/global/typedefinition";
|
import type { FlarumLoginPayload } from "@/global/typedefinition";
|
||||||
|
|
||||||
type FlarumTokenResponse = {
|
type FlarumTokenResponse = {
|
||||||
@@ -18,8 +15,9 @@ const asRecord = (value: unknown): Record<string, unknown> => {
|
|||||||
export const requestFlarumToken = async (
|
export const requestFlarumToken = async (
|
||||||
payload: FlarumLoginPayload,
|
payload: FlarumLoginPayload,
|
||||||
): Promise<FlarumTokenResponse> => {
|
): Promise<FlarumTokenResponse> => {
|
||||||
const response = await axios.post(`${FLARUM_BASE_URL}/api/token`, payload);
|
const data = asRecord(
|
||||||
const data = asRecord(response.data);
|
await window.ipcRenderer.invoke("request-flarum-token", payload),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: String(data.token || ""),
|
token: String(data.token || ""),
|
||||||
|
|||||||
Vendored
+4
@@ -34,6 +34,10 @@ interface IpcRendererFacade {
|
|||||||
// IPC channel type definitions
|
// IPC channel type definitions
|
||||||
declare interface IpcChannels {
|
declare interface IpcChannels {
|
||||||
"get-app-version": () => string;
|
"get-app-version": () => string;
|
||||||
|
"request-flarum-token": (payload: {
|
||||||
|
identification: string;
|
||||||
|
password: string;
|
||||||
|
}) => Promise<{ token: string; userId: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const __APP_VERSION__: string;
|
declare const __APP_VERSION__: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user