mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 13:05:09 +00:00
refactor: 将默认密码相关逻辑重构为后端处理 (#1247)
* refactor: 将默认密码相关逻辑重构为后端处理 * refactor: 日志路由进行脱敏,生成随机密码使用node:crypto.randomBytes * feat: 更新密码功能增强,添加新密码强度验证和旧密码检查 * feat: 给文件管理添加WebUI配置文件的脱敏处理和验证逻辑 * refactor: 优化网络显示卡片按钮样式和行为,调整按钮属性以提升用户体验 * feat: 增强路径处理逻辑,添加安全验证以防止路径遍历攻击 * feat: 增强文件路径处理逻辑,添加安全验证以防止路径遍历攻击,并优化查询参数提取 * feat: CodeQL不认可 受不了
This commit is contained in:
@@ -62,24 +62,39 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
isDisabled={editing}
|
||||
radius="full"
|
||||
radius="sm"
|
||||
size="sm"
|
||||
variant="shadow"
|
||||
variant="flat"
|
||||
>
|
||||
<Button color="warning" startContent={<FiEdit3 />} onPress={onEdit}>
|
||||
<Button
|
||||
color="warning"
|
||||
startContent={<FiEdit3 size={16} />}
|
||||
onPress={onEdit}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color={debug ? 'success' : 'default'}
|
||||
startContent={<CgDebug />}
|
||||
color={debug ? 'secondary' : 'success'}
|
||||
variant="flat"
|
||||
startContent={
|
||||
<CgDebug
|
||||
style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
minWidth: '16px',
|
||||
minHeight: '16px'
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onPress={handleEnableDebug}
|
||||
>
|
||||
{debug ? '关闭调试' : '开启调试'}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
startContent={<MdDeleteForever />}
|
||||
className="bg-danger/20 text-danger hover:bg-danger/30 transition-colors"
|
||||
variant="flat"
|
||||
startContent={<MdDeleteForever size={16} />}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
删除
|
||||
|
||||
@@ -33,21 +33,6 @@ export default class WebUIManager {
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async changePasswordFromDefault(newToken: string) {
|
||||
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||
'/auth/update_token',
|
||||
{ newToken, fromDefault: true }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async checkUsingDefaultToken() {
|
||||
const { data } = await serverRequest.get<ServerResponse<boolean>>(
|
||||
'/auth/check_using_default_token'
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
public static async proxy<T>(url = '') {
|
||||
const data = await serverRequest.get<ServerResponse<string>>(
|
||||
'/base/proxy?url=' + encodeURIComponent(url)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Input } from '@heroui/input'
|
||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -12,14 +11,12 @@ import SaveButtons from '@/components/button/save_buttons'
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
|
||||
const ChangePasswordCard = () => {
|
||||
const [isDefaultToken, setIsDefaultToken] = useState<boolean>(false)
|
||||
const [isLoadingCheck, setIsLoadingCheck] = useState<boolean>(true)
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleWebuiSubmit,
|
||||
formState: { isSubmitting },
|
||||
reset
|
||||
formState: { isSubmitting, errors },
|
||||
reset,
|
||||
watch
|
||||
} = useForm<{
|
||||
oldToken: string
|
||||
newToken: string
|
||||
@@ -33,31 +30,13 @@ const ChangePasswordCard = () => {
|
||||
const navigate = useNavigate()
|
||||
const [_, setToken] = useLocalStorage(key.token, '')
|
||||
|
||||
// 检查是否使用默认密码
|
||||
useEffect(() => {
|
||||
const checkDefaultToken = async () => {
|
||||
try {
|
||||
const isDefault = await WebUIManager.checkUsingDefaultToken()
|
||||
setIsDefaultToken(isDefault)
|
||||
} catch (error) {
|
||||
console.error('检查默认密码状态失败:', error)
|
||||
} finally {
|
||||
setIsLoadingCheck(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkDefaultToken()
|
||||
}, [])
|
||||
// 监听旧密码的值
|
||||
const oldTokenValue = watch('oldToken')
|
||||
|
||||
const onSubmit = handleWebuiSubmit(async (data) => {
|
||||
try {
|
||||
if (isDefaultToken) {
|
||||
// 从默认密码更新
|
||||
await WebUIManager.changePasswordFromDefault(data.newToken)
|
||||
} else {
|
||||
// 正常密码更新
|
||||
await WebUIManager.changePassword(data.oldToken, data.newToken)
|
||||
}
|
||||
// 使用正常密码更新流程
|
||||
await WebUIManager.changePassword(data.oldToken, data.newToken)
|
||||
|
||||
toast.success('修改成功')
|
||||
setToken('')
|
||||
@@ -69,53 +48,74 @@ const ChangePasswordCard = () => {
|
||||
}
|
||||
})
|
||||
|
||||
if (isLoadingCheck) {
|
||||
return (
|
||||
<>
|
||||
<title>修改密码 - NapCat WebUI</title>
|
||||
<div className="flex justify-center items-center h-32">
|
||||
<div className="text-center">加载中...</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>修改密码 - NapCat WebUI</title>
|
||||
|
||||
{isDefaultToken && (
|
||||
<div className="mb-4 p-3 bg-warning-50 border border-warning-200 rounded-lg">
|
||||
<p className="text-warning-700 text-sm">
|
||||
检测到您正在使用默认密码,为了安全起见,请立即设置新密码。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isDefaultToken && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="oldToken"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="旧密码"
|
||||
placeholder="请输入旧密码"
|
||||
type="password"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="oldToken"
|
||||
rules={{
|
||||
required: '旧密码不能为空',
|
||||
validate: (value) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return '旧密码不能为空'
|
||||
}
|
||||
return true
|
||||
}
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="旧密码"
|
||||
placeholder="请输入旧密码"
|
||||
type="password"
|
||||
isRequired
|
||||
isInvalid={!!errors.oldToken}
|
||||
errorMessage={errors.oldToken?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="newToken"
|
||||
rules={{
|
||||
required: '新密码不能为空',
|
||||
minLength: {
|
||||
value: 6,
|
||||
message: '新密码至少需要6个字符'
|
||||
},
|
||||
validate: (value) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return '新密码不能为空'
|
||||
}
|
||||
if (value.trim().length !== value.length) {
|
||||
return '新密码不能包含前后空格'
|
||||
}
|
||||
if (value === oldTokenValue) {
|
||||
return '新密码不能与旧密码相同'
|
||||
}
|
||||
// 检查是否包含字母
|
||||
if (!/[a-zA-Z]/.test(value)) {
|
||||
return '新密码必须包含字母'
|
||||
}
|
||||
// 检查是否包含数字
|
||||
if (!/[0-9]/.test(value)) {
|
||||
return '新密码必须包含数字'
|
||||
}
|
||||
return true
|
||||
}
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
label={isDefaultToken ? "设置新密码" : "新密码"}
|
||||
placeholder={isDefaultToken ? "请设置一个安全的新密码" : "请输入新密码"}
|
||||
label="新密码"
|
||||
placeholder="至少6位,包含字母和数字"
|
||||
type="password"
|
||||
isRequired
|
||||
isInvalid={!!errors.newToken}
|
||||
errorMessage={errors.newToken?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,52 +1,15 @@
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { Suspense, useEffect } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Suspense } from 'react'
|
||||
import { Outlet, useLocation } from 'react-router-dom'
|
||||
|
||||
import useAuth from '@/hooks/auth'
|
||||
import useDialog from '@/hooks/use-dialog'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
import DefaultLayout from '@/layouts/default'
|
||||
|
||||
const CheckDefaultPassword = () => {
|
||||
const { isAuth } = useAuth()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const checkDefaultPassword = async () => {
|
||||
const data = await WebUIManager.checkUsingDefaultToken()
|
||||
if (data) {
|
||||
dialog.confirm({
|
||||
title: '修改默认密码',
|
||||
content: '检测到当前密码为默认密码,为了您的安全,必须立即修改密码。',
|
||||
confirmText: '前往修改',
|
||||
onConfirm: () => {
|
||||
navigate('/config?tab=token')
|
||||
},
|
||||
onCancel: () => {
|
||||
navigate('/config?tab=token')
|
||||
},
|
||||
onClose() {
|
||||
navigate('/config?tab=token')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
checkDefaultPassword()
|
||||
}
|
||||
}, [isAuth])
|
||||
return null
|
||||
}
|
||||
|
||||
export default function IndexPage() {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<CheckDefaultPassword />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex justify-center px-10">
|
||||
|
||||
@@ -92,42 +92,65 @@ export default function WebLoginPage() {
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="flex gap-5 py-5 px-5 md:px-10">
|
||||
<Input
|
||||
isClearable
|
||||
type="password"
|
||||
classNames={{
|
||||
label: 'text-black/50 dark:text-white/90',
|
||||
input: [
|
||||
'bg-transparent',
|
||||
'text-black/90 dark:text-white/90',
|
||||
'placeholder:text-default-700/50 dark:placeholder:text-white/60'
|
||||
],
|
||||
innerWrapper: 'bg-transparent',
|
||||
inputWrapper: [
|
||||
'shadow-xl',
|
||||
'bg-default-100/70',
|
||||
'dark:bg-default/60',
|
||||
'backdrop-blur-xl',
|
||||
'backdrop-saturate-200',
|
||||
'hover:bg-default-0/70',
|
||||
'dark:hover:bg-default/70',
|
||||
'group-data-[focus=true]:bg-default-100/50',
|
||||
'dark:group-data-[focus=true]:bg-default/60',
|
||||
'!cursor-text'
|
||||
]
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onSubmit()
|
||||
}}
|
||||
isDisabled={isLoading}
|
||||
label="Token"
|
||||
placeholder="请输入token"
|
||||
radius="lg"
|
||||
size="lg"
|
||||
startContent={
|
||||
<IoKeyOutline className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
value={tokenValue}
|
||||
onChange={(e) => setTokenValue(e.target.value)}
|
||||
onClear={() => setTokenValue('')}
|
||||
/>
|
||||
>
|
||||
{/* 隐藏的用户名字段,帮助浏览器识别登录表单 */}
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value="napcat-webui"
|
||||
autoComplete="username"
|
||||
className="absolute -left-[9999px] opacity-0 pointer-events-none"
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
aria-label="Username"
|
||||
/>
|
||||
<Input
|
||||
isClearable
|
||||
type="password"
|
||||
name="password"
|
||||
autoComplete="current-password"
|
||||
classNames={{
|
||||
label: 'text-black/50 dark:text-white/90',
|
||||
input: [
|
||||
'bg-transparent',
|
||||
'text-black/90 dark:text-white/90',
|
||||
'placeholder:text-default-700/50 dark:placeholder:text-white/60'
|
||||
],
|
||||
innerWrapper: 'bg-transparent',
|
||||
inputWrapper: [
|
||||
'shadow-xl',
|
||||
'bg-default-100/70',
|
||||
'dark:bg-default/60',
|
||||
'backdrop-blur-xl',
|
||||
'backdrop-saturate-200',
|
||||
'hover:bg-default-0/70',
|
||||
'dark:hover:bg-default/70',
|
||||
'group-data-[focus=true]:bg-default-100/50',
|
||||
'dark:group-data-[focus=true]:bg-default/60',
|
||||
'!cursor-text'
|
||||
]
|
||||
}}
|
||||
isDisabled={isLoading}
|
||||
label="Token"
|
||||
placeholder="请输入token"
|
||||
radius="lg"
|
||||
size="lg"
|
||||
startContent={
|
||||
<IoKeyOutline className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
value={tokenValue}
|
||||
onChange={(e) => setTokenValue(e.target.value)}
|
||||
onClear={() => setTokenValue('')}
|
||||
/>
|
||||
</form>
|
||||
<div className="text-center text-small text-default-600 dark:text-default-400 px-2">
|
||||
💡 提示:请从 NapCat 启动日志中查看登录密钥
|
||||
</div>
|
||||
<Button
|
||||
className="mx-10 mt-10 text-lg py-7"
|
||||
color="primary"
|
||||
|
||||
Reference in New Issue
Block a user