2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 04:22:33 +08:00

feat: 用户支持开启2FA

This commit is contained in:
2025-05-14 19:04:03 +08:00
parent 462d6c0789
commit 5fd00acd48
15 changed files with 476 additions and 88 deletions

View File

@@ -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)
}

View File

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

View File

@@ -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()

View File

@@ -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"))

View File

@@ -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"`

View File

@@ -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"`
}

View File

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

View File

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

View File

@@ -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)
}