5 Commits

Author SHA1 Message Date
GL.iNet-Yongping.Xie 133f344713 Merge branch 'bugfix/v2.1.0' 2026-02-27 18:28:24 -08:00
GL.iNet-Yongping.Xie 7fd9c39ffb fix: resolve issues in v2.1.0
Fix reported bugs and stability issues in v2.1.0.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2026-02-27 18:27:43 -08:00
pengyu.lu 96936ecdd8 Merge branch 'dev-ui-0129' 2026-02-28 10:10:40 +08:00
pengyu.lu a44c9a4833 fix: Fix some UI issues 2026-02-28 10:10:11 +08:00
GL.iNet-Yongping.Xie e6da02f711 feat: allow customizing system admin username
Add support to change the default system admin login name via config file.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2026-02-25 18:23:43 -08:00
15 changed files with 81 additions and 19 deletions
+3
View File
@@ -71,6 +71,9 @@ GLKVM_ACCESS_IP=
# rttys
RTTYS_TOKEN=DeviceTokenYouCanChangeMe
RTTYS_PASS=StrongP@ssw0rd
# Admin username (leave empty to default to "admin")
# Only letters and digits are allowed (e.g. admin, Admin01). No spaces or special characters.
RTTYS_ADMIN_NAME=
RTTYS_DEVICE_PORT=5912
RTTYS_WEBUI_PORT=443
RTTYS_HTTP_PROXY_PORT=10443
+3
View File
@@ -70,6 +70,9 @@ GLKVM_ACCESS_IP=
# rttys
RTTYS_TOKEN=DeviceTokenYouCanChangeMe
RTTYS_PASS=StrongP@ssw0rd
# Admin username (leave empty to default to "admin")
# Only letters and digits are allowed (e.g. admin, Admin01). No spaces or special characters.
RTTYS_ADMIN_NAME=
RTTYS_DEVICE_PORT=5912
RTTYS_WEBUI_PORT=443
RTTYS_HTTP_PROXY_PORT=10443
+1
View File
@@ -12,6 +12,7 @@ services:
# ---- rttys ----
RTTYS_TOKEN: ${RTTYS_TOKEN:-DeviceTokenYouCanChangeMe}
RTTYS_PASS: ${RTTYS_PASS:-StrongP@ssw0rd}
RTTYS_ADMIN_NAME: ${RTTYS_ADMIN_NAME:-}
# Ports inside container (mirrored to host via `ports` below)
RTTYS_DEVICE_PORT: ${RTTYS_DEVICE_PORT:-5912} # addr-dev
+1 -1
View File
@@ -60,7 +60,7 @@ case "$1" in
: "${TURN_PORT:=3478}"
render /tpl/rttys.conf.tmpl /home/rttys.conf \
RTTYS_TOKEN RTTYS_PASS \
RTTYS_TOKEN RTTYS_PASS RTTYS_ADMIN_NAME \
GLKVM_ACCESS_IP TURN_PORT TURN_USER TURN_PASS \
RTTYS_DEVICE_PORT RTTYS_WEBUI_PORT RTTYS_HTTP_PROXY_PORT \
LDAP_ENABLED LDAP_SERVER LDAP_PORT LDAP_USE_TLS \
@@ -4,6 +4,9 @@ token: {{RTTYS_TOKEN}}
# Web management password
password: {{RTTYS_PASS}}
# Admin username (leave empty to default to "admin")
admin-name: {{RTTYS_ADMIN_NAME}}
# WebRTC
webrtc-ip: {{GLKVM_ACCESS_IP}}
webrtc-port: {{TURN_PORT}}
+12
View File
@@ -161,6 +161,18 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
return
}
target, err := h.userSvc.FindByID(c.Request.Context(), id)
if err != nil {
dto.Write(c, dto.Err(traceID, dto.CodeNotFound, "Not found", nil))
return
}
if target.IsSystem {
req.Username = nil
req.Role = nil
req.Password = nil
req.Repassword = nil
}
if err := h.userSvc.UpdateUser(c.Request.Context(), id, req.Username, req.Description, req.Password, req.Role, nil); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
dto.Write(c, dto.Err(traceID, dto.CodeNotFound, "Not found", nil))
+19 -5
View File
@@ -80,7 +80,7 @@ func InitAppContainer(r *gin.Engine) (*AppContainer, error) {
if err := sqlite.InitSchema(ctx, appDB.SQL(), "/home/database/schema.sql"); err != nil {
log.Fatal().Err(err).Msg("init schema failed")
}
if err := ensureAdminUser(ctx, appDB.Gorm(), cfg.Password); err != nil {
if err := ensureAdminUser(ctx, appDB.Gorm(), cfg.AdminName, cfg.Password); err != nil {
log.Fatal().Err(err).Msg("ensure admin user failed")
}
@@ -117,7 +117,7 @@ func InitAppContainer(r *gin.Engine) (*AppContainer, error) {
return c, nil
}
func ensureAdminUser(ctx context.Context, db *gorm.DB, plainPassword string) error {
func ensureAdminUser(ctx context.Context, db *gorm.DB, adminName, plainPassword string) error {
if db == nil {
return fmt.Errorf("db is nil")
}
@@ -126,15 +126,29 @@ func ensureAdminUser(ctx context.Context, db *gorm.DB, plainPassword string) err
if err != nil {
return err
}
// First, rename the existing system admin user to the configured name (if changed).
// This handles the case where the admin username was previously "admin" (or another name)
// and the user now wants a different username via RTTYS_ADMIN_NAME.
if err := db.WithContext(ctx).Exec(
`UPDATE users SET username = ? WHERE is_system = 1 AND role = 'admin' AND username != ?`,
adminName, adminName,
).Error; err != nil {
return fmt.Errorf("rename system admin user: %w", err)
}
// Upsert: create the admin user if not exists, or update password/role/status.
// On conflict, also set description to 'System Administrator' if it is currently empty.
return db.WithContext(ctx).Exec(
`INSERT INTO users (username, description, password_hash, role, status, is_system)
VALUES ('admin', 'Admin', ?, 'admin', 'active', 1)
VALUES (?, 'System Administrator', ?, 'admin', 'active', 1)
ON CONFLICT(username) DO UPDATE SET
password_hash=excluded.password_hash,
role='admin',
status='active',
is_system=1`,
hash,
is_system=1,
description=CASE WHEN (description IS NULL OR description = '') THEN 'System Administrator' ELSE description END`,
adminName, hash,
).Error
}
+1 -1
View File
@@ -1,7 +1,7 @@
package server
const RttysVersion = "5.2.0"
const KVMCloudVersion = "v2.0.0"
const KVMCloudVersion = "v2.2.0"
var (
GitCommit = ""
+4 -2
View File
@@ -2,7 +2,7 @@
* @Author: shufei.han
* @Date: 2025-06-10 16:46:00
* @LastEditors: LPY
* @LastEditTime: 2026-01-30 17:41:49
* @LastEditTime: 2026-02-28 09:21:41
* @FilePath: \glkvm-cloud\ui\src\stores\modules\device.ts
* @Description: 设备有关的状态管理
*/
@@ -77,7 +77,9 @@ export const useDeviceStore = defineStore('device', () => {
}
pageLink.value.setTotal(res.data.items.length)
state.deviceList = res.data.items.filter(d => {
return (d?.id?.toString().toLowerCase()?.indexOf(computedDeviceQuery.value.searchText) > -1
return (d?.ddns?.toString().toLowerCase()?.indexOf(computedDeviceQuery.value.searchText) > -1
|| d?.mac?.toString().toLowerCase()?.indexOf(computedDeviceQuery.value.searchText) > -1
|| d?.ip?.toString().toLowerCase()?.indexOf(computedDeviceQuery.value.searchText) > -1
|| d?.description?.toLowerCase()?.indexOf(computedDeviceQuery.value.searchText) > -1) &&
(computedDeviceQuery.value.deviceGroupId ? d.deviceGroupId === computedDeviceQuery.value.deviceGroupId : true) &&
(!computedDeviceQuery.value.onlyShowUnassigned || (computedDeviceQuery.value.onlyShowUnassigned && !d.deviceGroupId))
+2 -3
View File
@@ -2,7 +2,7 @@
* @Author: LPY
* @Date: 2026-01-30 10:19:24
* @LastEditors: LPY
* @LastEditTime: 2026-02-04 10:58:29
* @LastEditTime: 2026-02-28 09:28:39
* @FilePath: \glkvm-cloud\ui\src\stores\modules\deviceGroup.ts
* @Description: 设备组有关的状态管理
*/
@@ -63,8 +63,7 @@ export const useDeviceGroupStore = defineStore('deviceGroup', () => {
const res = await reqDeviceGroupList()
pageLink.value.setTotal(res.data.items.length)
state.deviceGroupList = res.data.items.filter(d => {
return (d?.id?.toString()?.indexOf(computedDeviceGroupQuery.value.searchText) > -1
|| d?.description?.indexOf(computedDeviceGroupQuery.value.searchText) > -1
return (d?.description?.indexOf(computedDeviceGroupQuery.value.searchText) > -1
|| d?.name?.indexOf(computedDeviceGroupQuery.value.searchText) > -1)
}) || []
state.completeDeviceGroupList = res.data.items || []
+2 -2
View File
@@ -2,7 +2,7 @@
* @Author: LPY
* @Date: 2026-02-03 12:07:45
* @LastEditors: LPY
* @LastEditTime: 2026-02-04 10:56:54
* @LastEditTime: 2026-02-28 09:27:05
* @FilePath: \glkvm-cloud\ui\src\stores\modules\userGroupManage.ts
* @Description: 用户组管理相关状态管理
*/
@@ -65,7 +65,7 @@ export const useUserGroupManageStore = defineStore('userGroupManage', () => {
const res = await reqUserGroupList()
pageLink.value.setTotal(res.data.items.length)
state.userGroupList = res.data.items.filter(d => {
return (d?.id?.toString()?.indexOf(computedUserGroupManageQuery.value.searchText) > -1
return (d?.description?.toString()?.toLowerCase()?.indexOf(computedUserGroupManageQuery.value.searchText) > -1
|| d?.userGroup?.indexOf(computedUserGroupManageQuery.value.searchText) > -1) &&
(computedUserGroupManageQuery.value.deviceGroupId ?
d.deviceGroupList.some(u => u.deviceGroupId === computedUserGroupManageQuery.value.deviceGroupId) : true)
+2 -2
View File
@@ -2,7 +2,7 @@
* @Author: LPY
* @Date: 2026-02-02 15:00:13
* @LastEditors: LPY
* @LastEditTime: 2026-02-03 12:08:51
* @LastEditTime: 2026-02-28 09:26:50
* @FilePath: \glkvm-cloud\ui\src\stores\modules\userManage.ts
* @Description: 用户管理相关状态管理
*/
@@ -69,7 +69,7 @@ export const useUserManageStore = defineStore('userManage', () => {
const res = await reqUserList()
pageLink.value.setTotal(res.data.items.length)
state.userList = res.data.items.filter(d => {
return (d?.id?.toString().toLowerCase()?.indexOf(computedUserManageQuery.value.searchText) > -1
return (d?.description?.toString().toLowerCase()?.indexOf(computedUserManageQuery.value.searchText) > -1
|| d?.username?.toLowerCase()?.indexOf(computedUserManageQuery.value.searchText) > -1) &&
(computedUserManageQuery.value.userGroupId ? d.userGroupList.some(u => u.userGroupId === computedUserManageQuery.value.userGroupId) : true)
}) || []
+6 -1
View File
@@ -2,7 +2,7 @@
* @Author: LPY
* @Date: 2025-05-30 10:48:43
* @LastEditors: LPY
* @LastEditTime: 2026-02-05 17:34:48
* @LastEditTime: 2026-02-28 09:32:36
* @FilePath: \glkvm-cloud\ui\src\views\login\loginPage.vue
* @Description: 登录页面
-->
@@ -137,6 +137,11 @@ onMounted(async () => {
// 提取配置数据 (Extract config data)
authConfig.value = response.data
useAppStore().setVersion(authConfig.value.kvmCloudVersion)
// 若支持LDAP且当前登录方式为legacy,则切换到ldap (If LDAP is supported and current auth method is legacy, switch to ldap)
if (authConfig.value.ldapEnabled && state.formModel.authMethod === 'legacy') {
state.formModel.authMethod = 'ldap'
}
} catch (error) {
console.error('Failed to load auth config:', error)
// 回退 - 无LDAP可用 (Fallback - no LDAP available)
@@ -2,7 +2,7 @@
* @Author: LPY
* @Date: 2026-02-03 11:24:20
* @LastEditors: LPY
* @LastEditTime: 2026-02-09 09:14:59
* @LastEditTime: 2026-02-28 09:51:05
* @FilePath: \glkvm-cloud\ui\src\views\userManage\components\editUserDialog.vue
* @Description: 编辑用户弹窗
-->
@@ -44,7 +44,12 @@
</ARadioGroup>
</AFormItem>
<AFormItem name="username" :label="$t('user.userName')" labelAlign="left">
<AInput v-model:value="state.formData.username" :maxlength="32" :placeholder="$t('device.requiredDeviceGroupName')" style="width: 100%;" />
<AInput
v-model:value="state.formData.username"
:maxlength="32"
:placeholder="$t('device.requiredDeviceGroupName')"
:disabled="props.currentUser?.isSystem"
style="width: 100%;" />
</AFormItem>
<AFormItem name="description" :label="$t('device.description')" labelAlign="left">
<ATextarea
@@ -58,6 +63,7 @@
v-model:value="state.formData.password"
:placeholder="$t('user.enterPassword')"
autocomplete="off"
:disabled="props.currentUser?.isSystem"
style="width: 100%;" />
</AFormItem>
<AFormItem name="repassword" :label="$t('user.reEnterPassword')" labelAlign="left">
@@ -65,6 +71,7 @@
v-model:value="state.formData.repassword"
:placeholder="$t('user.reEnterPasswordPlc')"
autocomplete="off"
:disabled="props.currentUser?.isSystem"
style="width: 100%;" />
</AFormItem>
<div class="flex-end">
+13
View File
@@ -27,6 +27,7 @@ package xconfig
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -45,6 +46,7 @@ type Config struct {
UserHookUrl string
LocalAuth bool
Password string
AdminName string
AllowOrigins bool
PprofAddr string
AuthSessionTTL time.Duration
@@ -168,6 +170,7 @@ func parseYamlCfg(cfg *Config, conf string) error {
getConfigOpt(yamlCfg, "user-hook-url", &cfg.UserHookUrl)
getConfigOpt(yamlCfg, "local-auth", &cfg.LocalAuth)
getConfigOpt(yamlCfg, "password", &cfg.Password)
getConfigOpt(yamlCfg, "admin-name", &cfg.AdminName)
getConfigOpt(yamlCfg, "allow-origins", &cfg.AllowOrigins)
if err := getDurationOpt(yamlCfg, "auth-session-ttl", &cfg.AuthSessionTTL); err != nil {
@@ -237,6 +240,16 @@ func parseYamlCfg(cfg *Config, conf string) error {
}
func applyEnvCfg(cfg *Config) error {
if v := strings.TrimSpace(os.Getenv("RTTYS_ADMIN_NAME")); v != "" {
cfg.AdminName = v
}
if cfg.AdminName == "" {
cfg.AdminName = "admin"
}
if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(cfg.AdminName) {
return fmt.Errorf("invalid RTTYS_ADMIN_NAME %q: only letters and digits are allowed", cfg.AdminName)
}
if v := strings.TrimSpace(os.Getenv("RTTYS_LOG")); v != "" {
cfg.LogPath = v
}