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

feat: 2fa相关接口

This commit is contained in:
2025-05-14 03:59:22 +08:00
parent 4386334c31
commit daccf651b6
9 changed files with 390 additions and 376 deletions

View File

@@ -1,6 +1,7 @@
package biz
import (
"image"
"time"
"gorm.io/gorm"
@@ -18,8 +19,13 @@ type User struct {
}
type UserRepo interface {
Create(username, password string) (*User, error)
CheckPassword(username, password string) (*User, error)
List(page, limit uint) ([]*User, int64, error)
Get(id uint) (*User, error)
Save(user *User) error
Create(username, password string) (*User, error)
UpdatePassword(id uint, password string) error
Delete(id uint) error
CheckPassword(username, password string) (*User, error)
IsTwoFA(username string) (bool, error)
GenerateTwoFA(id uint) (image.Image, string, string, error)
UpdateTwoFA(id uint, code, secret string) error
}

View File

@@ -10,8 +10,6 @@ import (
"github.com/go-rat/utils/hash"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
@@ -382,19 +380,3 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return restartFlag, nil
}
// GetTwoFA 生成两步验证密钥
// TODO: 即将废弃
func (r *settingRepo) GetTwoFA() (*otp.Key, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "RatPanel",
AccountName: "admin",
SecretSize: 32,
Algorithm: otp.AlgorithmSHA256,
})
if err != nil {
return nil, err
}
return key, nil
}

View File

@@ -2,9 +2,13 @@ package data
import (
"errors"
"image"
"github.com/go-rat/utils/hash"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -24,6 +28,22 @@ func NewUserRepo(t *gotext.Locale, db *gorm.DB) biz.UserRepo {
}
}
func (r *userRepo) List(page, limit uint) ([]*biz.User, int64, error) {
users := make([]*biz.User, 0)
var total int64
err := r.db.Model(&biz.User{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&users).Error
return users, total, err
}
func (r *userRepo) Get(id uint) (*biz.User, error) {
user := new(biz.User)
if err := r.db.First(user, id).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *userRepo) Create(username, password string) (*biz.User, error) {
value, err := r.hasher.Make(password)
if err != nil {
@@ -41,6 +61,34 @@ func (r *userRepo) Create(username, password string) (*biz.User, error) {
return user, nil
}
func (r *userRepo) UpdatePassword(id uint, password string) error {
value, err := r.hasher.Make(password)
if err != nil {
return err
}
user, err := r.Get(id)
if err != nil {
return err
}
user.Password = value
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"))
}
user := new(biz.User)
if err := r.db.First(user, id).Error; err != nil {
return err
}
return r.db.Delete(user).Error
}
func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {
user := new(biz.User)
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
@@ -58,15 +106,66 @@ func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {
return user, nil
}
func (r *userRepo) Get(id uint) (*biz.User, error) {
func (r *userRepo) IsTwoFA(username string) (bool, error) {
user := new(biz.User)
if err := r.db.First(user, id).Error; err != nil {
return nil, err
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, errors.New(r.t.Get("username or password error"))
} else {
return false, err
}
}
return user, nil
return user.TwoFA != "", nil
}
func (r *userRepo) Save(user *biz.User) error {
func (r *userRepo) GenerateTwoFA(id uint) (image.Image, string, string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "RatPanel",
AccountName: cast.ToString(id),
SecretSize: 32,
Algorithm: otp.AlgorithmSHA256,
})
if err != nil {
return nil, "", "", err
}
img, err := key.Image(200, 200)
if err != nil {
return nil, "", "", err
}
return img, key.URL(), key.Secret(), nil
}
func (r *userRepo) UpdateTwoFA(id uint, code, secret string) error {
user, err := r.Get(id)
if err != nil {
return err
}
// 保存前先验证一次,防止错误开启
if !totp.Validate(code, secret) {
return errors.New(r.t.Get("invalid 2fa code"))
}
user.TwoFA = secret
return r.db.Save(user).Error
}
func (r *userRepo) CheckTwoFA(id uint, code string) (bool, error) {
user, err := r.Get(id)
if err != nil {
return false, err
}
if user.TwoFA == "" {
return true, nil // 未开启2FA无需验证
}
if !totp.Validate(code, user.TwoFA) {
return false, errors.New(r.t.Get("invalid 2fa code"))
}
return true, nil
}

View File

@@ -1,7 +1,37 @@
package request
type UserLogin struct {
Username string `json:"username" form:"username" validate:"required"`
Password string `json:"password" form:"password" validate:"required"`
SafeLogin bool `json:"safe_login" form:"safe_login"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
SafeLogin bool `json:"safe_login"`
PassCode string `json:"pass_code"`
}
type UserIsTwoFA struct {
Username string `uri:"username" validate:"required"`
}
type UserCreate struct {
Username string `json:"username" validate:"required|notExists:users,username"`
Password string `json:"password" validate:"required|password"`
}
type UserUpdatePassword struct {
ID uint `json:"id" validate:"required|exists:users,id"`
Password string `json:"password" validate:"required|password"`
}
type UserUpdateEmail struct {
ID uint `json:"id" validate:"required|exists:users,id"`
TwoFA string `json:"two_fa" validate:"required"`
}
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"`
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/go-rat/sessions"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/biz"
@@ -87,6 +88,13 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) {
return
}
if user.TwoFA != "" {
if !totp.Validate(req.PassCode, user.TwoFA) {
Error(w, http.StatusForbidden, s.t.Get("invalid 2fa code"))
return
}
}
// 安全登录下,将当前客户端与会话绑定
// 安全登录只在未启用面板 HTTPS 时生效
ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
@@ -128,6 +136,17 @@ func (s *UserService) IsLogin(w http.ResponseWriter, r *http.Request) {
Success(w, sess.Has("user_id"))
}
func (s *UserService) IsTwoFA(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.UserIsTwoFA](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
twoFA, _ := s.userRepo.IsTwoFA(req.Username)
Success(w, twoFA)
}
func (s *UserService) Info(w http.ResponseWriter, r *http.Request) {
userID := cast.ToUint(r.Context().Value("user_id"))
if userID == 0 {
@@ -148,3 +167,53 @@ func (s *UserService) Info(w http.ResponseWriter, r *http.Request) {
"email": user.Email,
})
}
func (s *UserService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.Paginate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
users, total, err := s.userRepo.List(req.Page, req.Limit)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, chix.M{
"total": total,
"items": users,
})
}
func (s *UserService) Create(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.UserCreate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
user, err := s.userRepo.Create(req.Username, req.Password)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, user)
}
func (s *UserService) UpdatePassword(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.UserUpdatePassword](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.userRepo.UpdatePassword(req.ID, req.Password); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}