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 @@ 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 @@ + + + + + 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 @@ + + + + +