feat: 账号凭据默认脱敏并支持主动查看
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite://")
|
||||
os.environ.setdefault("APP_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from web.backend.database import Base
|
||||
from web.backend.models import Account, User
|
||||
from web.backend.routers.accounts import list_accounts
|
||||
|
||||
|
||||
class AccountSensitiveFieldsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session = sessionmaker(bind=self.engine)()
|
||||
self.admin = User(username="admin", password_hash="hash", role="super_admin")
|
||||
self.session.add(self.admin)
|
||||
self.session.add(Account(
|
||||
username="account",
|
||||
password="account-password",
|
||||
email="account@example.com",
|
||||
email_password="email-password",
|
||||
))
|
||||
self.session.commit()
|
||||
|
||||
def tearDown(self):
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(self.engine)
|
||||
self.engine.dispose()
|
||||
|
||||
def test_sensitive_fields_are_hidden_by_default_even_for_admin(self):
|
||||
result = list_accounts(
|
||||
assigned_only=False,
|
||||
tag=None,
|
||||
has_cookie=False,
|
||||
search="",
|
||||
page=1,
|
||||
page_size=20,
|
||||
include_sensitive=False,
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
)
|
||||
|
||||
item = result["items"][0]
|
||||
self.assertIsNone(item.password)
|
||||
self.assertIsNone(item.email)
|
||||
self.assertIsNone(item.email_password)
|
||||
|
||||
def test_admin_can_explicitly_request_sensitive_fields(self):
|
||||
result = list_accounts(
|
||||
assigned_only=False,
|
||||
tag=None,
|
||||
has_cookie=False,
|
||||
search="",
|
||||
page=1,
|
||||
page_size=20,
|
||||
include_sensitive=True,
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
)
|
||||
|
||||
item = result["items"][0]
|
||||
self.assertEqual(item.password, "account-password")
|
||||
self.assertEqual(item.email, "account@example.com")
|
||||
self.assertEqual(item.email_password, "email-password")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -100,7 +100,7 @@ def list_accounts(
|
||||
search: str = Query(""),
|
||||
page: int | None = Query(None, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
include_sensitive: bool = Query(True),
|
||||
include_sensitive: bool = Query(False),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import type { TableProps } from 'antd';
|
||||
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import { accountApi, userApi, type AccountItem, type BasicSummary, type UserInfo } from '../api/modules';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { formatTime } from '../utils/time';
|
||||
@@ -33,6 +33,7 @@ export default function AccountsPage() {
|
||||
const [selectedAllMatching, setSelectedAllMatching] = useState(false);
|
||||
const [batchTagInput, setBatchTagInput] = useState('');
|
||||
const [batchTagVisible, setBatchTagVisible] = useState(false);
|
||||
const [sensitiveVisible, setSensitiveVisible] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0, tag_count: 0 });
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
@@ -58,7 +59,7 @@ export default function AccountsPage() {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
search: searchText.trim() || undefined,
|
||||
include_sensitive: canViewFull,
|
||||
include_sensitive: canViewFull && sensitiveVisible,
|
||||
});
|
||||
setAccounts(data.items);
|
||||
setTotal(data.total);
|
||||
@@ -67,7 +68,7 @@ export default function AccountsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canViewFull, currentPage, pageSize, searchText, tagFilter]);
|
||||
}, [canViewFull, currentPage, pageSize, searchText, sensitiveVisible, tagFilter]);
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
try {
|
||||
@@ -224,6 +225,27 @@ export default function AccountsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSensitiveVisibility = () => {
|
||||
if (sensitiveVisible) {
|
||||
// 立即清除已加载的敏感内容,再由 effect 重新获取脱敏列表。
|
||||
setAccounts((previous) => previous.map((account) => ({
|
||||
...account,
|
||||
password: undefined,
|
||||
email: undefined,
|
||||
email_password: undefined,
|
||||
})));
|
||||
setSensitiveVisible(false);
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '显示账号凭据?',
|
||||
content: '密码、邮箱和邮箱密码将临时显示在当前页面。关闭或刷新页面后会恢复脱敏。',
|
||||
okText: '显示',
|
||||
cancelText: '取消',
|
||||
onOk: () => setSensitiveVisible(true),
|
||||
});
|
||||
};
|
||||
|
||||
const columns: TableProps<AccountItem>['columns'] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
@@ -277,8 +299,7 @@ export default function AccountsPage() {
|
||||
},
|
||||
];
|
||||
|
||||
// 只有管理员可看完整字段(密码、邮箱等)
|
||||
if (canViewFull) {
|
||||
if (canViewFull && sensitiveVisible) {
|
||||
columns.push(
|
||||
{ title: '密码', dataIndex: 'password', width: 120 },
|
||||
{ title: '邮箱', dataIndex: 'email' },
|
||||
@@ -346,6 +367,14 @@ export default function AccountsPage() {
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
{canViewFull && (
|
||||
<Button
|
||||
icon={sensitiveVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={handleSensitiveVisibility}
|
||||
>
|
||||
{sensitiveVisible ? '隐藏凭据' : '显示凭据'}
|
||||
</Button>
|
||||
)}
|
||||
{canImport && (
|
||||
<Button
|
||||
disabled={selectedCount === 0}
|
||||
|
||||
Reference in New Issue
Block a user