Skip to content

Commit cf1a6e8

Browse files
committed
feat: 添加基于NodeBB的用户系统
1 parent d6bca8b commit cf1a6e8

File tree

3 files changed

+166
-10
lines changed

3 files changed

+166
-10
lines changed

app/src/core/service/GlobalMenu.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Button } from "@/components/ui/button";
2+
import { Dialog } from "@/components/ui/dialog";
13
import {
24
Menubar,
35
MenubarContent,
@@ -9,8 +11,6 @@ import {
911
MenubarSubTrigger,
1012
MenubarTrigger,
1113
} from "@/components/ui/menubar";
12-
import { Button } from "@/components/ui/button";
13-
import { Dialog } from "@/components/ui/dialog";
1414

1515
import { Input } from "@/components/ui/input";
1616
import { Textarea } from "@/components/ui/textarea";
@@ -20,10 +20,18 @@ import { activeProjectAtom, isClassroomModeAtom, projectsAtom, store } from "@/s
2020
import AIWindow from "@/sub/AIWindow";
2121
import AttachmentsWindow from "@/sub/AttachmentsWindow";
2222
import ExportPngWindow from "@/sub/ExportPngWindow";
23+
import FindWindow from "@/sub/FindWindow";
24+
import LoginWindow from "@/sub/LoginWindow";
2325
import NodeDetailsWindow from "@/sub/NodeDetailsWindow";
26+
import RecentFilesWindow from "@/sub/RecentFilesWindow";
2427
import SettingsWindow from "@/sub/SettingsWindow";
28+
import TestWindow from "@/sub/TestWindow";
29+
import UserWindow from "@/sub/UserWindow";
2530
import { getDeviceId } from "@/utils/otherApi";
31+
import { PathString } from "@/utils/pathString";
32+
import { Color, Vector } from "@graphif/data-structures";
2633
import { deserialize, serialize } from "@graphif/serializer";
34+
import { Rectangle } from "@graphif/shapes";
2735
import { Decoder } from "@msgpack/msgpack";
2836
import { getVersion } from "@tauri-apps/api/app";
2937
import { appCacheDir, dataDir, join } from "@tauri-apps/api/path";
@@ -90,15 +98,9 @@ import { LineEdge } from "../stage/stageObject/association/LineEdge";
9098
import { TextNode } from "../stage/stageObject/entity/TextNode";
9199
import { RecentFileManager } from "./dataFileService/RecentFileManager";
92100
import { FeatureFlags } from "./FeatureFlags";
93-
import { Telemetry } from "./Telemetry";
94-
import { SubWindow } from "./SubWindow";
95-
import { Rectangle } from "@graphif/shapes";
96-
import { Color, Vector } from "@graphif/data-structures";
97-
import FindWindow from "@/sub/FindWindow";
98101
import { Settings } from "./Settings";
99-
import TestWindow from "@/sub/TestWindow";
100-
import { PathString } from "@/utils/pathString";
101-
import RecentFilesWindow from "@/sub/RecentFilesWindow";
102+
import { SubWindow } from "./SubWindow";
103+
import { Telemetry } from "./Telemetry";
102104

103105
const Content = MenubarContent;
104106
const Item = MenubarItem;
@@ -1005,6 +1007,8 @@ export function GlobalMenu() {
10051007
>
10061008
输出选中节点的详细信息
10071009
</Item>
1010+
<Item onClick={() => LoginWindow.open()}>login</Item>
1011+
<Item onClick={() => UserWindow.open()}>user</Item>
10081012
</SubContent>
10091013
</Sub>
10101014
</Content>

app/src/sub/LoginWindow.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Button } from "@/components/ui/button";
2+
import { Input } from "@/components/ui/input";
3+
import { SubWindow } from "@/core/service/SubWindow";
4+
import { Vector } from "@graphif/data-structures";
5+
import { Rectangle } from "@graphif/shapes";
6+
import { fetch } from "@tauri-apps/plugin-http";
7+
import { open } from "@tauri-apps/plugin-shell";
8+
import { Check, ExternalLink, KeyRound, User } from "lucide-react";
9+
import { useState } from "react";
10+
import { toast } from "sonner";
11+
12+
export default function LoginWindow() {
13+
const [username, setUsername] = useState("");
14+
const [password, setPassword] = useState("");
15+
16+
async function login() {
17+
// 获取 CSRF Token
18+
const loginPageHtml = await (await fetch("https://bbs.project-graph.top/login")).text();
19+
const csrfTokenMatch = loginPageHtml.match(/"csrf_token":"([a-z0-9]+)"/);
20+
if (!csrfTokenMatch) {
21+
throw new Error("获取 CSRF Token 失败");
22+
}
23+
const csrfToken = csrfTokenMatch[1];
24+
// 发送登录请求,获取 Cookie
25+
const response = await fetch("https://bbs.project-graph.top/login", {
26+
method: "POST",
27+
headers: {
28+
"Content-Type": "application/x-www-form-urlencoded",
29+
},
30+
body: new URLSearchParams({
31+
username: username,
32+
password: password,
33+
noscript: "false",
34+
remember: "on",
35+
_csrf: csrfToken,
36+
}).toString(),
37+
});
38+
if (response.status !== 200) {
39+
const data = await response.text();
40+
if (data === "Forbidden") {
41+
throw new Error("CSRF Token 不正确");
42+
} else if (data === "[[error:invalid-login-credentials]]") {
43+
throw new Error("无效的登录凭证");
44+
}
45+
return;
46+
}
47+
}
48+
49+
return (
50+
<div className="flex flex-col items-center gap-4 p-4">
51+
<span className="flex items-center gap-4">
52+
<User />
53+
<Input placeholder="邮箱 / 用户名" value={username} onChange={(e) => setUsername(e.target.value)} />
54+
</span>
55+
<span className="flex items-center gap-4">
56+
<KeyRound />
57+
<Input placeholder="密码" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
58+
</span>
59+
<span className="flex gap-4">
60+
<Button
61+
onClick={() =>
62+
toast.promise(login, {
63+
loading: "正在登录...",
64+
success: "登录成功",
65+
error: (err) => `登录失败: ${err.message}`,
66+
})
67+
}
68+
>
69+
<Check />
70+
登录
71+
</Button>
72+
<Button variant="outline" onClick={() => open("https://bbs.project-graph.top/register")}>
73+
<ExternalLink />
74+
前往注册
75+
</Button>
76+
</span>
77+
</div>
78+
);
79+
}
80+
81+
LoginWindow.open = () => {
82+
SubWindow.create({
83+
title: "登录",
84+
children: <LoginWindow />,
85+
rect: Rectangle.inCenter(new Vector(230, 240)),
86+
});
87+
};

app/src/sub/UserWindow.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Button } from "@/components/ui/button";
2+
import { SubWindow } from "@/core/service/SubWindow";
3+
import { Vector } from "@graphif/data-structures";
4+
import { Rectangle } from "@graphif/shapes";
5+
import { fetch } from "@tauri-apps/plugin-http";
6+
import { LogOut } from "lucide-react";
7+
import { useEffect, useState } from "react";
8+
import { toast } from "sonner";
9+
10+
export default function UserWindow() {
11+
const [status, setStatus] = useState<"loading" | "ok" | "out">("loading");
12+
const [user, setUser] = useState<any>({});
13+
14+
useEffect(() => {
15+
(async () => {
16+
const loginApiResponse = await (await fetch("https://bbs.project-graph.top/api/login")).text();
17+
if (loginApiResponse.startsWith('"')) {
18+
setStatus("ok");
19+
// "/user/<username>"
20+
const userDataEndpoint = loginApiResponse.slice(1, -1);
21+
const userData = await (await fetch(`https://bbs.project-graph.top/api${userDataEndpoint}`)).json();
22+
setUser(userData);
23+
} else {
24+
setStatus("out");
25+
}
26+
})();
27+
}, []);
28+
29+
return (
30+
<div className="flex flex-col items-center gap-4 p-4">
31+
{status === "loading" && <span>加载中...</span>}
32+
{status === "out" && <span>未登录</span>}
33+
{status === "ok" && (
34+
<>
35+
<div className="flex items-center gap-2">
36+
<img src={`https://bbs.project-graph.top${user.uploadedpicture}`} crossOrigin="" className="size-12" />
37+
<div className="flex flex-col gap-1">
38+
<span>{user.username}</span>
39+
<span className="text-sm opacity-50">UID: {user.uid}</span>
40+
</div>
41+
</div>
42+
<Button
43+
onClick={() => {
44+
fetch("https://bbs.project-graph.top/logout", {
45+
method: "POST",
46+
});
47+
toast.success("已退出登录");
48+
}}
49+
>
50+
<LogOut />
51+
退出登录
52+
</Button>
53+
</>
54+
)}
55+
</div>
56+
);
57+
}
58+
59+
UserWindow.open = () => {
60+
SubWindow.create({
61+
title: "用户",
62+
children: <UserWindow />,
63+
rect: Rectangle.inCenter(new Vector(230, 240)),
64+
});
65+
};

0 commit comments

Comments
 (0)