Skip to content

Commit 45714c2

Browse files
committed
feat: add device session protection and logout flow
1 parent ec47d1f commit 45714c2

File tree

20 files changed

+880
-27
lines changed

20 files changed

+880
-27
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@solid-primitives/keyboard": "^1.2.5",
6565
"@solid-primitives/storage": "^1.3.1",
6666
"@stitches/core": "^1.2.8",
67+
"@types/uuid": "^10.0.0",
6768
"@viselect/vanilla": "^3.5.0",
6869
"aplayer": "^1.10.1",
6970
"artplayer": "^5.2.2",
@@ -96,7 +97,8 @@
9697
"solid-markdown": "^1.2.0",
9798
"solid-transition-group": "^0.0.12",
9899
"streamsaver": "^2.0.6",
99-
"typescript-natural-sort": "^0.7.2"
100+
"typescript-natural-sort": "^0.7.2",
101+
"uuid": "^11.1.0"
100102
},
101103
"lint-staged": {
102104
"**/*": "prettier --write"

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Base.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,24 @@ export const Error = (props: {
4848
)
4949
}
5050

51+
// 检查是否是设备数上限错误
52+
const isTooManyDevicesError = () => {
53+
return props.msg.includes("too many active devices")
54+
}
55+
56+
// 检查是否是会话失效错误
57+
const isSessionInactiveError = () => {
58+
return props.msg.includes("session inactive")
59+
}
60+
5161
const handleGoToStorages = () => {
5262
to("/@manage/storages")
5363
}
5464

65+
const handleGoToLogin = () => {
66+
to(`/@login?redirect=${encodeURIComponent(window.location.pathname)}`)
67+
}
68+
5569
return (
5670
<Center h={merged.h} p="$2" flexDirection="column">
5771
<Box
@@ -61,20 +75,37 @@ export const Error = (props: {
6175
bgColor={useColorModeValue("white", "$neutral3")()}
6276
>
6377
<VStack spacing="$4" textAlign="center">
64-
<Heading
65-
css={{
66-
wordBreak: "break-all",
67-
}}
68-
>
69-
{props.msg}
70-
</Heading>
78+
<Show when={!isTooManyDevicesError() && !isSessionInactiveError()}>
79+
<Heading
80+
css={{
81+
wordBreak: "break-all",
82+
}}
83+
>
84+
{props.msg}
85+
</Heading>
86+
</Show>
7187

7288
<Show when={isStorageError()}>
7389
<Button onClick={handleGoToStorages} size="md">
7490
{t("home.go_to_storages")}
7591
</Button>
7692
</Show>
7793

94+
<Show when={isTooManyDevicesError()}>
95+
<VStack spacing="$2">
96+
<Text fontSize="sm">{t("session.too_many_devices")}</Text>
97+
</VStack>
98+
</Show>
99+
100+
<Show when={isSessionInactiveError()}>
101+
<VStack spacing="$2">
102+
<Text fontSize="sm">{t("session.session_inactive")}</Text>
103+
<Button onClick={handleGoToLogin} size="md" colorScheme="accent">
104+
{t("global.go_login")}
105+
</Button>
106+
</VStack>
107+
</Show>
108+
78109
<Show when={!props.disableColor}>
79110
<Flex mt="$2" justifyContent="end">
80111
<SwitchColorMode />

src/lang/en/manage.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"global": "Global",
99
"other": "Other",
1010
"users": "Users",
11+
"session": "Session",
12+
"my_session": "My Session",
13+
"session_management": "Management",
1114
"storages": "Storages",
1215
"metas": "Metas",
1316
"labels": "Labels",

src/lang/en/session.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"device_id": "Device ID",
3+
"user_id": "User ID",
4+
"system_info": "System Info",
5+
"ip": "IP",
6+
"last_active": "Last Active",
7+
"status": "Status",
8+
"active": "Active",
9+
"offline": "Offline",
10+
"kick_out": "Kick Out",
11+
"kick_out_confirm": "Are you sure you want to kick out this session?",
12+
"kick_out_success": "Session kicked out successfully",
13+
"kick_out_failed": "Failed to kick out session",
14+
"kick_out_current_session": "Current session has been kicked out. Please log in again.",
15+
"too_many_devices": "Device limit reached. Please contact administrator to kick out other devices in 'My Sessions'.",
16+
"session_inactive": "Current session has been invalidated. Please log in again."
17+
}

src/pages/home/folder/ListItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export const cols: Col[] = [
5151
{
5252
name: "size",
5353
textAlign: "right",
54-
w: { "@initial": "100px", "@md": "12%" },
54+
w: { "@initial": "100px", "@md": "30%" },
5555
},
56-
{ name: "modified", textAlign: "right", w: { "@initial": 0, "@md": "20%" } },
56+
{ name: "modified", textAlign: "right", w: { "@initial": 0, "@md": "25%" } },
5757
]
5858

5959
// 添加选中统计组件

src/pages/login/index.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ const Login = () => {
105105
})
106106

107107
const [loading, data] = useFetch(
108-
async (): Promise<Resp<{ token: string }>> => {
108+
async (): Promise<Resp<{ token: string; device_key?: string }>> => {
109109
if (useLdap()) {
110110
return r.post("/auth/login/ldap", {
111111
username: username(),
@@ -137,7 +137,7 @@ const Login = () => {
137137
credentials: AuthenticationPublicKeyCredential,
138138
username: string,
139139
signal: AbortSignal | undefined,
140-
): Promise<Resp<{ token: string }>> =>
140+
): Promise<Resp<{ token: string; device_key?: string }>> =>
141141
r.post(
142142
"/authn/webauthn_finish_login?username=" + username,
143143
JSON.stringify(credentials),
@@ -212,6 +212,19 @@ const Login = () => {
212212
handleRespWithoutNotify(resp, (data) => {
213213
notify.success(t("login.success"))
214214
changeToken(data.token)
215+
// 保存 device_key 到 localStorage
216+
if (data.device_key) {
217+
localStorage.setItem("device_key", data.device_key)
218+
console.log("=== Login Debug (Hash) ===")
219+
console.log("Saved device_key:", data.device_key)
220+
console.log("Full response data:", data)
221+
console.log("========================")
222+
} else {
223+
console.log("=== Login Debug (Hash) ===")
224+
console.log("No device_key in response")
225+
console.log("Full response data:", data)
226+
console.log("========================")
227+
}
215228
to(
216229
decodeURIComponent(searchParams.redirect || base_path || "/"),
217230
true,
@@ -272,6 +285,19 @@ const Login = () => {
272285
(data) => {
273286
notify.success(t("login.success"))
274287
changeToken(data.token)
288+
// 保存 device_key 到 localStorage
289+
if (data.device_key) {
290+
localStorage.setItem("device_key", data.device_key)
291+
console.log("=== Login Debug ===")
292+
console.log("Saved device_key:", data.device_key)
293+
console.log("Full response data:", data)
294+
console.log("==================")
295+
} else {
296+
console.log("=== Login Debug ===")
297+
console.log("No device_key in response")
298+
console.log("Full response data:", data)
299+
console.log("==================")
300+
}
275301
to(
276302
decodeURIComponent(searchParams.redirect || base_path || "/"),
277303
true,

src/pages/manage/Header.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import { SwitchColorMode, SwitchLanguageWhite } from "~/components"
2020
import { useFetch, useRouter, useT } from "~/hooks"
2121
import { SideMenu } from "./SideMenu"
2222
import { side_menu_items } from "./sidemenu_items"
23-
import { changeToken, handleResp, notify, r } from "~/utils"
23+
import { handleResp, notify, r } from "~/utils"
24+
import { clearUserData } from "~/utils/auth"
2425
import { PResp } from "~/types"
2526
const { isOpen, onOpen, onClose } = createDisclosure()
2627
const [logOutReqLoading, logOutReq] = useFetch(
@@ -32,7 +33,7 @@ const Header = () => {
3233
const { to } = useRouter()
3334
const logOut = async () => {
3435
handleResp(await logOutReq(), () => {
35-
changeToken()
36+
clearUserData()
3637
notify.success(t("manage.logout_success"))
3738
to(`/@login?redirect=${encodeURIComponent(location.pathname)}`)
3839
})

src/pages/manage/SideMenu.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,24 @@ export interface SideMenuItemProps {
3030

3131
const SideMenuItem = (props: SideMenuItemProps) => {
3232
const ifShow = createMemo(() => {
33-
if (!UserMethods.is_admin(me())) {
34-
if (props.role === undefined) return false
35-
else if (props.role === UserRole.GENERAL && !UserMethods.is_general(me()))
33+
// 使用层级权限检查
34+
if (props.role !== undefined && !UserMethods.hasAccess(me(), props.role)) {
35+
return false
36+
}
37+
38+
// 如果有子菜单项,检查是否有可见的子菜单项
39+
if (props.children) {
40+
const hasVisibleChildren = props.children.some((child) => {
41+
if (child.role !== undefined) {
42+
return UserMethods.hasAccess(me(), child.role)
43+
}
44+
return true
45+
})
46+
if (!hasVisibleChildren) {
3647
return false
48+
}
3749
}
50+
3851
return true
3952
})
4053
return (
@@ -102,6 +115,18 @@ const SideMenuItemWithChildren = (props: SideMenuItemProps) => {
102115
const { pathname } = useRouter()
103116
const [open, setOpen] = createSignal(pathname().includes(props.to))
104117
const t = useT()
118+
119+
// 检查是否有可见的子菜单项
120+
const hasVisibleChildren = createMemo(() => {
121+
if (!props.children) return false
122+
return props.children.some((child) => {
123+
if (child.role !== undefined) {
124+
return UserMethods.hasAccess(me(), child.role)
125+
}
126+
return true
127+
})
128+
})
129+
105130
return (
106131
<Box w="$full">
107132
<Flex
@@ -135,7 +160,7 @@ const SideMenuItemWithChildren = (props: SideMenuItemProps) => {
135160
transition="transform 0.2s"
136161
/>
137162
</Flex>
138-
<Show when={open()}>
163+
<Show when={open() && hasVisibleChildren()}>
139164
<Box pl="$2">
140165
<SideMenu items={props.children!} />
141166
</Box>

src/pages/manage/common/DeletePopover.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ export interface DeletePopoverProps {
1515
loading: boolean
1616
onClick: () => void
1717
disabled?: boolean
18+
buttonText?: string
1819
}
1920
export const DeletePopover = (props: DeletePopoverProps) => {
2021
const t = useT()
2122
const isDisabled = props.disabled ?? false // 默认值为 false
23+
const buttonText = props.buttonText ?? t("global.delete") // 默认使用删除文本
2224
return (
2325
<Popover>
2426
{({ onClose }) => (
@@ -28,7 +30,7 @@ export const DeletePopover = (props: DeletePopoverProps) => {
2830
colorScheme="danger"
2931
disabled={isDisabled}
3032
>
31-
{t("global.delete")}
33+
{buttonText}
3234
</PopoverTrigger>
3335
<PopoverContent>
3436
<PopoverArrow />

0 commit comments

Comments
 (0)