mirror of
https://github.com/Melorise/nPanel.git
synced 2026-11-07 14:49:35 +00:00
fix: make panel access settings declarative
This commit is contained in:
@@ -17,7 +17,10 @@ nPanel 不编辑、生成、扫描或接管用户的 Flake、`hardware-configura
|
||||
imports = [ inputs.npanel.nixosModules.default ];
|
||||
|
||||
virtualisation.docker.enable = true;
|
||||
services.nPanel.enable = true;
|
||||
services.nPanel = {
|
||||
enable = true;
|
||||
securityEntrance = "npanelAccess";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,6 +31,8 @@ nPanel 不编辑、生成、扫描或接管用户的 Flake、`hardware-configura
|
||||
services.nPanel = {
|
||||
stateDir = "/srv/npanel";
|
||||
runtimeDir = "/run/npanel";
|
||||
port = 9999;
|
||||
securityEntrance = "npanelAccess";
|
||||
initialPasswordFile = "/run/secrets/npanel-initial-password";
|
||||
};
|
||||
}
|
||||
@@ -37,9 +42,9 @@ nPanel 不编辑、生成、扫描或接管用户的 Flake、`hardware-configura
|
||||
权限写入 `stateDir/initial-password`。账户及密码属于 nPanel 自身的可变状态,
|
||||
不写入 Nix 配置或 Nix store。
|
||||
|
||||
首次启动会额外生成 16 位随机安全入口并以 `0600` 权限写入
|
||||
`stateDir/initial-entrance`。使用 `http://127.0.0.1:<port>/<该文件内容>` 首次访问;
|
||||
登录后可在面板安全设置中修改或关闭该入口。
|
||||
使用 `http://127.0.0.1:<port>/<securityEntrance>` 访问面板。`port` 与
|
||||
`securityEntrance` 是 NixOS 声明配置,在每次 nPanel 启动时同步到内部数据库;面板
|
||||
不会提供修改它们的入口。
|
||||
|
||||
## 状态与权限
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/core/constant"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/ctl_conf"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -91,6 +92,10 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if req.Key == "SecurityEntrance" {
|
||||
if ctl_conf.LoadWithoutPanic("MANAGED_ENTRANCE") != "" {
|
||||
helper.ErrorWithDetail(c, http.StatusForbidden, "ErrInvalidParams", errors.New("SecurityEntrance is managed by NixOS"))
|
||||
return
|
||||
}
|
||||
if !checkEntrancePattern(req.Value) {
|
||||
helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrInvalidParams", buserr.WithName("ErrEntranceFormat", req.Value))
|
||||
return
|
||||
@@ -330,6 +335,11 @@ func (b *BaseApi) UpdateBindInfo(c *gin.Context) {
|
||||
// @Router /core/settings/port/update [post]
|
||||
// @x-panel-log {"bodyKeys":["serverPort"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统端口 => [serverPort]","formatEN":"update system port => [serverPort]"}
|
||||
func (b *BaseApi) UpdatePort(c *gin.Context) {
|
||||
if ctl_conf.LoadWithoutPanic("MANAGED_PORT") != "" {
|
||||
helper.ErrorWithDetail(c, http.StatusForbidden, "ErrInvalidParams", errors.New("ServerPort is managed by NixOS"))
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.PortUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
|
||||
@@ -27,11 +27,30 @@ func Init() {
|
||||
}
|
||||
|
||||
handleUserInfo(global.CONF.Base.ChangeUserInfo, settingRepo)
|
||||
applyManagedSettings(settingRepo)
|
||||
|
||||
generateKey()
|
||||
initDockerConf()
|
||||
}
|
||||
|
||||
// applyManagedSettings makes values declared by the NixOS module authoritative
|
||||
// over the panel's mutable settings database on every core startup.
|
||||
func applyManagedSettings(settingRepo repo.ISettingRepo) {
|
||||
if port := ctl_conf.LoadWithoutPanic("MANAGED_PORT"); port != "" {
|
||||
if err := settingRepo.Update("ServerPort", port); err != nil {
|
||||
global.LOG.Fatalf("apply managed panel port failed, err: %v", err)
|
||||
}
|
||||
global.CONF.Conn.Port = port
|
||||
}
|
||||
|
||||
if entrance := ctl_conf.LoadWithoutPanic("MANAGED_ENTRANCE"); entrance != "" {
|
||||
if err := settingRepo.Update("SecurityEntrance", entrance); err != nil {
|
||||
global.LOG.Fatalf("apply managed security entrance failed, err: %v", err)
|
||||
}
|
||||
global.CONF.Conn.Entrance = entrance
|
||||
}
|
||||
}
|
||||
|
||||
func handleUserInfo(tags string, settingRepo repo.ISettingRepo) {
|
||||
if len(tags) == 0 {
|
||||
return
|
||||
|
||||
@@ -18,11 +18,16 @@ nPanel 是独立维护的、源自社区版 1Panel 的 NixOS 单机 Docker 服
|
||||
imports = [ inputs.npanel.nixosModules.default ];
|
||||
|
||||
virtualisation.docker.enable = true;
|
||||
services.nPanel.enable = true;
|
||||
services.nPanel = {
|
||||
enable = true;
|
||||
securityEntrance = "npanelAccess";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
具体选项名和默认状态目录将在打包阶段确定。以上示例不意味着用户需要按任何特定方式重组其配置文件。
|
||||
`services.nPanel.port` 与 `services.nPanel.securityEntrance` 是服务访问面的声明式
|
||||
配置。它们在每次 core 启动时覆盖 nPanel 内部数据库,面板不提供修改入口。以上示例
|
||||
不意味着用户需要按任何特定方式重组其配置文件。
|
||||
|
||||
## 所有权边界
|
||||
|
||||
@@ -49,7 +54,7 @@ Nix 配置不能替代日常运维。下列功能保留在 nPanel 范围内:
|
||||
- Docker、Compose、容器、镜像、网络、卷、镜像仓库、模板与日志;
|
||||
- 应用商店、已安装应用、网站、运行时、反向代理、数据库、SSL 与 AI;
|
||||
- 仪表盘、主机监控、资源历史、进程查看与进程终止;
|
||||
- 用于运维数据与故障处理的 root 级文件操作,并准确将不可变的 NixOS 路径标记为只读;
|
||||
- 用于运维数据与故障处理的 root 级文件操作。Nix store 与 `/etc` 生成链接等路径由系统权限拒绝写入;在未知用户 Flake 位置的前提下,面板不能自动识别和保护任意用户配置文件;
|
||||
- 本地与远程 SSH 终端、容器终端与数据库终端;
|
||||
- systemd 单元状态、journal 查看,以及运行时的启动、停止、重启与重载操作;
|
||||
- 备份、恢复、告警与 1Panel 领域内的计划任务。
|
||||
@@ -195,4 +200,4 @@ NixOS 模块从本项目源码打包 core、agent 和前端,并提供:
|
||||
- 独立 Flake、可配置状态目录、运行时目录和 systemd core/agent 服务已实现。
|
||||
- 前端与后端已移除持久化系统策略、工具箱、脚本库、快照、多节点和商业入口的可达路由。
|
||||
- 保留 Docker、Compose、应用、网站、数据库、备份、文件、终端、监控、进程和日志的运行时运维能力。
|
||||
- 核心、agent、前端和 Flake 求值必须在发布前持续执行构建与边界审计;NixOS VM 冒烟测试是发布门槛。
|
||||
- 核心、agent、前端和 Flake 求值必须在发布前持续执行构建与边界审计;NixOS VM 冒烟测试是发布门槛,本轮受限修正不执行 VM 测试。
|
||||
|
||||
@@ -129,18 +129,6 @@ const containerRouter = {
|
||||
permission: 'container_view',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'ContainerSetting',
|
||||
component: () => import('@/views/container/setting/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/containers',
|
||||
parent: 'menu.container',
|
||||
title: 'container.setting',
|
||||
permission: 'container_view',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -86,18 +86,6 @@ const settingRouter = {
|
||||
adminOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'safe',
|
||||
name: 'Safe',
|
||||
component: () => import('@/views/setting/safe/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
parent: 'menu.settings',
|
||||
title: 'setting.safe',
|
||||
activeMenu: '/settings',
|
||||
adminOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -32,21 +32,6 @@
|
||||
<template #main>
|
||||
<div>
|
||||
<MainDiv :heightDiff="mode === 'upgrade' ? 280 : 300">
|
||||
<el-alert type="info" :closable="false" v-if="mode === 'installed' && !isIntl">
|
||||
<template #title>
|
||||
<span class="flx-align-center">
|
||||
{{ $t('app.installHelper') }}
|
||||
<el-link
|
||||
class="ml-5"
|
||||
icon="Position"
|
||||
@click="jumpToPath(router, '/containers/setting')"
|
||||
type="primary"
|
||||
>
|
||||
{{ $t('firewall.quickJump') }}
|
||||
</el-link>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<div class="update-prompt" v-if="data === null">
|
||||
<span>{{ mode === 'upgrade' ? $t('app.updatePrompt') : $t('app.installPrompt') }}</span>
|
||||
<div>
|
||||
@@ -164,7 +149,7 @@ import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
|
||||
|
||||
const { currentNode, isMaster, currentNodeAddr, isIntl } = useGlobalStore();
|
||||
const { currentNode, isMaster, currentNodeAddr } = useGlobalStore();
|
||||
useOperateNodeContext(currentNode);
|
||||
|
||||
const data = ref<any>();
|
||||
|
||||
@@ -172,35 +172,14 @@
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</CardWithHeader>
|
||||
<CardWithHeader :header="$t('container.setting')" class="card-interval">
|
||||
<template #body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label-width="200" :label="$t('container.sockPath')">
|
||||
{{ countItem.sockPath }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('container.mirrors')">
|
||||
<div v-for="item in countItem.mirrors" :key="item" :value="item" :label="item">
|
||||
<div class="mt-1">
|
||||
<el-tag>{{ item }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-button class="mt-2" type="primary" link @click="routerToName('ContainerSetting')">
|
||||
<el-icon class="mr-1"><Position /></el-icon>
|
||||
{{ $t('container.goSetting') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</CardWithHeader>
|
||||
</div>
|
||||
<TaskLog ref="taskLogRef" width="70%" @close="loadUsage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { containerItemStats, containerPrune, loadContainerStatus, loadDaemonJson } from '@/api/modules/container';
|
||||
import { containerItemStats, containerPrune, loadContainerStatus } from '@/api/modules/container';
|
||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
import { getAgentSettingInfo } from '@/api/modules/setting';
|
||||
import { computeSize2 } from '@/utils/size';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
@@ -239,9 +218,6 @@ const countItem = reactive({
|
||||
volumeReclaimable: 0,
|
||||
buildCacheUsage: 0,
|
||||
buildCacheReclaimable: 0,
|
||||
|
||||
mirrors: [],
|
||||
sockPath: '',
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
@@ -250,7 +226,6 @@ const search = () => {
|
||||
}
|
||||
loadContainerCount();
|
||||
loadUsage();
|
||||
loadContainerSetting();
|
||||
};
|
||||
|
||||
const loadContainerCount = async () => {
|
||||
@@ -291,14 +266,6 @@ const loadUsage = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const loadContainerSetting = async () => {
|
||||
const res = await loadDaemonJson();
|
||||
countItem.mirrors = res.data.registryMirrors || [];
|
||||
|
||||
const settingRes = await getAgentSettingInfo();
|
||||
countItem.sockPath = settingRes.data.dockerSockPath || 'unix:///var/run/docker.sock';
|
||||
};
|
||||
|
||||
const onClean = async (type: string, withAll: boolean) => {
|
||||
let msg = '';
|
||||
switch (type) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div>
|
||||
<el-card v-if="isExist && !isActive && !prop.isHide" class="mask-prompt">
|
||||
<span>{{ $t('container.serviceUnavailable') }}</span>
|
||||
<el-button type="primary" link class="bt" @click="goSetting">【 {{ $t('container.setting') }} 】</el-button>
|
||||
<span>{{ $t('container.startIn') }}</span>
|
||||
</el-card>
|
||||
<NoSuchService v-if="!isExist" name="Docker" />
|
||||
@@ -13,7 +12,6 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { loadDockerStatus } from '@/api/modules/container';
|
||||
import NoSuchService from '@/components/layout-content/no-such-service.vue';
|
||||
import { routerToName } from '@/utils/router';
|
||||
|
||||
const prop = defineProps({
|
||||
isHide: Boolean,
|
||||
@@ -41,10 +39,6 @@ const loadStatus = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const goSetting = async () => {
|
||||
routerToName('ContainerSetting');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStatus();
|
||||
});
|
||||
|
||||
@@ -43,9 +43,5 @@ const buttons = [
|
||||
label: i18n.global.t('container.composeTemplate', 2),
|
||||
path: '/containers/template',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('container.setting', 2),
|
||||
path: '/containers/setting',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -13,15 +13,6 @@
|
||||
<template #title>
|
||||
<span class="flx-align-center">
|
||||
<span>{{ $t('home.entranceHelper') }}</span>
|
||||
<el-link
|
||||
style="font-size: 12px; margin-left: 5px"
|
||||
icon="Position"
|
||||
v-if="isAdmin"
|
||||
@click="jumpToPath(router, '/settings/safe')"
|
||||
type="primary"
|
||||
>
|
||||
{{ $t('firewall.quickJump') }}
|
||||
</el-link>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
@@ -21,10 +21,6 @@ const buttons = computed<RouterButton[]>(() => {
|
||||
label: i18n.global.t('setting.panel'),
|
||||
path: '/settings/panel',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('setting.safe'),
|
||||
path: '/settings/safe',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(globalStore.hasPermission('alert_view')
|
||||
|
||||
@@ -107,16 +107,6 @@
|
||||
<span class="input-help">{{ $t('setting.systemIPHelper') }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('setting.proxy')" prop="proxyShow">
|
||||
<el-input disabled v-model="form.proxyShow">
|
||||
<template #append>
|
||||
<el-button @click="onChangeProxy" icon="Setting">
|
||||
{{ $t('commons.button.set') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="!isEnterprise"
|
||||
:label="$t('setting.developerMode')"
|
||||
@@ -155,7 +145,6 @@
|
||||
|
||||
<PanelName ref="panelNameRef" @search="search()" />
|
||||
<SystemIP ref="systemIPRef" @search="search()" />
|
||||
<Proxy ref="proxyRef" @search="search()" />
|
||||
<Timeout ref="timeoutRef" @search="search()" />
|
||||
<HideMenu ref="hideMenuRef" @search="search()" />
|
||||
</div>
|
||||
@@ -171,9 +160,7 @@ import { MsgSuccess } from '@/utils/message';
|
||||
import Timeout from '@/views/setting/panel/timeout/index.vue';
|
||||
import PanelName from '@/views/setting/panel/name/index.vue';
|
||||
import SystemIP from '@/views/setting/panel/systemip/index.vue';
|
||||
import Proxy from '@/views/setting/panel/proxy/index.vue';
|
||||
import HideMenu from '@/views/setting/panel/hidemenu/index.vue';
|
||||
import { getXpackProxyDocker } from '@/extensions/xpack';
|
||||
import { getXpackSetting, updateXpackSettingByKey } from '@/utils/xpack';
|
||||
import { setPrimaryColor } from '@/utils/theme';
|
||||
import { codeEditorThemeStorageKey } from '@/utils/code-editor-theme';
|
||||
@@ -220,15 +207,6 @@ const form = reactive({
|
||||
developerMode: '',
|
||||
systemIP: '',
|
||||
|
||||
proxyShow: '',
|
||||
proxyUrl: '',
|
||||
proxyType: '',
|
||||
proxyPort: '',
|
||||
proxyUser: '',
|
||||
proxyPasswd: '',
|
||||
proxyPasswdKeep: '',
|
||||
proxyDocker: '',
|
||||
|
||||
hideMenu: '',
|
||||
});
|
||||
|
||||
@@ -236,7 +214,6 @@ const show = ref();
|
||||
|
||||
const panelNameRef = ref();
|
||||
const systemIPRef = ref();
|
||||
const proxyRef = ref();
|
||||
const timeoutRef = ref();
|
||||
const hideMenuRef = ref();
|
||||
const watermarkRef = ref();
|
||||
@@ -278,22 +255,11 @@ const search = async () => {
|
||||
form.docSource = res.data.docSource || 'withByRegion';
|
||||
form.edition = res.data.edition;
|
||||
|
||||
form.proxyUrl = res.data.proxyUrl;
|
||||
form.proxyType = res.data.proxyType;
|
||||
form.proxyPort = res.data.proxyPort;
|
||||
form.proxyShow = form.proxyUrl ? form.proxyUrl + ':' + form.proxyPort : unset.value;
|
||||
form.proxyUser = res.data.proxyUser;
|
||||
form.proxyPasswd = res.data.proxyPasswd;
|
||||
form.proxyPasswdKeep = res.data.proxyPasswdKeep;
|
||||
|
||||
form.developerMode = res.data.developerMode;
|
||||
form.hideMenu = res.data.hideMenu;
|
||||
|
||||
if (isXpackOrEE.value) {
|
||||
const [xpackRes, proxyDockerRes] = await Promise.all([
|
||||
getXpackSetting(),
|
||||
getXpackProxyDocker().catch(() => null),
|
||||
]);
|
||||
const xpackRes = await getXpackSetting();
|
||||
if (xpackRes) {
|
||||
form.theme = xpackRes.data.theme || themeConfig.value.theme || 'light';
|
||||
form.themeColor = JSON.parse(xpackRes.data.themeColor || '{"light":"#005eeb","dark":"#F0BE96"}');
|
||||
@@ -309,7 +275,6 @@ const search = async () => {
|
||||
watermark.value = null;
|
||||
}
|
||||
}
|
||||
form.proxyDocker = proxyDockerRes?.data?.proxyDocker || '';
|
||||
} else {
|
||||
themeConfig.value.theme = form.theme;
|
||||
}
|
||||
@@ -324,18 +289,6 @@ const onChangeTimeout = () => {
|
||||
const onChangeSystemIP = () => {
|
||||
systemIPRef.value.acceptParams({ systemIP: form.systemIP });
|
||||
};
|
||||
const onChangeProxy = () => {
|
||||
proxyRef.value.acceptParams({
|
||||
url: form.proxyUrl,
|
||||
type: form.proxyType,
|
||||
port: form.proxyPort,
|
||||
user: form.proxyUser,
|
||||
passwd: form.proxyPasswd,
|
||||
passwdKeep: form.proxyPasswdKeep,
|
||||
proxyDocker: form.proxyDocker,
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeHideMenus = () => {
|
||||
hideMenuRef.value.acceptParams({
|
||||
hideMenu: form.hideMenu,
|
||||
|
||||
+52
-46
@@ -34,62 +34,58 @@ let
|
||||
password_file=${lib.escapeShellArg (if cfg.initialPasswordFile == null then "" else cfg.initialPasswordFile)}
|
||||
|
||||
install -d -m 0750 "$state_dir"
|
||||
if [ -s "$control_file" ]; then
|
||||
# Older nPanel generations created this file without the mandatory
|
||||
# upstream security-entrance setting. Repair that state in place while
|
||||
# keeping all user-owned panel data and credentials untouched.
|
||||
if ! grep -q '^ORIGINAL_ENTRANCE=' "$control_file"; then
|
||||
entrance="$(set +o pipefail; tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16)"
|
||||
if [ "''${#entrance}" -ne 16 ]; then
|
||||
echo "nPanel: failed to generate an initial security entrance" >&2
|
||||
if [ ! -s "$control_file" ]; then
|
||||
if [ -n "$password_file" ]; then
|
||||
password="$(cat "$password_file")"
|
||||
if [ -z "$password" ]; then
|
||||
echo "nPanel: initialPasswordFile is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '\nORIGINAL_ENTRANCE=%s\n' "$entrance" >> "$control_file"
|
||||
chmod 0600 "$control_file"
|
||||
(umask 077; printf '%s\n' "$entrance" > "$state_dir/initial-entrance")
|
||||
echo "nPanel created a one-time security entrance in $state_dir/initial-entrance" >&2
|
||||
else
|
||||
# `head` deliberately closes the random stream after 32 bytes. Do not
|
||||
# let that expected SIGPIPE be promoted by the script-wide pipefail.
|
||||
password="$(set +o pipefail; tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)"
|
||||
if [ "''${#password}" -ne 32 ]; then
|
||||
echo "nPanel: failed to generate an initial password" >&2
|
||||
exit 1
|
||||
fi
|
||||
(umask 077; printf '%s\n' "$password" > "$state_dir/initial-password")
|
||||
echo "nPanel created a one-time bootstrap password in $state_dir/initial-password" >&2
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$password_file" ]; then
|
||||
password="$(cat "$password_file")"
|
||||
if [ -z "$password" ]; then
|
||||
echo "nPanel: initialPasswordFile is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# `head` deliberately closes the random stream after 32 bytes. Do not
|
||||
# let that expected SIGPIPE be promoted by the script-wide pipefail.
|
||||
password="$(set +o pipefail; tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)"
|
||||
if [ "''${#password}" -ne 32 ]; then
|
||||
echo "nPanel: failed to generate an initial password" >&2
|
||||
exit 1
|
||||
fi
|
||||
(umask 077; printf '%s\n' "$password" > "$state_dir/initial-password")
|
||||
echo "nPanel created a one-time bootstrap password in $state_dir/initial-password" >&2
|
||||
fi
|
||||
|
||||
entrance="$(set +o pipefail; tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16)"
|
||||
if [ "''${#entrance}" -ne 16 ]; then
|
||||
echo "nPanel: failed to generate an initial security entrance" >&2
|
||||
exit 1
|
||||
fi
|
||||
(umask 077; printf '%s\n' "$entrance" > "$state_dir/initial-entrance")
|
||||
echo "nPanel created a one-time security entrance in $state_dir/initial-entrance" >&2
|
||||
|
||||
umask 077
|
||||
cat > "$control_file" <<EOF
|
||||
umask 077
|
||||
cat > "$control_file" <<EOF
|
||||
BASE_DIR=/var/lib/npanel
|
||||
ORIGINAL_PORT=${toString cfg.port}
|
||||
ORIGINAL_VERSION=${cfg.package.version or "unknown"}
|
||||
ORIGINAL_USERNAME=${cfg.initialUser}
|
||||
ORIGINAL_PASSWORD=$password
|
||||
ORIGINAL_ENTRANCE=$entrance
|
||||
ORIGINAL_ENTRANCE=${cfg.securityEntrance}
|
||||
LANGUAGE=${cfg.language}
|
||||
PANEL_EDITION=cn
|
||||
EOF
|
||||
chmod 0600 "$control_file"
|
||||
fi
|
||||
|
||||
# These are declarative NixOS settings. They must be rewritten on every
|
||||
# start so a panel-side database edit cannot survive the next activation.
|
||||
update_control_value() {
|
||||
key="$1"
|
||||
value="$2"
|
||||
tmp="$(mktemp "$state_dir/.npanel.conf.XXXXXX")"
|
||||
if grep -q "^$key=" "$control_file"; then
|
||||
sed "s|^$key=.*|$key=$value|" "$control_file" > "$tmp"
|
||||
else
|
||||
cat "$control_file" > "$tmp"
|
||||
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
||||
fi
|
||||
chmod 0600 "$tmp"
|
||||
mv "$tmp" "$control_file"
|
||||
}
|
||||
|
||||
update_control_value ORIGINAL_PORT ${lib.escapeShellArg (toString cfg.port)}
|
||||
update_control_value ORIGINAL_ENTRANCE ${lib.escapeShellArg cfg.securityEntrance}
|
||||
update_control_value MANAGED_PORT ${lib.escapeShellArg (toString cfg.port)}
|
||||
update_control_value MANAGED_ENTRANCE ${lib.escapeShellArg cfg.securityEntrance}
|
||||
'';
|
||||
|
||||
commonServiceConfig = {
|
||||
@@ -126,7 +122,7 @@ in
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = packageForSystem;
|
||||
defaultText = lib.literalExpression "inputs.npanel.packages.\${pkgs.system}.default";
|
||||
defaultText = lib.literalExpression "self.packages.\${pkgs.system}.default";
|
||||
description = "nPanel core 软件包。agent 与前端从该软件包的 passthru 属性取得。";
|
||||
};
|
||||
|
||||
@@ -145,7 +141,13 @@ in
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 9999;
|
||||
description = "首次初始化时写入 nPanel 数据库的监听端口;之后请在 nPanel 自身设置中调整。";
|
||||
description = "nPanel Web 服务监听端口。此值由 NixOS 持续接管,不能在面板中修改。";
|
||||
};
|
||||
|
||||
securityEntrance = mkOption {
|
||||
type = types.strMatching "[A-Za-z0-9]{5,116}";
|
||||
example = "npanelAccess";
|
||||
description = "nPanel 安全入口路径后缀(不含斜杠)。此值由 NixOS 持续接管,不能在面板中修改。";
|
||||
};
|
||||
|
||||
initialUser = mkOption {
|
||||
@@ -178,6 +180,10 @@ in
|
||||
assertion = cfg.stateDir != cfg.runtimeDir;
|
||||
message = "services.nPanel.stateDir 与 runtimeDir 必须是不同目录。";
|
||||
}
|
||||
{
|
||||
assertion = cfg.securityEntrance != "";
|
||||
message = "services.nPanel.securityEntrance 是必填项,且必须为 5 到 116 位字母或数字。";
|
||||
}
|
||||
];
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
|
||||
Reference in New Issue
Block a user