mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 2fa相关接口
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user