mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 11:27:17 +08:00
feat: 支持创建access_token
This commit is contained in:
1
.github/workflows/mockery.yml
vendored
1
.github/workflows/mockery.yml
vendored
@@ -3,7 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
mockery:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -73,6 +73,8 @@ func initWeb() (*app.Web, error) {
|
||||
middlewares := middleware.NewMiddlewares(koanf, logger, manager, appRepo)
|
||||
userRepo := data.NewUserRepo(locale, db)
|
||||
userService := service.NewUserService(locale, koanf, manager, userRepo)
|
||||
userTokenRepo := data.NewUserTokenRepo(locale, db)
|
||||
userTokenService := service.NewUserTokenService(locale, userTokenRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
|
||||
@@ -142,7 +144,7 @@ func initWeb() (*app.Web, error) {
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
toolboxApp := toolbox.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(benchmarkApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, minioApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp, toolboxApp)
|
||||
http := route.NewHttp(userService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader)
|
||||
http := route.NewHttp(userService, userTokenService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader)
|
||||
wsService := service.NewWsService(locale, koanf, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
|
||||
5
go.sum
5
go.sum
@@ -121,6 +121,7 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
@@ -403,6 +404,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -512,6 +515,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -38,7 +38,6 @@ func (r *SSH) BeforeSave(tx *gorm.DB) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *SSH) AfterFind(tx *gorm.DB) error {
|
||||
|
||||
@@ -21,7 +21,7 @@ type User struct {
|
||||
type UserRepo interface {
|
||||
List(page, limit uint) ([]*User, int64, error)
|
||||
Get(id uint) (*User, error)
|
||||
Create(username, password string) (*User, error)
|
||||
Create(username, password, email string) (*User, error)
|
||||
UpdatePassword(id uint, password string) error
|
||||
UpdateEmail(id uint, email string) error
|
||||
Delete(id uint) error
|
||||
|
||||
@@ -2,20 +2,37 @@ package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserToken struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
Token string `gorm:"not null;default:'';unique" json:"token"`
|
||||
Token string `gorm:"not null;default:'';unique" json:"-"`
|
||||
IPs []string `gorm:"not null;default:'[]';serializer:json" json:"ips"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserTokenRepo interface {
|
||||
Create(userID uint, ips []string) (*UserToken, error)
|
||||
Get(id uint) (*UserToken, error)
|
||||
Save(user *UserToken) error
|
||||
func (r *UserToken) BeforeSave(tx *gorm.DB) error {
|
||||
hasher := hash.NewArgon2id()
|
||||
var err error
|
||||
|
||||
r.Token, err = hasher.Make(r.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserTokenRepo interface {
|
||||
List(userID, page, limit uint) ([]*UserToken, int64, error)
|
||||
Create(userID uint, ips []string, expired time.Time) (*UserToken, error)
|
||||
Get(id uint) (*UserToken, error)
|
||||
Delete(id uint) error
|
||||
Update(id uint, ips []string, expired time.Time) (*UserToken, error)
|
||||
}
|
||||
|
||||
@@ -25,5 +25,6 @@ var ProviderSet = wire.NewSet(
|
||||
NewSSHRepo,
|
||||
NewTaskRepo,
|
||||
NewUserRepo,
|
||||
NewUserTokenRepo,
|
||||
NewWebsiteRepo,
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ func (r *userRepo) Get(id uint) (*biz.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *userRepo) Create(username, password string) (*biz.User, error) {
|
||||
func (r *userRepo) Create(username, password, email string) (*biz.User, error) {
|
||||
value, err := r.hasher.Make(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -53,6 +53,7 @@ func (r *userRepo) Create(username, password string) (*biz.User, error) {
|
||||
user := &biz.User{
|
||||
Username: username,
|
||||
Password: value,
|
||||
Email: email,
|
||||
}
|
||||
if err = r.db.Create(user).Error; err != nil {
|
||||
return nil, err
|
||||
|
||||
89
internal/data/user_token.go
Normal file
89
internal/data/user_token.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/biz"
|
||||
)
|
||||
|
||||
type userTokenRepo struct {
|
||||
t *gotext.Locale
|
||||
db *gorm.DB
|
||||
hasher hash.Hasher
|
||||
}
|
||||
|
||||
func NewUserTokenRepo(t *gotext.Locale, db *gorm.DB) biz.UserTokenRepo {
|
||||
return &userTokenRepo{
|
||||
t: t,
|
||||
db: db,
|
||||
hasher: hash.NewArgon2id(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r userTokenRepo) List(userID, page, limit uint) ([]*biz.UserToken, int64, error) {
|
||||
userTokens := make([]*biz.UserToken, 0)
|
||||
var total int64
|
||||
err := r.db.Model(&biz.UserToken{}).Where("user_id = ?", userID).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&userTokens).Error
|
||||
return userTokens, total, err
|
||||
}
|
||||
|
||||
func (r userTokenRepo) Create(userID uint, ips []string, expired time.Time) (*biz.UserToken, error) {
|
||||
token := str.Random(32)
|
||||
hashedToken, err := r.hasher.Make(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userToken := &biz.UserToken{
|
||||
UserID: userID,
|
||||
Token: hashedToken,
|
||||
IPs: ips,
|
||||
ExpiredAt: expired,
|
||||
}
|
||||
if err = r.db.Create(userToken).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userToken.Token = token
|
||||
|
||||
return userToken, nil
|
||||
}
|
||||
|
||||
func (r userTokenRepo) Get(id uint) (*biz.UserToken, error) {
|
||||
userToken := new(biz.UserToken)
|
||||
if err := r.db.First(userToken, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userToken, nil
|
||||
}
|
||||
|
||||
func (r userTokenRepo) Delete(id uint) error {
|
||||
userToken := new(biz.UserToken)
|
||||
if err := r.db.First(userToken, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.db.Delete(userToken).Error
|
||||
}
|
||||
|
||||
func (r userTokenRepo) Update(id uint, ips []string, expired time.Time) (*biz.UserToken, error) {
|
||||
userToken := new(biz.UserToken)
|
||||
if err := r.db.First(userToken, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userToken.IPs = ips
|
||||
userToken.ExpiredAt = expired
|
||||
|
||||
if err := r.db.Save(userToken).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userToken, nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package request
|
||||
|
||||
type ID struct {
|
||||
ID uint `json:"id" form:"id" query:"id" validate:"required|min:1"`
|
||||
ID uint `json:"id" form:"id" query:"id" uri:"id" validate:"required|min:1"`
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type UserIsTwoFA struct {
|
||||
type UserCreate struct {
|
||||
Username string `json:"username" validate:"required|notExists:users,username"`
|
||||
Password string `json:"password" validate:"required|password"`
|
||||
Email string `json:"email" validate:"required|email"`
|
||||
}
|
||||
|
||||
type UserUpdatePassword struct {
|
||||
|
||||
32
internal/http/request/user_token.go
Normal file
32
internal/http/request/user_token.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package request
|
||||
|
||||
import "net/http"
|
||||
|
||||
type UserTokenList struct {
|
||||
UserID uint `query:"user_id"`
|
||||
Paginate
|
||||
}
|
||||
|
||||
type UserTokenCreate struct {
|
||||
UserID uint `json:"user_id" validate:"required|exists:users,id"`
|
||||
IPs []string `json:"ips"`
|
||||
ExpiredAt int64 `json:"expired_at" validate:"required"`
|
||||
}
|
||||
|
||||
func (r *UserTokenCreate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"IPs.*": "required|ip",
|
||||
}
|
||||
}
|
||||
|
||||
type UserTokenUpdate struct {
|
||||
ID uint `uri:"id"`
|
||||
IPs []string `json:"ips"`
|
||||
ExpiredAt int64 `json:"expired_at" validate:"required"`
|
||||
}
|
||||
|
||||
func (r *UserTokenUpdate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"IPs.*": "required|ip",
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
type Http struct {
|
||||
user *service.UserService
|
||||
userToken *service.UserTokenService
|
||||
dashboard *service.DashboardService
|
||||
task *service.TaskService
|
||||
website *service.WebsiteService
|
||||
@@ -46,6 +47,7 @@ type Http struct {
|
||||
|
||||
func NewHttp(
|
||||
user *service.UserService,
|
||||
userToken *service.UserTokenService,
|
||||
dashboard *service.DashboardService,
|
||||
task *service.TaskService,
|
||||
website *service.WebsiteService,
|
||||
@@ -75,6 +77,7 @@ func NewHttp(
|
||||
) *Http {
|
||||
return &Http{
|
||||
user: user,
|
||||
userToken: userToken,
|
||||
dashboard: dashboard,
|
||||
task: task,
|
||||
website: website,
|
||||
@@ -125,6 +128,13 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Delete("/{id}", route.user.Delete)
|
||||
})
|
||||
|
||||
r.Route("/user_tokens", func(r chi.Router) {
|
||||
r.Get("/", route.userToken.List)
|
||||
r.Post("/", route.userToken.Create)
|
||||
r.Put("/{id}", route.userToken.Update)
|
||||
r.Delete("/{id}", route.userToken.Delete)
|
||||
})
|
||||
|
||||
r.Route("/dashboard", func(r chi.Router) {
|
||||
r.Get("/panel", route.dashboard.Panel)
|
||||
r.Get("/home_apps", route.dashboard.HomeApps)
|
||||
|
||||
@@ -138,9 +138,6 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
user.Username = str.Random(8)
|
||||
user.Password = hashed
|
||||
if user.Email == "" {
|
||||
user.Email = str.Random(8) + "@yourdomain.com"
|
||||
}
|
||||
|
||||
if err = s.db.Save(user).Error; err != nil {
|
||||
return errors.New(s.t.Get("Failed to save user info: %v", err))
|
||||
@@ -882,7 +879,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
}
|
||||
|
||||
_, err = s.userRepo.Create("admin", value)
|
||||
_, err = s.userRepo.Create("admin", value, str.Random(8)+"@yourdomain.com")
|
||||
if err != nil {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewSystemctlService,
|
||||
NewTaskService,
|
||||
NewUserService,
|
||||
NewUserTokenService,
|
||||
NewWebsiteService,
|
||||
NewWsService,
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ func (s *UserService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.userRepo.Create(req.Username, req.Password)
|
||||
user, err := s.userRepo.Create(req.Username, req.Password, req.Email)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
119
internal/service/user_token.go
Normal file
119
internal/service/user_token.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/biz"
|
||||
"github.com/tnb-labs/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type UserTokenService struct {
|
||||
t *gotext.Locale
|
||||
userTokenRepo biz.UserTokenRepo
|
||||
}
|
||||
|
||||
func NewUserTokenService(t *gotext.Locale, userToken biz.UserTokenRepo) *UserTokenService {
|
||||
return &UserTokenService{
|
||||
t: t,
|
||||
userTokenRepo: userToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserTokenService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.UserTokenList](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
userTokens, total, err := s.userTokenRepo.List(req.UserID, req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": userTokens,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserTokenService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.UserTokenCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expiredAt := time.Unix(0, req.ExpiredAt*int64(time.Millisecond))
|
||||
if expiredAt.Before(time.Now()) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("expiration time must be greater than current time"))
|
||||
return
|
||||
}
|
||||
if expiredAt.After(time.Now().AddDate(10, 0, 0)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("expiration time must be less than 10 years"))
|
||||
return
|
||||
}
|
||||
|
||||
userToken, err := s.userTokenRepo.Create(req.UserID, req.IPs, expiredAt)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 手动组装响应,因为 Token 设置了 json:"-"
|
||||
Success(w, chix.M{
|
||||
"id": userToken.ID,
|
||||
"user_id": userToken.UserID,
|
||||
"token": userToken.Token,
|
||||
"ips": userToken.IPs,
|
||||
"expired_at": userToken.ExpiredAt,
|
||||
"created_at": userToken.CreatedAt,
|
||||
"updated_at": userToken.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserTokenService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.UserTokenUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expiredAt := time.Unix(0, req.ExpiredAt*int64(time.Millisecond))
|
||||
if expiredAt.Before(time.Now()) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("expiration time must be greater than current time"))
|
||||
return
|
||||
}
|
||||
if expiredAt.After(time.Now().AddDate(10, 0, 0)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("expiration time must be less than 10 years"))
|
||||
return
|
||||
}
|
||||
|
||||
userToken, err := s.userTokenRepo.Update(req.ID, req.IPs, expiredAt)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, userToken)
|
||||
}
|
||||
|
||||
func (s *UserTokenService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.userTokenRepo.Delete(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
@@ -4,7 +4,8 @@ export default {
|
||||
// 获取注册表配置
|
||||
registryConfig: (): any => http.Get('/apps/podman/registry_config'),
|
||||
// 保存注册表配置
|
||||
saveRegistryConfig: (config: string): any => http.Post('/apps/podman/registry_config', { config }),
|
||||
saveRegistryConfig: (config: string): any =>
|
||||
http.Post('/apps/podman/registry_config', { config }),
|
||||
// 获取存储配置
|
||||
storageConfig: (): any => http.Get('/apps/podman/storage_config'),
|
||||
// 保存存储配置
|
||||
|
||||
@@ -27,7 +27,8 @@ export default {
|
||||
serverRemark: (id: number, remark: string) =>
|
||||
http.Put(`/database_server/${id}/remark`, { remark }),
|
||||
// 获取数据库用户列表
|
||||
userList: (page: number, limit: number) => http.Get('/database_user', { params: { page, limit } }),
|
||||
userList: (page: number, limit: number) =>
|
||||
http.Get('/database_user', { params: { page, limit } }),
|
||||
// 创建数据库用户
|
||||
userCreate: (data: any) => http.Post('/database_user', data),
|
||||
// 获取数据库用户
|
||||
|
||||
@@ -35,5 +35,17 @@ export default {
|
||||
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 })
|
||||
http.Post(`/users/${id}/2fa`, { code, secret }),
|
||||
|
||||
// 获取用户Token列表
|
||||
tokenList: (user_id: number, page: number, limit: number): any =>
|
||||
http.Get(`/user_tokens`, { params: { user_id, page, limit } }),
|
||||
// 创建用户Token
|
||||
tokenCreate: (user_id: number, ips: string[], expired_at: number): any =>
|
||||
http.Post('/user_tokens', { user_id, ips, expired_at }),
|
||||
// 删除用户Token
|
||||
tokenDelete: (id: number): any => http.Delete(`/user_tokens/${id}`),
|
||||
// 更新用户Token
|
||||
tokenUpdate: (id: number, ips: string[], expired_at: number): any =>
|
||||
http.Put(`/user_tokens/${id}`, { ips, expired_at })
|
||||
}
|
||||
|
||||
68
web/src/views/setting/CreateModal.vue
Normal file
68
web/src/views/setting/CreateModal.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import user from '@/api/panel/user'
|
||||
import { NButton, NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const model = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const handleCreate = () => {
|
||||
useRequest(() =>
|
||||
user.create(model.value.username, model.value.password, model.value.email)
|
||||
).onSuccess(() => {
|
||||
show.value = false
|
||||
window.$message.success($gettext('Created successfully'))
|
||||
window.$bus.emit('user:refresh')
|
||||
model.value.username = ''
|
||||
model.value.password = ''
|
||||
model.value.email = ''
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="$gettext('Create User')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="show = false"
|
||||
>
|
||||
<n-form :model="model">
|
||||
<n-form-item path="username" :label="$gettext('Username')">
|
||||
<n-input
|
||||
v-model:value="model.username"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Enter user name')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="password" :label="$gettext('Password')">
|
||||
<n-input
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Enter user password')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="email" :label="$gettext('Email')">
|
||||
<n-input
|
||||
v-model:value="model.email"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Enter user email')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleCreate">{{ $gettext('Submit') }}</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -8,6 +8,7 @@ import { useGettext } from 'vue3-gettext'
|
||||
import setting from '@/api/panel/setting'
|
||||
import TheIcon from '@/components/custom/TheIcon.vue'
|
||||
import { useThemeStore } from '@/store'
|
||||
import CreateModal from '@/views/setting/CreateModal.vue'
|
||||
import SettingBase from '@/views/setting/SettingBase.vue'
|
||||
import SettingSafe from '@/views/setting/SettingSafe.vue'
|
||||
import SettingUser from '@/views/setting/SettingUser.vue'
|
||||
@@ -15,6 +16,7 @@ import SettingUser from '@/views/setting/SettingUser.vue'
|
||||
const { $gettext } = useGettext()
|
||||
const themeStore = useThemeStore()
|
||||
const currentTab = ref('base')
|
||||
const createModal = ref(false)
|
||||
|
||||
const { data: model } = useRequest(setting.list, {
|
||||
initialData: {
|
||||
@@ -50,7 +52,9 @@ const handleSave = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = () => {}
|
||||
const handleCreate = () => {
|
||||
createModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -77,6 +81,7 @@ const handleCreate = () => {}
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<create-modal v-model:show="createModal" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import user from '@/api/panel/user'
|
||||
import { formatDateTime, renderIcon } from '@/utils'
|
||||
import PasswordModal from '@/views/setting/PasswordModal.vue'
|
||||
import TokenModal from '@/views/setting/TokenModal.vue'
|
||||
import TwoFaModal from '@/views/setting/TwoFaModal.vue'
|
||||
import { NButton, NDataTable, NInput, NPopconfirm, NSwitch } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
@@ -11,6 +12,7 @@ const { $gettext } = useGettext()
|
||||
const currentID = ref(0)
|
||||
const passwordModal = ref(false)
|
||||
const twoFaModal = ref(false)
|
||||
const tokenModal = ref(false)
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
@@ -73,7 +75,7 @@ const columns: any = [
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 260,
|
||||
width: 380,
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
@@ -82,6 +84,22 @@ const columns: any = [
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
currentID.value = row.id
|
||||
tokenModal.value = true
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Access Token'),
|
||||
icon: renderIcon('material-symbols:edit-outline', { size: 14 })
|
||||
}
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 15px;',
|
||||
onClick: () => {
|
||||
currentID.value = row.id
|
||||
passwordModal.value = true
|
||||
@@ -176,6 +194,7 @@ onMounted(() => {
|
||||
</n-flex>
|
||||
<password-modal v-model:id="currentID" v-model:show="passwordModal" />
|
||||
<two-fa-modal v-model:id="currentID" v-model:show="twoFaModal" />
|
||||
<token-modal v-model:id="currentID" v-model:show="tokenModal" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
307
web/src/views/setting/TokenModal.vue
Normal file
307
web/src/views/setting/TokenModal.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<script setup lang="ts">
|
||||
import user from '@/api/panel/user'
|
||||
import { formatDateTime, renderIcon } from '@/utils'
|
||||
import copy2clipboard from '@vavt/copy2clipboard'
|
||||
import { NAlert, NButton, NDataTable, NFlex, NInput, NPopconfirm } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const id = defineModel<number>('id', { type: Number, required: true })
|
||||
|
||||
const createModal = ref(false)
|
||||
const updateModal = ref(false)
|
||||
|
||||
const currentID = ref(0)
|
||||
const createModel = ref({
|
||||
ips: [] as Array<string>,
|
||||
expired_at: new Date().getTime() + 31536000 * 1000 // 1 year
|
||||
})
|
||||
const updateModel = ref({
|
||||
ips: [] as Array<string>,
|
||||
expired_at: new Date().getTime() + 31536000 * 1000 // 1 year
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('ID'),
|
||||
key: 'id',
|
||||
width: 100,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Creation Time'),
|
||||
key: 'created_at',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return formatDateTime(row.created_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Expiration Time'),
|
||||
key: 'expired_at',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return formatDateTime(row.expired_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 260,
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
currentID.value = row.id
|
||||
updateModal.value = true
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Change'),
|
||||
icon: renderIcon('material-symbols:edit-outline', { size: 14 })
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
style: 'margin-left: 15px;',
|
||||
onPositiveClick: () => handleDelete(row.id)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return $gettext('Are you sure you want to delete this access token?')
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Delete'),
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => user.tokenList(id.value, page, pageSize),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 20,
|
||||
total: (res: any) => res.total,
|
||||
data: (res: any) => res.items
|
||||
}
|
||||
)
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
useRequest(() => user.tokenDelete(id)).onSuccess(() => {
|
||||
window.$message.success($gettext('Deleted successfully'))
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
useRequest(() =>
|
||||
user.tokenCreate(id.value, createModel.value.ips, createModel.value.expired_at)
|
||||
).onSuccess(({ data }) => {
|
||||
createModal.value = false
|
||||
window.$dialog.success({
|
||||
title: $gettext('Created successfully'),
|
||||
content: () => {
|
||||
return [
|
||||
h(
|
||||
NFlex,
|
||||
{
|
||||
vertical: true
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NAlert,
|
||||
{
|
||||
type: 'warning'
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
$gettext(
|
||||
'Token is only displayed once, please save it before closing the dialog'
|
||||
)
|
||||
}
|
||||
),
|
||||
h(NInput, {
|
||||
value: data.token,
|
||||
type: 'password',
|
||||
showPasswordOn: 'click',
|
||||
readonly: true
|
||||
})
|
||||
]
|
||||
}
|
||||
)
|
||||
]
|
||||
},
|
||||
maskClosable: false,
|
||||
positiveText: $gettext('Copy and close'),
|
||||
onPositiveClick: () => {
|
||||
copy2clipboard(data.token)
|
||||
.then(() => {
|
||||
window.$message.success($gettext('Copied successfully'))
|
||||
})
|
||||
.catch(() => {
|
||||
window.$message.error($gettext('Copy failed'))
|
||||
})
|
||||
.finally(() => {
|
||||
createModal.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
useRequest(() =>
|
||||
user.tokenUpdate(currentID.value, updateModel.value.ips, updateModel.value.expired_at)
|
||||
).onSuccess(() => {
|
||||
window.$message.success($gettext('Updated successfully'))
|
||||
updateModal.value = false
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => show.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
refresh()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="$gettext('Access Token')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="show = false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="createModal = true">
|
||||
{{ $gettext('Create Access Token') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="600"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => row.name"
|
||||
v-model:page="page"
|
||||
v-model:pageSize="pageSize"
|
||||
:pagination="{
|
||||
page: page,
|
||||
pageCount: pageCount,
|
||||
pageSize: pageSize,
|
||||
itemCount: total,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
}"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
v-model:show="createModal"
|
||||
preset="card"
|
||||
:title="$gettext('Create Access Token')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="createModal = false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('IP White List')">
|
||||
<n-dynamic-input
|
||||
v-model:value="createModel.ips"
|
||||
:placeholder="$gettext('127.0.0.1')"
|
||||
show-sort-button
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Expiration Time')">
|
||||
<n-date-picker
|
||||
v-model:value="createModel.expired_at"
|
||||
type="datetime"
|
||||
placeholder="$gettext('Please select the expiration time')"
|
||||
w-full
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="primary" @click="handleCreate">
|
||||
{{ $gettext('Create') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
v-model:show="updateModal"
|
||||
preset="card"
|
||||
:title="$gettext('Update Access Token')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="updateModal = false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('IP White List')">
|
||||
<n-dynamic-input
|
||||
v-model:value="updateModel.ips"
|
||||
:placeholder="$gettext('127.0.0.1')"
|
||||
show-sort-button
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Expiration Time')">
|
||||
<n-date-picker
|
||||
v-model:value="updateModel.expired_at"
|
||||
type="datetime"
|
||||
placeholder="$gettext('Please select the expiration time')"
|
||||
w-full
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="primary" @click="handleUpdate">
|
||||
{{ $gettext('Update') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -63,7 +63,7 @@ watch(
|
||||
}}
|
||||
</n-text>
|
||||
<n-text style="max-width: 400px; word-break: break-all">
|
||||
{{ model.url }}
|
||||
<a :href="model.url" target="_blank">{{ model.url }}</a>
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
Reference in New Issue
Block a user