diff --git a/internal/biz/setting.go b/internal/biz/setting.go
index fa38c8f6..cbeac4ad 100644
--- a/internal/biz/setting.go
+++ b/internal/biz/setting.go
@@ -1,7 +1,6 @@
package biz
import (
- "context"
"time"
"github.com/tnb-labs/panel/internal/http/request"
@@ -38,6 +37,6 @@ type SettingRepo interface {
Set(key SettingKey, value string) error
SetSlice(key SettingKey, value []string) error
Delete(key SettingKey) error
- GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
- UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
+ GetPanelSetting() (*request.PanelSetting, error)
+ UpdatePanelSetting(req *request.PanelSetting) (bool, error)
}
diff --git a/internal/biz/user.go b/internal/biz/user.go
index 7c42df8e..a0054b08 100644
--- a/internal/biz/user.go
+++ b/internal/biz/user.go
@@ -23,6 +23,7 @@ type UserRepo interface {
Get(id uint) (*User, error)
Create(username, password string) (*User, error)
UpdatePassword(id uint, password string) error
+ UpdateEmail(id uint, email string) error
Delete(id uint) error
CheckPassword(username, password string) (*User, error)
IsTwoFA(username string) (bool, error)
diff --git a/internal/data/setting.go b/internal/data/setting.go
index 2db9b1c1..96cbb03b 100644
--- a/internal/data/setting.go
+++ b/internal/data/setting.go
@@ -1,13 +1,11 @@
package data
import (
- "context"
"encoding/json"
"errors"
"path/filepath"
"sync"
- "github.com/go-rat/utils/hash"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
@@ -199,7 +197,7 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
return nil
}
-func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
+func (r *settingRepo) GetPanelSetting() (*request.PanelSetting, error) {
name, err := r.Get(biz.SettingKeyName)
if err != nil {
return nil, err
@@ -225,12 +223,6 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
return nil, err
}
- userID := cast.ToUint(ctx.Value("user_id"))
- user := new(biz.User)
- if err := r.db.Where("id = ?", userID).First(user).Error; err != nil {
- return nil, err
- }
-
crt, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
if err != nil {
return nil, err
@@ -253,8 +245,6 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
BindUA: r.conf.Strings("http.bind_ua"),
WebsitePath: websitePath,
BackupPath: backupPath,
- Username: user.Username,
- Email: user.Email,
Port: uint(r.conf.Int("http.port")),
HTTPS: r.conf.Bool("http.tls"),
Cert: crt,
@@ -262,43 +252,23 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
}, nil
}
-func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error) {
- if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
+func (r *settingRepo) UpdatePanelSetting(req *request.PanelSetting) (bool, error) {
+ if err := r.Set(biz.SettingKeyName, req.Name); err != nil {
return false, err
}
- if err := r.Set(biz.SettingKeyChannel, setting.Channel); err != nil {
+ if err := r.Set(biz.SettingKeyChannel, req.Channel); err != nil {
return false, err
}
- if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
+ if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(req.OfflineMode)); err != nil {
return false, err
}
- if err := r.Set(biz.SettingKeyAutoUpdate, cast.ToString(setting.AutoUpdate)); err != nil {
+ if err := r.Set(biz.SettingKeyAutoUpdate, cast.ToString(req.AutoUpdate)); err != nil {
return false, err
}
- if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
+ if err := r.Set(biz.SettingKeyWebsitePath, req.WebsitePath); err != nil {
return false, err
}
- if err := r.Set(biz.SettingKeyBackupPath, setting.BackupPath); err != nil {
- return false, err
- }
-
- // 用户
- user := new(biz.User)
- userID := cast.ToUint(ctx.Value("user_id"))
- if err := r.db.Where("id = ?", userID).First(user).Error; err != nil {
- return false, err
- }
-
- user.Username = setting.Username
- user.Email = setting.Email
- if setting.Password != "" {
- value, err := hash.NewArgon2id().Make(setting.Password)
- if err != nil {
- return false, err
- }
- user.Password = value
- }
- if err := r.db.Save(user).Error; err != nil {
+ if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
return false, err
}
@@ -307,22 +277,22 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
restartFlag := false
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
- if oldCert != setting.Cert || oldKey != setting.Key {
+ if oldCert != req.Cert || oldKey != req.Key {
if r.task.HasRunningTask() {
return false, errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
}
restartFlag = true
}
- if _, err := cert.ParseCert(setting.Cert); err != nil {
+ if _, err := cert.ParseCert(req.Cert); err != nil {
return false, errors.New(r.t.Get("failed to parse certificate: %v", err))
}
- if _, err := cert.ParseKey(setting.Key); err != nil {
+ if _, err := cert.ParseKey(req.Key); err != nil {
return false, errors.New(r.t.Get("failed to parse private key: %v", err))
}
- if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
+ if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0644); err != nil {
return false, err
}
- if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), setting.Key, 0644); err != nil {
+ if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0644); err != nil {
return false, err
}
@@ -336,20 +306,20 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
- if setting.Port != config.HTTP.Port {
- if os.TCPPortInUse(setting.Port) {
+ if req.Port != config.HTTP.Port {
+ if os.TCPPortInUse(req.Port) {
return false, errors.New(r.t.Get("port is already in use"))
}
}
- config.App.Locale = setting.Locale
- config.HTTP.Port = setting.Port
- config.HTTP.Entrance = setting.Entrance
- config.HTTP.TLS = setting.HTTPS
- config.HTTP.BindDomain = setting.BindDomain
- config.HTTP.BindIP = setting.BindIP
- config.HTTP.BindUA = setting.BindUA
- config.Session.Lifetime = setting.Lifetime
+ config.App.Locale = req.Locale
+ config.HTTP.Port = req.Port
+ config.HTTP.Entrance = req.Entrance
+ config.HTTP.TLS = req.HTTPS
+ config.HTTP.BindDomain = req.BindDomain
+ config.HTTP.BindIP = req.BindIP
+ config.HTTP.BindUA = req.BindUA
+ config.Session.Lifetime = req.Lifetime
// 放行端口
fw := firewall.NewFirewall()
diff --git a/internal/data/user.go b/internal/data/user.go
index efa41f47..134faa74 100644
--- a/internal/data/user.go
+++ b/internal/data/user.go
@@ -76,6 +76,16 @@ func (r *userRepo) UpdatePassword(id uint, password string) error {
return r.db.Save(user).Error
}
+func (r *userRepo) UpdateEmail(id uint, email string) error {
+ user, err := r.Get(id)
+ if err != nil {
+ return err
+ }
+
+ user.Email = email
+ return r.db.Save(user).Error
+}
+
func (r *userRepo) Delete(id uint) error {
if id == 1 {
return errors.New(r.t.Get("please don't do this"))
diff --git a/internal/http/request/setting.go b/internal/http/request/setting.go
index 2aabb513..6ae1b0b4 100644
--- a/internal/http/request/setting.go
+++ b/internal/http/request/setting.go
@@ -16,9 +16,6 @@ type PanelSetting struct {
BindUA []string `json:"bind_ua"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
- Username string `json:"username" validate:"required"`
- Password string `json:"password" validate:"password"`
- Email string `json:"email" validate:"required"`
Port uint `json:"port" validate:"required|min:1|max:65535"`
HTTPS bool `json:"https"`
Cert string `json:"cert" validate:"required"`
diff --git a/internal/http/request/user.go b/internal/http/request/user.go
index 5301c406..d2249c88 100644
--- a/internal/http/request/user.go
+++ b/internal/http/request/user.go
@@ -1,5 +1,9 @@
package request
+type UserID struct {
+ ID uint `json:"id" validate:"required|exists:users,id"`
+}
+
type UserLogin struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
@@ -23,15 +27,11 @@ type UserUpdatePassword struct {
type UserUpdateEmail struct {
ID uint `json:"id" validate:"required|exists:users,id"`
- TwoFA string `json:"two_fa" validate:"required"`
+ Email string `json:"email" validate:"required|email"`
}
type UserUpdateTwoFA struct {
- ID uint `json:"id" validate:"required|exists:users,id"`
- TwoFA string `json:"two_fa" validate:"required"`
- Code string `json:"code" validate:"required"`
-}
-
-type UserDelete struct {
- ID uint `json:"id" validate:"required|exists:users,id"`
+ ID uint `uri:"id" validate:"required|exists:users,id"`
+ Secret string `json:"secret"`
+ Code string `json:"code"`
}
diff --git a/internal/route/http.go b/internal/route/http.go
index 3c2c7531..b492f386 100644
--- a/internal/route/http.go
+++ b/internal/route/http.go
@@ -114,6 +114,16 @@ func (route *Http) Register(r *chi.Mux) {
r.Get("/info", route.user.Info)
})
+ r.Route("/users", func(r chi.Router) {
+ r.Get("/", route.user.List)
+ r.Post("/", route.user.Create)
+ r.Post("/{id}/password", route.user.UpdatePassword)
+ r.Post("/{id}/email", route.user.UpdateEmail)
+ r.Get("/{id}/2fa", route.user.GenerateTwoFA)
+ r.Post("/{id}/2fa", route.user.UpdateTwoFA)
+ r.Delete("/{id}", route.user.Delete)
+ })
+
r.Route("/dashboard", func(r chi.Router) {
r.Get("/panel", route.dashboard.Panel)
r.Get("/home_apps", route.dashboard.HomeApps)
diff --git a/internal/service/setting.go b/internal/service/setting.go
index cf474f5a..f6f94e08 100644
--- a/internal/service/setting.go
+++ b/internal/service/setting.go
@@ -19,7 +19,7 @@ func NewSettingService(setting biz.SettingRepo) *SettingService {
}
func (s *SettingService) Get(w http.ResponseWriter, r *http.Request) {
- setting, err := s.settingRepo.GetPanelSetting(r.Context())
+ setting, err := s.settingRepo.GetPanelSetting()
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -36,7 +36,7 @@ func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) {
}
restart := false
- if restart, err = s.settingRepo.UpdatePanelSetting(r.Context(), req); err != nil {
+ if restart, err = s.settingRepo.UpdatePanelSetting(req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
diff --git a/internal/service/user.go b/internal/service/user.go
index 26d52c9c..410b9ebb 100644
--- a/internal/service/user.go
+++ b/internal/service/user.go
@@ -1,10 +1,13 @@
package service
import (
+ "bytes"
"crypto/rsa"
"crypto/sha256"
+ "encoding/base64"
"encoding/gob"
"fmt"
+ "image/png"
"net"
"net/http"
"strings"
@@ -217,3 +220,74 @@ func (s *UserService) UpdatePassword(w http.ResponseWriter, r *http.Request) {
Success(w, nil)
}
+
+func (s *UserService) UpdateEmail(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.UserUpdateEmail](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = s.userRepo.UpdateEmail(req.ID, req.Email); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *UserService) GenerateTwoFA(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.UserID](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ img, url, secret, err := s.userRepo.GenerateTwoFA(req.ID)
+ if err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ buf := new(bytes.Buffer)
+ if err = png.Encode(buf, img); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, chix.M{
+ "img": base64.StdEncoding.EncodeToString(buf.Bytes()),
+ "url": url,
+ "secret": secret,
+ })
+}
+
+func (s *UserService) UpdateTwoFA(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.UserUpdateTwoFA](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = s.userRepo.UpdateTwoFA(req.ID, req.Code, req.Secret); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
+
+func (s *UserService) Delete(w http.ResponseWriter, r *http.Request) {
+ req, err := Bind[request.UserID](r)
+ if err != nil {
+ Error(w, http.StatusUnprocessableEntity, "%v", err)
+ return
+ }
+
+ if err = s.userRepo.Delete(req.ID); err != nil {
+ Error(w, http.StatusInternalServerError, "%v", err)
+ return
+ }
+
+ Success(w, nil)
+}
diff --git a/web/src/api/panel/user/index.ts b/web/src/api/panel/user/index.ts
index 63d01837..ad0a61c5 100644
--- a/web/src/api/panel/user/index.ts
+++ b/web/src/api/panel/user/index.ts
@@ -15,5 +15,22 @@ export default {
// 是否登录
isLogin: () => http.Get('/user/is_login'),
// 获取用户信息
- info: () => http.Get('/user/info')
+ info: () => http.Get('/user/info'),
+ // 获取用户列表
+ list: (page: number, limit: number): any => http.Get(`/users`, { params: { page, limit } }),
+ // 创建用户
+ create: (username: string, password: string, email: string): any =>
+ http.Post('/users', { username, password, email }),
+ // 删除用户
+ delete: (id: number): any => http.Delete(`/users/${id}`),
+ // 更新用户邮箱
+ updateEmail: (id: number, email: string): any => http.Post(`/users/${id}/email`, { email }),
+ // 更新用户密码
+ updatePassword: (id: number, password: string): any =>
+ http.Post(`/users/${id}/password`, { password }),
+ // 生成2FA密钥
+ generateTwoFA: (id: number): any => http.Get(`/users/${id}/2fa`),
+ // 保存2FA密钥
+ updateTwoFA: (id: number, code: string, secret: string): any =>
+ http.Post(`/users/${id}/2fa`, { code, secret })
}
diff --git a/web/src/views/setting/IndexView.vue b/web/src/views/setting/IndexView.vue
index 5b080840..965be6f9 100644
--- a/web/src/views/setting/IndexView.vue
+++ b/web/src/views/setting/IndexView.vue
@@ -1,16 +1,16 @@
-
+
{{ $gettext('Save') }}
+
+
+ {{ $gettext('Create User') }}
+
@@ -70,6 +72,9 @@ const handleSave = () => {
+
+
+
diff --git a/web/src/views/setting/PasswordModal.vue b/web/src/views/setting/PasswordModal.vue
new file mode 100644
index 00000000..7a013a69
--- /dev/null
+++ b/web/src/views/setting/PasswordModal.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Submit') }}
+
+
+
+
diff --git a/web/src/views/setting/SettingBase.vue b/web/src/views/setting/SettingBase.vue
index c681652c..709f77ee 100644
--- a/web/src/views/setting/SettingBase.vue
+++ b/web/src/views/setting/SettingBase.vue
@@ -46,15 +46,6 @@ const channels = [
-
-
-
-
-
-
-
-
-
diff --git a/web/src/views/setting/SettingUser.vue b/web/src/views/setting/SettingUser.vue
new file mode 100644
index 00000000..7d346798
--- /dev/null
+++ b/web/src/views/setting/SettingUser.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/setting/TwoFaModal.vue b/web/src/views/setting/TwoFaModal.vue
new file mode 100644
index 00000000..1c9f612a
--- /dev/null
+++ b/web/src/views/setting/TwoFaModal.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Scan the QR code with your 2FA app and enter the code below') }}
+
+
+ {{
+ $gettext('If you cannot scan the QR code, please enter the URL below in your 2FA app')
+ }}
+
+
+ {{ model.url }}
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Submit') }}
+
+
+
+
+