mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-10-07 21:40:51 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 133f344713 | |||
| 7fd9c39ffb | |||
| 96936ecdd8 | |||
| a44c9a4833 | |||
| e6da02f711 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
@@ -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,7 +1,7 @@
|
||||
package server
|
||||
|
||||
const RttysVersion = "5.2.0"
|
||||
const KVMCloudVersion = "v2.0.0"
|
||||
const KVMCloudVersion = "v2.2.0"
|
||||
|
||||
var (
|
||||
GitCommit = ""
|
||||
|
||||
@@ -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,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,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,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)
|
||||
}) || []
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user