diff --git a/backend/routers/config.py b/backend/routers/config.py index fbe332c..c18a33a 100644 --- a/backend/routers/config.py +++ b/backend/routers/config.py @@ -25,9 +25,10 @@ from ..deps import get_current_user, require_admin def _require_config_permission(request: Request, user: dict = Depends(get_current_user)) -> dict: - """普通用户仅可管理 Apps;其余配置及凭据只允许管理员访问。""" + """普通用户可管理 Apps 与版本号;其余配置及凭据只允许管理员访问。""" path = request.url.path.rstrip("/") - if path == "/api/config/apps" or path.startswith("/api/config/apps/"): + if (path == "/api/config/apps" or path.startswith("/api/config/apps/") + or path == "/api/config/versions"): return user return require_admin(user) diff --git a/frontend/src/__tests__/ConfigView.test.js b/frontend/src/__tests__/ConfigView.test.js index 8f231e7..5c8dad1 100644 --- a/frontend/src/__tests__/ConfigView.test.js +++ b/frontend/src/__tests__/ConfigView.test.js @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' +import { ref } from 'vue' import ConfigView from '../views/ConfigView.vue' global.fetch = vi.fn() @@ -23,6 +24,7 @@ function mockFetch(url) { '/api/config/servers': { '测试环境': { api: 'https://test.com', assDom: '', universalLink: '' } }, '/api/config/branches': ['main', 'dev'], '/api/config/build': { max_concurrent_builds: 2, build_dir_retention_hours: 24, build_base_dir: '/tmp' }, + '/api/config/versions': { app_ver: '2.195.0', build_ver: '2.195.0.0' }, '/api/config': { apps: {}, schemes: {}, branches: ['main'] }, } return Promise.resolve({ @@ -30,6 +32,17 @@ function mockFetch(url) { }) } +const mountConfigView = ({ loggedIn = true, admin = true } = {}) => mount(ConfigView, { + global: { + provide: { + showLogin: ref(false), + getToken: () => '', + isLoggedIn: ref(loggedIn), + isAdmin: ref(admin), + }, + }, +}) + describe('ConfigView.vue', () => { beforeEach(() => { vi.clearAllMocks() @@ -39,21 +52,19 @@ describe('ConfigView.vue', () => { }) it('未登录时显示权限提示', async () => { - const wrapper = mount(ConfigView) + const wrapper = mountConfigView({ loggedIn: false }) await flushPromises() - expect(wrapper.text()).toContain('需要管理员权限') + expect(wrapper.text()).toContain('请先登录') }) it('已登录时显示管理页面', async () => { - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + const wrapper = mountConfigView() await flushPromises() expect(wrapper.text()).toContain('配置管理') }) it('显示侧边栏菜单', async () => { - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + const wrapper = mountConfigView() await flushPromises() const menuItems = wrapper.findAll('.sidebar-menu li') @@ -65,16 +76,14 @@ describe('ConfigView.vue', () => { expect(texts).toContain('打包设置') }) - it('默认显示服务器环境 tab', async () => { - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + it('默认显示 Apps 配置 tab', async () => { + const wrapper = mountConfigView() await flushPromises() - expect(wrapper.text()).toContain('服务器环境配置') + expect(wrapper.text()).toContain('Apps 配置') }) it('切换到分支管理 tab', async () => { - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') @@ -93,8 +102,7 @@ describe('ConfigView.vue', () => { return mockFetch(url) }) - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') @@ -122,8 +130,7 @@ describe('ConfigView.vue', () => { return mockFetch(url) }) - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') @@ -147,8 +154,7 @@ describe('ConfigView.vue', () => { }) window.alert = vi.fn() - localStorageMock.getItem.mockReturnValue('true') - const wrapper = mount(ConfigView) + const wrapper = mountConfigView() await flushPromises() const buildMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '打包设置') @@ -161,4 +167,20 @@ describe('ConfigView.vue', () => { expect(window.alert).toHaveBeenCalledWith('设置已保存') }) + + it('普通账号可修改版本号但看不到打包参数', async () => { + const wrapper = mountConfigView({ admin: false }) + await flushPromises() + + const menuTexts = wrapper.findAll('.sidebar-menu li').map(li => li.text()) + expect(menuTexts).toEqual(['Apps 配置', '打包设置']) + + const buildMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '打包设置') + await buildMenu.trigger('click') + await wrapper.vm.$nextTick() + + expect(wrapper.text()).toContain('应用版本号') + expect(wrapper.text()).not.toContain('打包参数') + expect(wrapper.findAll('input').length).toBe(2) + }) }) diff --git a/frontend/src/views/ConfigView.vue b/frontend/src/views/ConfigView.vue index 13816f2..c10ce06 100644 --- a/frontend/src/views/ConfigView.vue +++ b/frontend/src/views/ConfigView.vue @@ -15,7 +15,7 @@
  • Schemes 配置
  • 分支管理
  • 上传配置
  • -
  • 打包设置
  • +
  • 打包设置
  • JSON 编辑
  • @@ -280,21 +280,23 @@ -

    打包参数

    -
    - - -
    建议 1-4,过高可能影响构建稳定性
    -
    -
    - - -
    -
    - - -
    - + @@ -612,8 +614,12 @@ onMounted(async () => { const loadData = async () => { if (!isAdmin.value) { - const appsRes = await authFetch('/api/config/apps') + const [appsRes, versionsRes] = await Promise.all([ + authFetch('/api/config/apps'), + authFetch('/api/config/versions'), + ]) if (appsRes.ok) apps.value = await appsRes.json() + if (versionsRes.ok) versions.value = await versionsRes.json() return } diff --git a/tests/test_security.py b/tests/test_security.py index ed13ba9..0143e8c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -37,8 +37,8 @@ def test_production_rejects_default_security_settings(monkeypatch): config.validate_production_security() -def test_config_routes_limit_regular_users_to_apps(client, tmp_config): - """普通账号只能管理 Apps,不能读取或修改包含密钥的配置。""" +def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_config): + """普通账号可管理 Apps 与版本号,不能读取或修改管理员配置。""" response = client.post("/api/users", json={ "username": "builder", "password": "builder-password-123", @@ -55,6 +55,11 @@ def test_config_routes_limit_regular_users_to_apps(client, tmp_config): assert client.get("/api/config/apps", headers=headers).status_code == 200 assert client.post("/api/config/apps", json={"name": "普通用户 App"}, headers=headers).status_code == 200 + assert client.get("/api/config/versions", headers=headers).status_code == 200 + assert client.put("/api/config/versions", json={ + "app_ver": "2.196.0", "build_ver": "2.196.0.0", + }, headers=headers).status_code == 200 assert client.get("/api/config", headers=headers).status_code == 403 assert client.get("/api/config/servers", headers=headers).status_code == 403 + assert client.put("/api/config/build", json={"max_concurrent_builds": 1}, headers=headers).status_code == 403 assert client.put("/api/config/upload", json={"mode": "oss"}, headers=headers).status_code == 403