diff --git a/core/cmd/server/cmd/reset.go b/core/cmd/server/cmd/reset.go index 0fba632bf9d0..a7bea40a3480 100644 --- a/core/cmd/server/cmd/reset.go +++ b/core/cmd/server/cmd/reset.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "strings" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/i18n" @@ -17,6 +18,7 @@ func init() { RootCmd.AddCommand(resetCmd) resetCmd.AddCommand(resetMFACmd) + resetMFACmd.Flags().StringVar(&resetMFAUserNameFlag, "username", "", "username") resetCmd.AddCommand(resetSSLCmd) resetCmd.AddCommand(resetEntranceCmd) resetCmd.AddCommand(resetBindIpsCmd) @@ -24,6 +26,8 @@ func init() { resetCmd.AddCommand(resetPasskeyCmd) } +var resetMFAUserNameFlag string + var resetCmd = &cobra.Command{ Use: "reset", RunE: func(cmd *cobra.Command, args []string) error { @@ -41,6 +45,29 @@ var resetMFACmd = &cobra.Command{ fmt.Println(i18n.GetMsgWithMapForCmd("SudoHelper", map[string]interface{}{"cmd": "sudo 1pctl reset mfa"})) return nil } + if isEnterprise() && len(strings.TrimSpace(resetMFAUserNameFlag)) == 0 { + fmt.Println(i18n.GetMsgByKeyForCmd("UsernameNeed")) + return nil + } + if isEnterprise() { + db, err := loadDBConn("enterprise.db") + if err != nil { + return err + } + username := strings.TrimSpace(resetMFAUserNameFlag) + var count int64 + if err := db.Table("users").Where("name = ?", username).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fmt.Errorf("%s: %s", i18n.GetMsgByKeyForCmd("ErrRecordNotFound"), strings.TrimSpace(resetMFAUserNameFlag)) + } + result := db.Exec("UPDATE users SET mfa_status = ? WHERE name = ?", constant.StatusDisable, username) + if result.Error != nil { + return result.Error + } + return nil + } db, err := loadDBConn("core.db") if err != nil { return err diff --git a/core/cmd/server/cmd/update.go b/core/cmd/server/cmd/update.go index 51297f860796..7ee2466298f5 100644 --- a/core/cmd/server/cmd/update.go +++ b/core/cmd/server/cmd/update.go @@ -60,7 +60,7 @@ var updateUserName = &cobra.Command{ return nil } if isEnterprise() && len(strings.TrimSpace(updateUserNameFlag)) == 0 { - fmt.Println(i18n.GetMsgByKey("UsernameNeed")) + fmt.Println(i18n.GetMsgByKeyForCmd("UsernameNeed")) return nil } username() @@ -77,7 +77,7 @@ var updatePassword = &cobra.Command{ return nil } if isEnterprise() && len(strings.TrimSpace(updatePasswordUserName)) == 0 { - fmt.Println(i18n.GetMsgByKey("UsernameNeed")) + fmt.Println(i18n.GetMsgByKeyForCmd("UsernameNeed")) return nil } password() @@ -268,16 +268,18 @@ func password() { return } if err := updateEnterprisePassword(enterpriseDB, strings.TrimSpace(updatePasswordUserName), p); err != nil { - fmt.Println("\n", i18n.GetMsgWithMapForCmd("UpdatePortErr", map[string]interface{}{"err": err.Error()})) + fmt.Println("\n", i18n.GetMsgWithMapForCmd("UpdatePasswordErr", map[string]interface{}{"err": err.Error()})) return } } else if err := setSettingByKey(db, "Password", p); err != nil { - fmt.Println("\n", i18n.GetMsgWithMapForCmd("UpdatePortErr", map[string]interface{}{"err": err.Error()})) + fmt.Println("\n", i18n.GetMsgWithMapForCmd("UpdatePasswordErr", map[string]interface{}{"err": err.Error()})) return } - username := getSettingByKey(db, "UserName") + username := "" if isEnterprise() { username = strings.TrimSpace(updatePasswordUserName) + } else { + username = getSettingByKey(db, "UserName") } fmt.Println("\n" + i18n.GetMsgByKeyForCmd("UpdateSuccessful")) diff --git a/core/cmd/server/cmd/user.go b/core/cmd/server/cmd/user.go new file mode 100644 index 000000000000..29b11e7a96a1 --- /dev/null +++ b/core/cmd/server/cmd/user.go @@ -0,0 +1,221 @@ +package cmd + +import ( + "fmt" + "time" + + "github.com/1Panel-dev/1Panel/core/i18n" + "github.com/spf13/cobra" + "gorm.io/gorm" +) + +func init() { + RootCmd.AddCommand(userListCmd) +} + +var userListCmd = &cobra.Command{ + Use: "user-list", + Short: i18n.GetMsgByKeyForCmd("UserList"), + RunE: func(cmd *cobra.Command, args []string) error { + i18n.UseI18nForCmd(language) + if !isRoot() { + fmt.Println(i18n.GetMsgWithMapForCmd("SudoHelper", map[string]interface{}{"cmd": "sudo 1pctl user-list"})) + return nil + } + if isEnterprise() { + db, err := loadDBConn("enterprise.db") + if err != nil { + return err + } + return listEnterpriseUsers(db) + } + db, err := loadDBConn("core.db") + if err != nil { + return err + } + return listCommunityUsers(db) + }, +} + +func listEnterpriseUsers(db *gorm.DB) error { + var userModels []enterpriseUserModel + if err := db.Order("created_at desc").Find(&userModels).Error; err != nil { + return err + } + + rows := make([]enterpriseUserRow, 0, len(userModels)) + for _, user := range userModels { + superAdmin := "no" + if user.IsSuperAdmin { + superAdmin = "yes" + } + rows = append(rows, enterpriseUserRow{ + Name: user.Name, + MFAStatus: user.MFAStatus, + SuperAdmin: superAdmin, + CreatedAt: user.CreatedAt.Format(time.RFC3339), + }) + } + + if len(rows) == 0 { + fmt.Println(i18n.GetMsgByKeyForCmd("UserEmptyList")) + return nil + } + + return printUserRows(rows) +} + +func listCommunityUsers(db *gorm.DB) error { + nameSetting, err := loadSettingRecord(db, "UserName") + if err != nil { + return err + } + mfaSetting, err := loadSettingRecord(db, "MFAStatus") + if err != nil { + return err + } + + rows := []enterpriseUserRow{ + { + Name: nameSetting.Value, + MFAStatus: mfaSetting.Value, + SuperAdmin: "yes", + CreatedAt: createdAtString(nameSetting.CreatedAt), + }, + } + + return printUserRows(rows) +} + +func printUserRows(rows []enterpriseUserRow) error { + headers := []string{ + i18n.GetMsgByKeyForCmd("UserTableName"), + i18n.GetMsgByKeyForCmd("UserTableMFAStatus"), + i18n.GetMsgByKeyForCmd("UserTableSuperAdmin"), + i18n.GetMsgByKeyForCmd("UserTableCreatedAt"), + } + widths := make([]int, len(headers)) + for i, header := range headers { + widths[i] = displayWidth(header) + } + for _, row := range rows { + values := []string{row.Name, row.MFAStatus, row.SuperAdmin, row.CreatedAt} + for i, value := range values { + if w := displayWidth(value); w > widths[i] { + widths[i] = w + } + } + } + fmt.Println(joinColumns(headers, widths)) + for _, row := range rows { + fmt.Println(joinColumns([]string{row.Name, row.MFAStatus, row.SuperAdmin, row.CreatedAt}, widths)) + } + return nil +} + +func loadSettingRecord(db *gorm.DB, key string) (setting, error) { + var record setting + if err := db.Where("key = ?", key).First(&record).Error; err != nil { + return setting{}, err + } + return record, nil +} + +func createdAtString(t time.Time) string { + if t.IsZero() { + return "-" + } + return t.Format(time.RFC3339) +} + +type enterpriseUserRow struct { + Name string + MFAStatus string + SuperAdmin string + CreatedAt string +} + +type enterpriseUserModel struct { + Name string + MFAStatus string + IsSuperAdmin bool + CreatedAt time.Time +} + +func joinColumns(values []string, widths []int) string { + out := "" + for i, value := range values { + out += padDisplayWidth(value, widths[i]) + if i != len(values)-1 { + out += " " + } + } + return out +} + +func padDisplayWidth(value string, width int) string { + pad := width - displayWidth(value) + if pad <= 0 { + return value + } + return value + spaces(pad) +} + +func spaces(n int) string { + if n <= 0 { + return "" + } + return fmt.Sprintf("%*s", n, "") +} + +func displayWidth(s string) int { + width := 0 + for _, r := range s { + width += runeDisplayWidth(r) + } + return width +} + +func runeDisplayWidth(r rune) int { + if r == 0 { + return 0 + } + if r < 32 || (r >= 0x7f && r < 0xa0) { + return 0 + } + if isWideRune(r) { + return 2 + } + return 1 +} + +func isWideRune(r rune) bool { + if r < 0x1100 { + return false + } + if r <= 0x115f || r == 0x2329 || r == 0x232a { + return true + } + if r >= 0x2e80 && r <= 0xa4cf && r != 0x303f { + return true + } + if r >= 0xac00 && r <= 0xd7a3 { + return true + } + if r >= 0xf900 && r <= 0xfaff { + return true + } + if r >= 0xfe10 && r <= 0xfe19 { + return true + } + if r >= 0xfe30 && r <= 0xfe6f { + return true + } + if r >= 0xff00 && r <= 0xff60 { + return true + } + if r >= 0xffe0 && r <= 0xffe6 { + return true + } + return r >= 0x20000 && r <= 0x3fffd +} diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index e5fe4ee9fdbf..ce790b9af4d2 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -208,6 +208,7 @@ AppVersionExist: "Version already exists!" AppCreateSuccessful: "Creation successful!" AppWriteErr: "File {{ .name }} write failed {{ .err }}" SudoHelper: "Use {{ .cmd }} or switch to root user" +UsernameNeed: "Enterprise edition must specify --username" ListenIPCommands: "Switch listening ip" ListenIPv4: "Listen on IPv4" ListenIPv6: "Listen on IPv6" @@ -219,6 +220,12 @@ ResetEntrance: "Cancel 1Panel secure entrance" ResetIPs: "Cancel 1Panel authorized ip restrictions" ResetDomain: "Cancel 1Panel domain binding" ResetPasskey: "Clear 1Panel passkeys" +UserList: "Get 1Panel user list" +UserTableName: "Name" +UserTableMFAStatus: "MFA Status" +UserTableSuperAdmin: "Super Admin" +UserTableCreatedAt: "Created At" +UserEmptyList: "No users found" RestoreCommands: "Roll back 1Panel service and data" RestoreNoSuchFile: "No rollback files found" RestoreStep1: "(1/5) Starting rollback of 1Panel service and data from {{ .name }} directory..." diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index 89de2073a38b..0c7e1b64a1da 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -202,6 +202,7 @@ AppVersionExist: "La versión ya existe" AppCreateSuccessful: "Creación finalizada con éxito" AppWriteErr: "Error al escribir el archivo {{ .name }} {{ .err }}" SudoHelper: "Por favor use {{ .cmd }} o cambie al usuario root" +UsernameNeed: "La edición Enterprise debe especificar --username" ListenIPCommands: "Cambiar ip de escucha" ListenIPv4: "Escuchar en IPv4" ListenIPv6: "Escuchar en IPv6" @@ -213,6 +214,12 @@ ResetEntrance: "Cancelar entrada segura de 1Panel" ResetIPs: "Cancelar restricciones de IP autorizadas de 1Panel" ResetDomain: "Cancelar vinculación de dominio de 1Panel" ResetPasskey: "Eliminar passkeys de 1Panel" +UserList: "Obtener la lista de usuarios de 1Panel" +UserTableName: "Nombre" +UserTableMFAStatus: "Estado MFA" +UserTableSuperAdmin: "Superadministrador" +UserTableCreatedAt: "Creado en" +UserEmptyList: "No se encontraron usuarios" RestoreCommands: "Revertir el servicio y los datos de 1Panel" RestoreNoSuchFile: "No se encontraron archivos de reversión" RestoreStep1: "(1/5) Iniciando la reversión del servicio y los datos de 1Panel desde el directorio {{ .name }}..." diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index 7f692d689d2c..88e02390b872 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -202,6 +202,7 @@ AppVersionExist: "バージョンはすでに存在します!" AppCreateSuccessful: "創造は成功しました!" AppWriteErr: "file {{ .name }} write failed {{ .err }}" SudoHelper: "{{.cmd}}を使用するか、ルートユーザーに切り替えてください" +UsernameNeed: "Enterprise版では --username を指定する必要があります" ListenIPCommands: "リスニングIPを切り替えます" ListenIPv4: "IPv4で聞いてください" ListenIPv6: "IPv6で聞く" @@ -213,6 +214,12 @@ ResetEntrance: "1パネルの安全な入り口をキャンセルします" ResetIPs: "1パネル認定IP制限をキャンセルします" ResetDomain: "1パネルドメインバインディングをキャンセルします" ResetPasskey: "1Panel のパスキーをクリアします" +UserList: "1Panel ユーザー一覧を取得します" +UserTableName: "名前" +UserTableMFAStatus: "MFA ステータス" +UserTableSuperAdmin: "スーパー管理者" +UserTableCreatedAt: "作成日時" +UserEmptyList: "ユーザーが見つかりません" RestoreCommands: "1Panel のサービスとデータをロールバック" RestoreNoSuchFile: "ロールバックに使えるファイルが見つかりません" RestoreStep1: "(1/5){{ .name }} ディレクトリから 1Panel のサービスとデータのロールバックを開始しています..." diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 97dc78f1cbc3..3d42ed2ad5fe 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -201,6 +201,7 @@ AppVersionExist: "버전이 이미 존재합니다" AppCreateSuccessful: "생성 성공" AppWriteErr: "파일 {{ .name }} 쓰기 실패 {{ .err }}" SudoHelper: "{{ .cmd }} 명령을 사용하거나 root 사용자로 전환하세요." +UsernameNeed: "Enterprise 버전은 --username을 지정해야 합니다" ListenIPCommands: "IP 변경" ListenIPv4: "IPv4 에서 수신 대기" ListenIPv6: "IPv6 에서 수신 대기" @@ -212,6 +213,12 @@ ResetEntrance: "1Panel 보안 입구 취소" ResetIPs: "1Panel 허용 IP 제한 취소" ResetDomain: "1Panel 도메인 바인딩 취소" ResetPasskey: "1Panel 패스키 초기화" +UserList: "1Panel 사용자 목록 가져오기" +UserTableName: "이름" +UserTableMFAStatus: "MFA 상태" +UserTableSuperAdmin: "슈퍼 관리자" +UserTableCreatedAt: "생성일" +UserEmptyList: "사용자를 찾을 수 없습니다" RestoreCommands: "1Panel 서비스와 데이터를 롤백" RestoreNoSuchFile: "롤백 파일을 찾을 수 없습니다" RestoreStep1: "(1/5) {{ .name }} 디렉터리에서 1Panel 서비스와 데이터의 롤백을 시작합니다..." diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml index 2550008719a9..b33fd0dd37c7 100644 --- a/core/i18n/lang/ms.yaml +++ b/core/i18n/lang/ms.yaml @@ -196,6 +196,7 @@ AppVersionExist: "Versi sudah wujud" AppCreateSuccessful: "Ciptaan berjaya" AppWriteErr: "Penulisan fail {{ .name }} gagal {{ .err }}" SudoHelper: "Sila gunakan {{ .cmd }} atau tukar ke pengguna root" +UsernameNeed: "Edisi Enterprise mesti menentukan --username" ListenIPCommands: "Tukar IP mendengar" ListenIPv4: "Mendengar pada IPv4" ListenIPv6: "Mendengar pada IPv6" @@ -207,6 +208,12 @@ ResetEntrance: "Batal pintu masuk keselamatan 1Panel" ResetIPs: "Batal sekatan IP yang dibenarkan 1Panel" ResetDomain: "Batal pengikatan domain 1Panel" ResetPasskey: "Kosongkan passkey 1Panel" +UserList: "Dapatkan senarai pengguna 1Panel" +UserTableName: "Nama" +UserTableMFAStatus: "Status MFA" +UserTableSuperAdmin: "Pentadbir Super" +UserTableCreatedAt: "Dicipta Pada" +UserEmptyList: "Tiada pengguna ditemui" RestoreCommands: "Rollback perkhidmatan dan data 1Panel" RestoreNoSuchFile: "Tiada fail rollback ditemui" RestoreStep1: "(1/5) Memulakan rollback perkhidmatan dan data 1Panel daripada direktori {{ .name }}..." diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index 875832030f7d..0d75e7cb41a4 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -201,6 +201,7 @@ AppVersionExist: "A versão já existe" AppCreateSuccessful: "Criação bem-sucedida" AppWriteErr: "Falha ao escrever no arquivo {{ .name }} {{ .err }}" SudoHelper: "Por favor, use {{ .cmd }} ou altere para o usuário root" +UsernameNeed: "A edição Enterprise deve especificar --username" ListenIPCommands: "Trocar IP de escuta" ListenIPv4: "Escutando em IPv4" ListenIPv6: "Escutando em IPv6" @@ -212,6 +213,12 @@ ResetEntrance: "Cancelar entrada segura do 1Panel" ResetIPs: "Cancelar restrições de IP autorizado do 1Panel" ResetDomain: "Cancelar vinculação de domínio do 1Panel" ResetPasskey: "Redefinir passkeys do 1Panel" +UserList: "Obter a lista de usuários do 1Panel" +UserTableName: "Nome" +UserTableMFAStatus: "Status MFA" +UserTableSuperAdmin: "Superadministrador" +UserTableCreatedAt: "Criado em" +UserEmptyList: "Nenhum usuário encontrado" RestoreCommands: "Fazer rollback do serviço e dos dados do 1Panel" RestoreNoSuchFile: "Nenhum arquivo de rollback encontrado" RestoreStep1: "(1/5) Iniciando o rollback do serviço e dos dados do 1Panel a partir do diretório {{ .name }}..." diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index b5b9ee8a2577..cea4375245fa 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -200,6 +200,7 @@ AppVersionExist: "Версия уже существует" AppCreateSuccessful: "Успешно создано" AppWriteErr: "Не удалось записать файл {{ .name }} {{ .err }}" SudoHelper: "Пожалуйста, используйте {{ .cmd }} или переключитесь на пользователя root" +UsernameNeed: "Для Enterprise-версии необходимо указать --username" ListenIPCommands: "Переключить IP для прослушивания" ListenIPv4: "Прослушивание IPv4" ListenIPv6: "Прослушивание IPv6" @@ -211,6 +212,12 @@ ResetEntrance: "Отменить безопасный вход 1Panel" ResetIPs: "Отменить ограничения авторизованных IP для 1Panel" ResetDomain: "Отменить привязку домена доступа 1Panel" ResetPasskey: "Сбросить passkey 1Panel" +UserList: "Получить список пользователей 1Panel" +UserTableName: "Имя" +UserTableMFAStatus: "Статус MFA" +UserTableSuperAdmin: "Суперадмин" +UserTableCreatedAt: "Создано" +UserEmptyList: "Пользователи не найдены" RestoreCommands: "Откатить сервис и данные 1Panel" RestoreNoSuchFile: "Файлы для отката не найдены" RestoreStep1: "(1/5) Начинаем откат сервиса и данных 1Panel из каталога {{ .name }}..." diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index f07b43b10980..cc7152b0bc33 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -200,6 +200,7 @@ AppVersionExist: "Sürüm zaten mevcut" AppCreateSuccessful: "Oluşturma başarılı" AppWriteErr: "Dosya {{ .name }} yazma başarısız {{ .err }}" SudoHelper: "Lütfen {{ .cmd }} kullanın veya root kullanıcısına geçin" +UsernameNeed: "Enterprise sürümü --username belirtmelidir" ListenIPCommands: "Dinleme IP'sini değiştir" ListenIPv4: "IPv4'te dinle" ListenIPv6: "IPv6'da dinle" @@ -211,6 +212,12 @@ ResetEntrance: "1Panel güvenli girişi iptal et" ResetIPs: "1Panel yetkili IP kısıtlamalarını iptal et" ResetDomain: "1Panel alan adı bağlamasını iptal et" ResetPasskey: "1Panel passkey'lerini sıfırla" +UserList: "1Panel kullanıcı listesini getir" +UserTableName: "Ad" +UserTableMFAStatus: "MFA Durumu" +UserTableSuperAdmin: "Süper Yönetici" +UserTableCreatedAt: "Oluşturulma" +UserEmptyList: "Kullanıcı bulunamadı" RestoreCommands: "1Panel servisini ve verilerini geri al" RestoreNoSuchFile: "Rollback için dosya bulunamadı" RestoreStep1: "(1/5) {{ .name }} dizininden 1Panel hizmeti ve verileri geri alınıyor..." diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 233320f3882f..9617ba47f3b3 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -203,6 +203,7 @@ AppVersionExist: "版本已存在!" AppCreateSuccessful: "建立成功!" AppWriteErr: "檔案 {{ .name }} 寫入失敗 {{ .err }}" SudoHelper: "請使用 {{ .cmd }} 或切換到 root 使用者" +UsernameNeed: "企業版必須指定 --username" ListenIPCommands: "切換監聽 IP" ListenIPv4: "監聽 IPv4" ListenIPv6: "監聽 IPv6" @@ -214,6 +215,12 @@ ResetEntrance: "取消 1Panel 安全入口" ResetIPs: "取消 1Panel 授權 IP 限制" ResetDomain: "取消 1Panel 存取網域綁定" ResetPasskey: "清空 1Panel 通行密鑰" +UserList: "獲取 1Panel 使用者列表" +UserTableName: "名稱" +UserTableMFAStatus: "MFA 狀態" +UserTableSuperAdmin: "超級管理員" +UserTableCreatedAt: "建立時間" +UserEmptyList: "暫無使用者" RestoreCommands: "回滾 1Panel 服務及資料" RestoreNoSuchFile: "暫無可回滾檔案" RestoreStep1: "(1/5)開始從 {{ .name }} 目錄回滾 1Panel 服務及資料... " diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index 704111abdede..576fe4e26b7e 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -209,6 +209,7 @@ AppVersionExist: "版本已存在!" AppCreateSuccessful: "创建成功!" AppWriteErr: "文件 {{ .name }} 写入失败 {{ .err }}" SudoHelper: "请使用 {{ .cmd }} 或切换到 root 用户" +UsernameNeed: "企业版必须指定 --username" ListenIPCommands: "切换监听 IP" ListenIPv4: "监听 IPv4" ListenIPv6: "监听 IPv6" @@ -220,6 +221,12 @@ ResetEntrance: "取消 1Panel 安全入口" ResetIPs: "取消 1Panel 授权 IP 限制" ResetDomain: "取消 1Panel 访问域名绑定" ResetPasskey: "清空 1Panel 通行密钥" +UserList: "获取 1Panel 用户列表" +UserTableName: "名称" +UserTableMFAStatus: "MFA 状态" +UserTableSuperAdmin: "超级管理员" +UserTableCreatedAt: "创建时间" +UserEmptyList: "暂无用户" RestoreCommands: "回滚 1Panel 服务及数据" RestoreNoSuchFile: "暂无可回滚文件" RestoreStep1: "(1/5)开始从 {{ .name }} 目录回滚 1Panel 服务及数据... " diff --git a/core/middleware/bind_domain.go b/core/middleware/bind_domain.go index b70ea29bc47b..0ee9af2a6e66 100644 --- a/core/middleware/bind_domain.go +++ b/core/middleware/bind_domain.go @@ -5,6 +5,7 @@ import ( "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/utils/security" "github.com/gin-gonic/gin" ) @@ -32,7 +33,7 @@ func BindDomain() gin.HandlerFunc { } if domains != bindDomain { - code := LoadErrCode() + code := security.LoadErrCode() helper.ErrWithHtml(c, code, "err_domain") return } diff --git a/core/middleware/helper.go b/core/middleware/helper.go deleted file mode 100644 index 7ab34a8f8c9e..000000000000 --- a/core/middleware/helper.go +++ /dev/null @@ -1,36 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/1Panel-dev/1Panel/core/app/repo" -) - -func LoadErrCode() int { - settingRepo := repo.NewISettingRepo() - codeVal, err := settingRepo.GetValueByKey("NoAuthSetting") - if err != nil { - return 500 - } - - switch codeVal { - case "400": - return http.StatusBadRequest - case "401": - return http.StatusUnauthorized - case "403": - return http.StatusForbidden - case "404": - return http.StatusNotFound - case "408": - return http.StatusRequestTimeout - case "416": - return http.StatusRequestedRangeNotSatisfiable - case "500": - return http.StatusInternalServerError - case "444": - return 444 - default: - return http.StatusOK - } -} diff --git a/core/middleware/ip_limit.go b/core/middleware/ip_limit.go index 5689157c6a5b..3d050139a0fe 100644 --- a/core/middleware/ip_limit.go +++ b/core/middleware/ip_limit.go @@ -1,11 +1,12 @@ package middleware import ( - "github.com/1Panel-dev/1Panel/core/utils/common" "strings" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/1Panel-dev/1Panel/core/utils/security" "github.com/gin-gonic/gin" ) @@ -43,7 +44,7 @@ func WhiteAllow() gin.HandlerFunc { return } } - code := LoadErrCode() + code := security.LoadErrCode() helper.ErrWithHtml(c, code, "err_ip_limit") } } diff --git a/core/middleware/password_expired.go b/core/middleware/password_expired.go index 4ed3241bfc3d..b5ff6cd6c70e 100644 --- a/core/middleware/password_expired.go +++ b/core/middleware/password_expired.go @@ -15,6 +15,10 @@ import ( func PasswordExpired() gin.HandlerFunc { return func(c *gin.Context) { + if !strings.HasPrefix(c.Request.URL.Path, "/api/v2/") { + c.Next() + return + } if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") || c.Request.URL.Path == "/api/v2/core/settings/search" || c.Request.URL.Path == "/api/v2/core/settings/search/base" || diff --git a/core/utils/security/security.go b/core/utils/security/security.go index 5b56c357c829..4680f0061056 100644 --- a/core/utils/security/security.go +++ b/core/utils/security/security.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "regexp" - "strconv" "strings" "github.com/1Panel-dev/1Panel/core/app/repo" @@ -41,7 +40,9 @@ func HandleNotRoute(c *gin.Context) bool { } func CheckSecurity(c *gin.Context) bool { - if !checkEntrance(c) && !checkSession(c) { + authService := service.NewIAuthService() + entrance := authService.GetSecurityEntrance() + if entrance != "" && !checkEntrance(c) && !checkSession(c) { HandleNotSecurity(c, "") return false } @@ -95,18 +96,14 @@ func checkEntrance(c *gin.Context) bool { } func HandleNotSecurity(c *gin.Context, resType string) { - resPage, err := service.NewIAuthService().GetResponsePage() - if err != nil { - c.String(http.StatusInternalServerError, "Internal Server Error") - return - } - if resPage == "444" { + code := LoadErrCode() + if code == 444 { CloseDirectly(c) return } - file := fmt.Sprintf("html/%s.html", resPage) - if resPage == "200" && resType != "" { + file := fmt.Sprintf("html/%d.html", code) + if code == http.StatusOK && resType != "" { file = fmt.Sprintf("html/200_%s.html", resType) } data, err := res.ErrorMsg.ReadFile(file) @@ -114,12 +111,36 @@ func HandleNotSecurity(c *gin.Context, resType string) { c.String(http.StatusInternalServerError, "Internal Server Error") return } - statusCode, err := strconv.Atoi(resPage) + c.Data(code, "text/html; charset=utf-8", data) +} + +func LoadErrCode() int { + settingRepo := repo.NewISettingRepo() + codeVal, err := settingRepo.GetValueByKey("NoAuthSetting") if err != nil { - c.String(http.StatusInternalServerError, "Internal Server Error") - return + return http.StatusInternalServerError + } + + switch codeVal { + case "400": + return http.StatusBadRequest + case "401": + return http.StatusUnauthorized + case "403": + return http.StatusForbidden + case "404": + return http.StatusNotFound + case "408": + return http.StatusRequestTimeout + case "416": + return http.StatusRequestedRangeNotSatisfiable + case "500": + return http.StatusInternalServerError + case "444": + return 444 + default: + return http.StatusOK } - c.Data(statusCode, "text/html; charset=utf-8", data) } func IsFrontendPath(path string) bool { diff --git a/frontend/src/components/router-button/index.vue b/frontend/src/components/router-button/index.vue index 3783fce7e5eb..c4e7f9942af4 100644 --- a/frontend/src/components/router-button/index.vue +++ b/frontend/src/components/router-button/index.vue @@ -26,7 +26,7 @@ import { routerToNameWithQuery, routerToPathWithQuery } from '@/utils/router'; import { computed, onMounted, ref, watch } from 'vue'; import { useRouter } from 'vue-router'; -import { hasPermissionMetaAccess } from '@/utils/rbac'; +import { hasPermissionMetaAccess, hasRouteAccess } from '@/utils/rbac'; defineOptions({ name: 'RouterButton' }); @@ -37,11 +37,20 @@ const props = defineProps({ }, }); +const router = useRouter(); const buttonArray = computed(() => { - return props.buttons.filter((button) => hasPermissionMetaAccess(button.permission)); + return props.buttons.filter((button) => { + if (!hasPermissionMetaAccess(button.permission)) { + return false; + } + if (button.path || button.name) { + const route = router.resolve(button.path ? { path: button.path } : { name: button.name }); + return route.matched.length === 0 || hasRouteAccess(route); + } + return true; + }); }); -const router = useRouter(); const activeName = ref(''); const handleChange = (label: string) => { diff --git a/frontend/src/routers/index.ts b/frontend/src/routers/index.ts index 3e8e4c13881d..37722df1e2eb 100644 --- a/frontend/src/routers/index.ts +++ b/frontend/src/routers/index.ts @@ -11,6 +11,7 @@ const axiosCanceler = new AxiosCanceler(); let isRedirecting = false; const enterpriseLicenseCheckWhiteList = ['EnterpriseLicenseRequired', 'entrance', 'login', 'Expired']; +const noLoginWhiteList = ['entrance', 'login', 'file-share', '404', 'Expired']; const clearLicenseStatus = () => { const { isEnterpriseLicenseLoaded, isEnterpriseLicensed } = useGlobalStore(); @@ -33,10 +34,31 @@ router.beforeEach(async (to, from, next) => { if (!isLogin.value) { clearLoginStatus(); } - if (to.name !== 'entrance' && !isLogin.value) { + if (!isLogin.value && !noLoginWhiteList.includes(String(to.name))) { + next( + entrance.value + ? { + name: 'entrance', + params: { code: entrance.value }, + } + : { + name: 'login', + }, + ); + NProgress.done(); + return; + } + if (to.name === 'login' && !isLogin.value && entrance.value) { next({ name: 'entrance', - params: to.params, + params: { code: entrance.value }, + }); + NProgress.done(); + return; + } + if (to.name === 'login' && isLogin.value) { + next({ + name: 'home', }); NProgress.done(); return;