From e06b92b216d4839c3a212a2d144f4a698e5f14a6 Mon Sep 17 00:00:00 2001
From: VaalaCat
Date: Mon, 28 Apr 2025 11:31:30 +0000
Subject: [PATCH] feat: rbac and temp node
---
biz/master/auth/login.go | 28 +-
biz/master/auth/register.go | 7 +-
biz/master/client/create_client.go | 4 +
biz/master/client/get_client.go | 14 +-
biz/master/client/list_client.go | 7 +-
biz/master/client/rpc_pull_config.go | 14 +-
biz/master/client/update_tunnel.go | 12 +-
biz/master/handler.go | 33 ++-
biz/master/server/get_server.go | 2 +-
biz/master/server/list_server.go | 2 +-
biz/master/server/update_tunnel.go | 2 +-
biz/master/user/sign_token.go | 45 +++
cmd/frpp/main.go | 11 +-
cmd/frpp/{ => shared}/client.go | 2 +-
cmd/frpp/{ => shared}/cmd.go | 150 ++++++----
cmd/frpp/{ => shared}/master.go | 3 +-
cmd/frpp/{ => shared}/modules.go | 15 +-
cmd/frpp/{ => shared}/providers.go | 122 ++++++--
cmd/frpp/{ => shared}/server.go | 2 +-
cmd/frpp/{ => shared}/test.go | 2 +-
cmd/frppc/client.go | 72 -----
cmd/frppc/cmd.go | 319 ---------------------
cmd/frppc/main.go | 29 +-
common/context.go | 25 ++
common/request.go | 2 +-
common/response.go | 2 +-
conf/helper.go | 29 +-
defs/const.go | 32 +++
defs/error.go | 1 +
defs/rbac_consts.go | 43 +++
docs/zh/deploy-client.md | 6 +-
go.mod | 20 +-
go.sum | 107 ++++++-
idl/api_auth.proto | 15 +
idl/api_client.proto | 3 +-
idl/common.proto | 4 +-
middleware/auth.go | 10 +-
middleware/jwt.go | 52 ++--
middleware/rbac.go | 82 ++++++
models/cert.go | 2 +-
models/client.go | 11 +-
models/db.go | 89 +++---
models/enums.go | 5 +-
models/server.go | 4 +-
models/user.go | 9 +-
models/user_group.go | 18 ++
pb/api_auth.pb.go | 209 +++++++++++++-
pb/api_client.pb.go | 17 +-
pb/common.pb.go | 30 +-
services/app/app_impl.go | 34 +++
services/app/application.go | 7 +
services/app/provider.go | 20 +-
services/dao/client.go | 29 +-
services/dao/user.go | 14 +
services/dao/user_group.go | 45 +++
services/master/grpc_server.go | 6 +
services/rbac/init.go | 43 +++
services/rbac/perm_manager.go | 82 ++++++
services/rbac/rbac_models.go | 17 ++
services/rpc/master.go | 11 +-
utils/ctx.go | 2 +-
utils/json.go | 11 +
utils/jwt.go | 2 +-
www/api/user.ts | 6 +
www/components/frpc/client_item.tsx | 4 +
www/components/frpc/client_join_button.tsx | 63 +++-
www/config/notify.ts | 12 +-
www/i18n/locales/en.json | 3 +-
www/i18n/locales/zh.json | 3 +-
www/lib/consts.ts | 2 +-
www/lib/pb/api_auth.ts | 199 +++++++++++++
www/lib/pb/api_client.ts | 13 +-
www/lib/pb/common.ts | 24 +-
73 files changed, 1664 insertions(+), 712 deletions(-)
create mode 100644 biz/master/user/sign_token.go
rename cmd/frpp/{ => shared}/client.go (99%)
rename cmd/frpp/{ => shared}/cmd.go (78%)
rename cmd/frpp/{ => shared}/master.go (97%)
rename cmd/frpp/{ => shared}/modules.go (81%)
rename cmd/frpp/{ => shared}/providers.go (74%)
rename cmd/frpp/{ => shared}/server.go (99%)
rename cmd/frpp/{ => shared}/test.go (97%)
delete mode 100644 cmd/frppc/client.go
delete mode 100644 cmd/frppc/cmd.go
create mode 100644 defs/error.go
create mode 100644 defs/rbac_consts.go
create mode 100644 middleware/rbac.go
create mode 100644 models/user_group.go
create mode 100644 services/dao/user_group.go
create mode 100644 services/rbac/init.go
create mode 100644 services/rbac/perm_manager.go
create mode 100644 services/rbac/rbac_models.go
create mode 100644 utils/json.go
diff --git a/biz/master/auth/login.go b/biz/master/auth/login.go
index e96620a..738926e 100644
--- a/biz/master/auth/login.go
+++ b/biz/master/auth/login.go
@@ -1,13 +1,14 @@
package auth
import (
- "fmt"
-
"github.com/VaalaCat/frp-panel/conf"
+ "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/middleware"
+ "github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
+ "github.com/VaalaCat/frp-panel/utils/logger"
)
func LoginHandler(ctx *app.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
@@ -24,7 +25,28 @@ func LoginHandler(ctx *app.Context, req *pb.LoginRequest) (*pb.LoginResponse, er
}, nil
}
- tokenStr := conf.GetCommonJWT(ctx.GetApp().GetConfig(), fmt.Sprint(user.GetUserID()))
+ userCount, err := dao.NewQuery(ctx).AdminCountUsers()
+ if err != nil {
+ logger.Logger(ctx).WithError(err).Error("get user count failed")
+ }
+
+ if userCount == 1 && user.GetSafeUserInfo().Role != defs.UserRole_Admin {
+ userEntity, ok := user.(models.User)
+ if !ok {
+ logger.Logger(ctx).Errorf("trans user entity failed, invalid user entity")
+ return &pb.LoginResponse{
+ Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "invalid user"},
+ }, nil
+ }
+
+ userEntity.Role = defs.UserRole_Admin
+
+ dao.NewQuery(ctx).AdminUpdateUser(&models.UserEntity{
+ UserID: user.GetUserID(),
+ }, userEntity.UserEntity)
+ }
+
+ tokenStr := conf.GetJWTWithAllPermission(ctx.GetApp().GetConfig(), user.GetUserID())
ginCtx := ctx.GetGinCtx()
middleware.PushTokenStr(ginCtx, ctx.GetApp(), tokenStr)
diff --git a/biz/master/auth/register.go b/biz/master/auth/register.go
index 630cb9d..fc44ea9 100644
--- a/biz/master/auth/register.go
+++ b/biz/master/auth/register.go
@@ -3,6 +3,7 @@ package auth
import (
"fmt"
+ "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
@@ -47,10 +48,14 @@ func RegisterHandler(c *app.Context, req *pb.RegisterRequest) (*pb.RegisterRespo
Password: hashedPassword,
Email: email,
Status: models.STATUS_NORMAL,
- Role: models.ROLE_NORMAL,
+ Role: defs.UserRole_Normal,
Token: uuid.New().String(),
}
+ if userCount == 0 {
+ newUser.Role = defs.UserRole_Admin
+ }
+
err = dao.NewQuery(c).CreateUser(newUser)
if err != nil {
return &pb.RegisterResponse{
diff --git a/biz/master/client/create_client.go b/biz/master/client/create_client.go
index 29a1a24..57c22d4 100644
--- a/biz/master/client/create_client.go
+++ b/biz/master/client/create_client.go
@@ -7,6 +7,7 @@ import (
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils"
+ "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/google/uuid"
)
@@ -28,6 +29,8 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien
globalClientID := app.GlobalClientID(userInfo.GetUserName(), "c", userClientID)
+ logger.Logger(c).Infof("start to init client, request:[%s], transformed global client id:[%s]", req.String(), globalClientID)
+
if err := dao.NewQuery(c).CreateClient(userInfo,
&models.ClientEntity{
ClientID: globalClientID,
@@ -35,6 +38,7 @@ func InitClientHandler(c *app.Context, req *pb.InitClientRequest) (*pb.InitClien
UserID: userInfo.GetUserID(),
ConnectSecret: uuid.New().String(),
IsShadow: true,
+ Ephemeral: req.GetEphemeral(),
}); err != nil {
return &pb.InitClientResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err
}
diff --git a/biz/master/client/get_client.go b/biz/master/client/get_client.go
index 49b6242..686990e 100644
--- a/biz/master/client/get_client.go
+++ b/biz/master/client/get_client.go
@@ -1,6 +1,8 @@
package client
import (
+ "strings"
+
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
@@ -31,6 +33,10 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient
}, nil
}
+ if !strings.Contains(clientID, ".") {
+ clientID = app.GlobalClientID(userInfo.GetUserName(), "c", clientID)
+ }
+
respCli := &pb.Client{}
if len(serverID) == 0 {
client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
@@ -50,6 +56,8 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient
Stopped: lo.ToPtr(client.Stopped),
Comment: lo.ToPtr(client.Comment),
ClientIds: clientIDs,
+ Ephemeral: &client.Ephemeral,
+ // LastSeenAt: lo.ToPtr(client.LastSeenAt.UnixMilli()),
}
} else {
client, err := dao.NewQuery(ctx).GetClientByFilter(userInfo, &models.ClientEntity{
@@ -73,9 +81,13 @@ func GetClientHandler(ctx *app.Context, req *pb.GetClientRequest) (*pb.GetClient
ServerId: lo.ToPtr(client.ServerID),
Stopped: lo.ToPtr(client.Stopped),
Comment: lo.ToPtr(client.Comment),
- FrpsUrl: lo.ToPtr(client.FRPsUrl),
+ FrpsUrl: lo.ToPtr(client.FrpsUrl),
+ Ephemeral: &client.Ephemeral,
ClientIds: nil,
}
+ if client.LastSeenAt != nil {
+ respCli.LastSeenAt = lo.ToPtr(client.LastSeenAt.UnixMilli())
+ }
}
return &pb.GetClientResponse{
diff --git a/biz/master/client/list_client.go b/biz/master/client/list_client.go
index 1acf867..eea8973 100644
--- a/biz/master/client/list_client.go
+++ b/biz/master/client/list_client.go
@@ -59,7 +59,7 @@ func ListClientsHandler(ctx *app.Context, req *pb.ListClientsRequest) (*pb.ListC
logger.Logger(ctx).Errorf("get client ids in shadow by client id error: %v", err)
}
- return &pb.Client{
+ respCli := &pb.Client{
Id: lo.ToPtr(c.ClientID),
Secret: lo.ToPtr(c.ConnectSecret),
Config: lo.ToPtr(string(c.ConfigContent)),
@@ -67,7 +67,12 @@ func ListClientsHandler(ctx *app.Context, req *pb.ListClientsRequest) (*pb.ListC
Stopped: lo.ToPtr(c.Stopped),
Comment: lo.ToPtr(c.Comment),
ClientIds: clientIDs,
+ Ephemeral: lo.ToPtr(c.Ephemeral),
}
+ if c.LastSeenAt != nil {
+ respCli.LastSeenAt = lo.ToPtr(c.LastSeenAt.UnixMilli())
+ }
+ return respCli
})
return &pb.ListClientsResponse{
diff --git a/biz/master/client/rpc_pull_config.go b/biz/master/client/rpc_pull_config.go
index aef657b..2cef8c6 100644
--- a/biz/master/client/rpc_pull_config.go
+++ b/biz/master/client/rpc_pull_config.go
@@ -1,8 +1,6 @@
package client
import (
- "fmt"
-
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
@@ -23,6 +21,11 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien
return nil, err
}
+ if err := dao.NewQuery(ctx).AdminUpdateClientLastSeen(cli.ClientID); err != nil {
+ logger.Logger(ctx).WithError(err).Errorf("update client last_seen_at time error, req:[%s] clientId:[%s]",
+ req.String(), cli.ClientID)
+ }
+
if cli.IsShadow {
proxies, err := dao.NewQuery(ctx).AdminListProxyConfigsWithFilters(&models.ProxyConfigEntity{
OriginClientID: cli.ClientID,
@@ -34,7 +37,12 @@ func RPCPullConfig(ctx *app.Context, req *pb.PullClientConfigReq) (*pb.PullClien
}
if cli.Stopped && cli.IsShadow {
- return nil, fmt.Errorf("client is stopped")
+ return &pb.PullClientConfigResp{
+ Client: &pb.Client{
+ Id: lo.ToPtr(cli.ClientID),
+ Stopped: lo.ToPtr(true),
+ },
+ }, nil
}
return &pb.PullClientConfigResp{
diff --git a/biz/master/client/update_tunnel.go b/biz/master/client/update_tunnel.go
index 0972e1c..9702e76 100644
--- a/biz/master/client/update_tunnel.go
+++ b/biz/master/client/update_tunnel.go
@@ -102,7 +102,7 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
cliCfg.ServerPort = srvConf.BindPort
}
- if len(req.GetFrpsUrl()) > 0 || len(cli.FRPsUrl) > 0 {
+ if len(req.GetFrpsUrl()) > 0 || len(cli.FrpsUrl) > 0 {
// 有一个有就需要覆盖,优先请求的url
var (
parsedFrpsUrl *url.URL
@@ -120,21 +120,21 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
urlToParse = req.GetFrpsUrl()
}
- if len(cli.FRPsUrl) > 0 && parsedFrpsUrl == nil {
- parsedFrpsUrl, err = ValidateFrpsUrl(cli.FRPsUrl)
+ if len(cli.FrpsUrl) > 0 && parsedFrpsUrl == nil {
+ parsedFrpsUrl, err = ValidateFrpsUrl(cli.FrpsUrl)
if err != nil {
- logger.Logger(c).WithError(err).Errorf("invalid old frps url, url: [%s]", cli.FRPsUrl)
+ logger.Logger(c).WithError(err).Errorf("invalid old frps url, url: [%s]", cli.FrpsUrl)
return &pb.UpdateFRPCResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()},
}, err
}
- urlToParse = cli.FRPsUrl
+ urlToParse = cli.FrpsUrl
}
cliCfg.ServerAddr = parsedFrpsUrl.Hostname()
cliCfg.ServerPort = cast.ToInt(parsedFrpsUrl.Port())
cliCfg.Transport.Protocol = parsedFrpsUrl.Scheme
- cli.FRPsUrl = urlToParse
+ cli.FrpsUrl = urlToParse
}
cliCfg.User = userInfo.GetUserName()
diff --git a/biz/master/handler.go b/biz/master/handler.go
index 0f3f796..36a3100 100644
--- a/biz/master/handler.go
+++ b/biz/master/handler.go
@@ -27,52 +27,51 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
router.POST("/auth", auth.MakeGinHandlerFunc(appInstance, auth.HandleLogin))
api := router.Group("/api")
- v1 := api.Group("/v1")
+ api.POST("/v1/auth/cert", app.Wrapper(appInstance, auth.GetClientCert))
+ api.POST("/v1/auth/login", app.Wrapper(appInstance, auth.LoginHandler))
+ api.POST("/v1/auth/register", app.Wrapper(appInstance, auth.RegisterHandler))
+ api.GET("/v1/auth/logout", auth.RemoveJWTHandler(appInstance))
+
+ v1 := api.Group("/v1", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), middleware.RBAC(appInstance))
{
- authRouter := v1.Group("/auth")
- {
- authRouter.POST("/login", app.Wrapper(appInstance, auth.LoginHandler))
- authRouter.POST("/register", app.Wrapper(appInstance, auth.RegisterHandler))
- authRouter.GET("/logout", auth.RemoveJWTHandler(appInstance))
- authRouter.POST("/cert", app.Wrapper(appInstance, auth.GetClientCert))
- }
- userRouter := v1.Group("/user", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ userRouter := v1.Group("/user")
{
userRouter.POST("/get", app.Wrapper(appInstance, user.GetUserInfoHandler))
userRouter.POST("/update", app.Wrapper(appInstance, user.UpdateUserInfoHander))
+ userRouter.POST("/sign-token", app.Wrapper(appInstance, user.SignTokenHandler))
}
- platformRouter := v1.Group("/platform", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ platformRouter := v1.Group("/platform")
{
platformRouter.GET("/baseinfo", platform.GetPlatformInfo(appInstance))
platformRouter.POST("/clientsstatus", app.Wrapper(appInstance, platform.GetClientsStatus))
}
- clientRouter := v1.Group("/client", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ clientRouter := v1.Group("/client")
{
clientRouter.POST("/get", app.Wrapper(appInstance, client.GetClientHandler))
clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler))
clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler))
clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler))
}
- serverRouter := v1.Group("/server", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ serverRouter := v1.Group("/server")
{
serverRouter.POST("/get", app.Wrapper(appInstance, server.GetServerHandler))
serverRouter.POST("/init", app.Wrapper(appInstance, server.InitServerHandler))
serverRouter.POST("/delete", app.Wrapper(appInstance, server.DeleteServerHandler))
serverRouter.POST("/list", app.Wrapper(appInstance, server.ListServersHandler))
}
- frpcRouter := v1.Group("/frpc", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ frpcRouter := v1.Group("/frpc")
{
frpcRouter.POST("/update", app.Wrapper(appInstance, client.UpdateFrpcHander))
frpcRouter.POST("/delete", app.Wrapper(appInstance, client.RemoveFrpcHandler))
frpcRouter.POST("/stop", app.Wrapper(appInstance, client.StopFRPCHandler))
frpcRouter.POST("/start", app.Wrapper(appInstance, client.StartFRPCHandler))
}
- frpsRouter := v1.Group("/frps", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ frpsRouter := v1.Group("/frps")
{
frpsRouter.POST("/update", app.Wrapper(appInstance, server.UpdateFrpsHander))
frpsRouter.POST("/delete", app.Wrapper(appInstance, server.RemoveFrpsHandler))
}
- proxyRouter := v1.Group("/proxy", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance))
+ proxyRouter := v1.Group("/proxy")
{
proxyRouter.POST("/get_by_cid", app.Wrapper(appInstance, proxy.GetProxyStatsByClientID))
proxyRouter.POST("/get_by_sid", app.Wrapper(appInstance, proxy.GetProxyStatsByServerID))
@@ -82,7 +81,7 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
proxyRouter.POST("/delete_config", app.Wrapper(appInstance, proxy.DeleteProxyConfig))
proxyRouter.POST("/get_config", app.Wrapper(appInstance, proxy.GetProxyConfig))
}
- v1.GET("/pty/:clientID", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), shell.PTYHandler(appInstance))
- v1.GET("/log", middleware.JWTAuth(appInstance), middleware.AuthCtx(appInstance), streamlog.GetLogHandler(appInstance))
+ v1.GET("/pty/:clientID", shell.PTYHandler(appInstance))
+ v1.GET("/log", streamlog.GetLogHandler(appInstance))
}
}
diff --git a/biz/master/server/get_server.go b/biz/master/server/get_server.go
index 96b61bf..7d49fdf 100644
--- a/biz/master/server/get_server.go
+++ b/biz/master/server/get_server.go
@@ -39,7 +39,7 @@ func GetServerHandler(c *app.Context, req *pb.GetServerRequest) (*pb.GetServerRe
Secret: lo.ToPtr(serverEntity.ConnectSecret),
Comment: lo.ToPtr(serverEntity.Comment),
Ip: lo.ToPtr(serverEntity.ServerIP),
- FrpsUrls: serverEntity.FRPsUrls,
+ FrpsUrls: serverEntity.FrpsUrls,
},
}, nil
}
diff --git a/biz/master/server/list_server.go b/biz/master/server/list_server.go
index 60a17f8..b1febd3 100644
--- a/biz/master/server/list_server.go
+++ b/biz/master/server/list_server.go
@@ -54,7 +54,7 @@ func ListServersHandler(c *app.Context, req *pb.ListServersRequest) (*pb.ListSer
Secret: lo.ToPtr(c.ConnectSecret),
Ip: lo.ToPtr(c.ServerIP),
Comment: lo.ToPtr(c.Comment),
- FrpsUrls: c.FRPsUrls,
+ FrpsUrls: c.FrpsUrls,
}
}),
Total: lo.ToPtr(int32(serverCounts)),
diff --git a/biz/master/server/update_tunnel.go b/biz/master/server/update_tunnel.go
index c0b83db..e1bab14 100644
--- a/biz/master/server/update_tunnel.go
+++ b/biz/master/server/update_tunnel.go
@@ -53,7 +53,7 @@ func UpdateFrpsHander(c *app.Context, req *pb.UpdateFRPSRequest) (*pb.UpdateFRPS
}
if len(req.GetFrpsUrls()) > 0 {
- srv.FRPsUrls = req.GetFrpsUrls()
+ srv.FrpsUrls = req.GetFrpsUrls()
}
if err := dao.NewQuery(c).UpdateServer(userInfo, srv); err != nil {
diff --git a/biz/master/user/sign_token.go b/biz/master/user/sign_token.go
new file mode 100644
index 0000000..96bfc32
--- /dev/null
+++ b/biz/master/user/sign_token.go
@@ -0,0 +1,45 @@
+package user
+
+import (
+ "time"
+
+ "github.com/VaalaCat/frp-panel/common"
+ "github.com/VaalaCat/frp-panel/conf"
+ "github.com/VaalaCat/frp-panel/defs"
+ "github.com/VaalaCat/frp-panel/pb"
+ "github.com/VaalaCat/frp-panel/services/app"
+ "github.com/VaalaCat/frp-panel/utils"
+ "github.com/VaalaCat/frp-panel/utils/logger"
+ "github.com/samber/lo"
+)
+
+func SignTokenHandler(ctx *app.Context, req *pb.SignTokenRequest) (*pb.SignTokenResponse, error) {
+ var (
+ userInfo = common.GetUserInfo(ctx)
+ permissions = req.GetPermissions()
+ expiresIn = req.GetExpiresIn()
+ cfg = ctx.GetApp().GetConfig()
+ )
+
+ token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(cfg),
+ time.Now().Unix(),
+ int64(expiresIn),
+ map[string]interface{}{
+ defs.UserIDKey: userInfo.GetUserID(),
+ defs.TokenPayloadKey_Permissions: permissions,
+ })
+ if err != nil {
+ logger.Logger(ctx).WithError(err).Errorf("get jwt token failed, req: [%s]", req.String())
+ return nil, err
+ }
+
+ logger.Logger(ctx).Infof("get jwt token success, req: [%s]", req.String())
+
+ return &pb.SignTokenResponse{
+ Token: lo.ToPtr(token),
+ Status: &pb.Status{
+ Code: pb.RespCode_RESP_CODE_SUCCESS,
+ Message: "ok",
+ },
+ }, nil
+}
diff --git a/cmd/frpp/main.go b/cmd/frpp/main.go
index 8df19fe..bbbe23d 100644
--- a/cmd/frpp/main.go
+++ b/cmd/frpp/main.go
@@ -1,16 +1,23 @@
package main
import (
+ "embed"
+
+ "github.com/VaalaCat/frp-panel/cmd/frpp/shared"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/fatedier/golib/crypto"
"github.com/spf13/cobra"
)
+//go:embed all:out
+var fs embed.FS
+
func main() {
crypto.DefaultSalt = "frp"
logger.InitLogger()
cobra.MousetrapHelpText = ""
- rootCmd := buildCommand()
- setMasterCommandIfNonePresent(rootCmd)
+
+ rootCmd := shared.BuildCommand(fs)
+ shared.SetMasterCommandIfNonePresent(rootCmd)
rootCmd.Execute()
}
diff --git a/cmd/frpp/client.go b/cmd/frpp/shared/client.go
similarity index 99%
rename from cmd/frpp/client.go
rename to cmd/frpp/shared/client.go
index 11208b6..ddf3e2e 100644
--- a/cmd/frpp/client.go
+++ b/cmd/frpp/shared/client.go
@@ -1,4 +1,4 @@
-package main
+package shared
import (
"context"
diff --git a/cmd/frpp/cmd.go b/cmd/frpp/shared/cmd.go
similarity index 78%
rename from cmd/frpp/cmd.go
rename to cmd/frpp/shared/cmd.go
index b6edee0..f5f72e5 100644
--- a/cmd/frpp/cmd.go
+++ b/cmd/frpp/shared/cmd.go
@@ -1,7 +1,8 @@
-package main
+package shared
import (
"context"
+ "embed"
"errors"
"fmt"
"os"
@@ -30,9 +31,10 @@ type CommonArgs struct {
RpcPort *int
ApiPort *int
ApiScheme *string
+ JoinToken *string
}
-func buildCommand() *cobra.Command {
+func BuildCommand(fs embed.FS) *cobra.Command {
cfg := conf.NewConfig()
logger.UpdateLoggerOpt(
@@ -41,7 +43,7 @@ func buildCommand() *cobra.Command {
)
return NewRootCmd(
- NewMasterCmd(cfg),
+ NewMasterCmd(cfg, fs),
NewClientCmd(cfg),
NewServerCmd(cfg),
NewJoinCmd(),
@@ -59,6 +61,7 @@ func AddCommonFlags(commonCmd *cobra.Command) {
commonCmd.Flags().StringP("id", "i", "", "client id")
commonCmd.Flags().String("rpc-url", "", "rpc url, master rpc url, scheme can be grpc/ws/wss://hostname:port")
commonCmd.Flags().String("api-url", "", "api url, master api url, scheme can be http/https://hostname:port")
+ commonCmd.Flags().StringP("join-token", "j", "", "your token from master, auto join with out webui")
// deprecated start
commonCmd.Flags().StringP("app", "a", "", "app secret")
@@ -109,12 +112,11 @@ func GetCommonArgs(cmd *cobra.Command) CommonArgs {
commonArgs.ApiScheme = &apiScheme
}
- return commonArgs
-}
+ if joinToken, err := cmd.Flags().GetString("join-token"); err == nil {
+ commonArgs.JoinToken = &joinToken
+ }
-type JoinArgs struct {
- CommonArgs
- JoinToken *string
+ return commonArgs
}
func NewJoinCmd() *cobra.Command {
@@ -122,29 +124,25 @@ func NewJoinCmd() *cobra.Command {
Use: "join [-j join token] [-r rpc host] [-p api port] [-e api scheme]",
Short: "join to master with token, save param to config",
Run: func(cmd *cobra.Command, args []string) {
+ ctx := context.Background()
commonArgs := GetCommonArgs(cmd)
warnDepParam(cmd)
- joinArgs := &JoinArgs{
- CommonArgs: commonArgs,
+ cli, err := JoinMaster(conf.NewConfig(), commonArgs)
+ if err != nil {
+ logger.Logger(ctx).Fatalf("join master failed: %s", err.Error())
}
- if joinToken, err := cmd.Flags().GetString("join-token"); err == nil {
- joinArgs.JoinToken = &joinToken
- }
-
- appInstance := app.NewApp()
- pullRunConfig(appInstance, joinArgs)
+ saveConfig(ctx, cli, commonArgs)
},
}
- joinCmd.Flags().StringP("join-token", "j", "", "your token from master")
AddCommonFlags(joinCmd)
return joinCmd
}
-func NewMasterCmd(cfg conf.Config) *cobra.Command {
+func NewMasterCmd(cfg conf.Config, fs embed.FS) *cobra.Command {
return &cobra.Command{
Use: "master",
Short: "run frp-panel manager",
@@ -159,6 +157,8 @@ func NewMasterCmd(cfg conf.Config) *cobra.Command {
fx.Supply(
CommonArgs{},
fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)),
+ fs,
+ defs.AppRole_Master,
),
fx.Provide(fx.Annotate(NewDefaultServerConfig, fx.ResultTags(`name:"defaultServerConfig"`))),
fx.Invoke(NewConfigPrinter),
@@ -202,6 +202,7 @@ func NewClientCmd(cfg conf.Config) *cobra.Command {
fx.Supply(
commonArgs,
fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)),
+ defs.AppRole_Client,
),
fx.Invoke(NewConfigPrinter),
fx.Invoke(runClient),
@@ -246,6 +247,7 @@ func NewServerCmd(cfg conf.Config) *cobra.Command {
fx.Supply(
commonArgs,
fx.Annotate(cfg, fx.ResultTags(`name:"originConfig"`)),
+ defs.AppRole_Server,
),
fx.Invoke(runServer),
}
@@ -403,7 +405,7 @@ func warnDepParam(cmd *cobra.Command) {
}
}
-func setMasterCommandIfNonePresent(rootCmd *cobra.Command) {
+func SetMasterCommandIfNonePresent(rootCmd *cobra.Command) {
cmd, _, err := rootCmd.Find(os.Args[1:])
if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
args := append([]string{"master"}, os.Args[1:]...)
@@ -411,96 +413,120 @@ func setMasterCommandIfNonePresent(rootCmd *cobra.Command) {
}
}
-func pullRunConfig(appInstance app.Application, joinArgs *JoinArgs) {
+func SetClientCommandIfNonePresent(rootCmd *cobra.Command) {
+ cmd, _, err := rootCmd.Find(os.Args[1:])
+ if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
+ args := append([]string{"client"}, os.Args[1:]...)
+ rootCmd.SetArgs(args)
+ }
+}
+
+func JoinMaster(cfg conf.Config, joinArgs CommonArgs) (*pb.Client, error) {
c := context.Background()
if err := checkPullParams(joinArgs); err != nil {
logger.Logger(c).Errorf("check pull params failed: %s", err.Error())
- return
- }
-
- if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil {
- logger.Logger(c).Errorf("ensure directory failed: %s", err.Error())
- return
+ return nil, err
}
var clientID string
if cliID := joinArgs.ClientID; cliID == nil || len(*cliID) == 0 {
clientID = utils.GetHostnameWithIP()
+ } else {
+ clientID = *cliID
}
clientID = utils.MakeClientIDPermited(clientID)
- patchConfig(appInstance, joinArgs.CommonArgs)
- initResp, err := rpc.InitClient(appInstance, clientID, *joinArgs.JoinToken)
- if err != nil {
- logger.Logger(c).Errorf("init client failed: %s", err.Error())
- return
- }
- if initResp == nil {
- logger.Logger(c).Errorf("init resp is nil")
- return
- }
- if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
- logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage())
- return
+ logger.Logger(c).Infof("join master with param, clientId:[%s] joinArgs:[%s]", clientID, utils.MarshalForJson(joinArgs))
+
+ // 检测是否存在已有的client
+ clientResp, err := rpc.GetClient(cfg, clientID, *joinArgs.JoinToken)
+ if err != nil || clientResp == nil || clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
+ logger.Logger(c).Infof("client [%s] not found, try to init client", clientID)
+
+ // 创建短期client
+ initResp, err := rpc.InitClient(cfg, clientID, *joinArgs.JoinToken, true)
+ if err != nil {
+ logger.Logger(c).Errorf("init client failed: %s", err.Error())
+ return nil, err
+ }
+ if initResp == nil {
+ logger.Logger(c).Errorf("init resp is nil")
+ return nil, err
+ }
+ if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
+ logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage())
+ return nil, err
+ }
+
+ clientID = initResp.GetClientId()
+ clientResp, err = rpc.GetClient(cfg, clientID, *joinArgs.JoinToken)
+ if err != nil {
+ logger.Logger(c).Errorf("get client failed: %s", err.Error())
+ return nil, err
+ }
}
- clientID = initResp.GetClientId()
- clientResp, err := rpc.GetClient(appInstance, clientID, *joinArgs.JoinToken)
- if err != nil {
- logger.Logger(c).Errorf("get client failed: %s", err.Error())
- return
- }
if clientResp == nil {
logger.Logger(c).Errorf("client resp is nil")
- return
+ return nil, err
}
if clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
logger.Logger(c).Errorf("client resp code is not success: %s", clientResp.GetStatus().GetMessage())
- return
+ return nil, err
}
client := clientResp.GetClient()
if client == nil {
logger.Logger(c).Errorf("client is nil")
+ return nil, err
+ }
+
+ return client, nil
+}
+
+func saveConfig(ctx context.Context, cli *pb.Client, joinArgs CommonArgs) {
+ if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil {
+ logger.Logger(ctx).Errorf("ensure directory failed: %s", err.Error())
return
}
envMap, err := godotenv.Read(defs.SysEnvPath)
if err != nil {
envMap = make(map[string]string)
- logger.Logger(c).Warnf("read env file failed, try to create: %s", err.Error())
+ logger.Logger(ctx).Warnf("read env file failed, try to create: %s", err.Error())
}
- envMap[defs.EnvClientID] = clientID
- envMap[defs.EnvClientSecret] = client.GetSecret()
+ envMap[defs.EnvClientID] = cli.GetId()
+ envMap[defs.EnvClientSecret] = cli.GetSecret()
envMap[defs.EnvClientAPIUrl] = *joinArgs.ApiUrl
envMap[defs.EnvClientRPCUrl] = *joinArgs.RpcUrl
if err = godotenv.Write(envMap, defs.SysEnvPath); err != nil {
- logger.Logger(c).Errorf("write env file failed: %s", err.Error())
+ logger.Logger(ctx).Errorf("write env file failed: %s", err.Error())
return
}
- logger.Logger(c).Infof("config saved to env file: %s, you can use `frp-panel client` without args to run client,\n\nconfig is: [%v]", defs.SysEnvPath, envMap)
+ logger.Logger(ctx).Infof("config saved to env file: %s, you can use `frp-panel client` without args to run client,\n\nconfig is: [%v]",
+ defs.SysEnvPath, envMap)
}
-func checkPullParams(joinArgs *JoinArgs) error {
+func checkPullParams(joinArgs CommonArgs) error {
if joinToken := joinArgs.JoinToken; joinToken != nil && len(*joinToken) == 0 {
return errors.New("join token is empty")
}
- if apiUrl := joinArgs.ApiUrl; apiUrl == nil || len(*apiUrl) == 0 {
- if apiHost := joinArgs.ApiHost; apiHost == nil || len(*apiHost) == 0 {
- return errors.New("api host is empty")
- }
- if apiScheme := joinArgs.ApiScheme; apiScheme == nil || len(*apiScheme) == 0 {
- return errors.New("api scheme is empty")
- }
+ var (
+ apiUrlAvaliable = joinArgs.ApiUrl != nil && len(*joinArgs.ApiUrl) > 0
+ rpcUrlAvaliable = joinArgs.RpcUrl != nil && len(*joinArgs.RpcUrl) > 0
+ )
+
+ if !apiUrlAvaliable {
+ return errors.New("api url is empty")
}
- if apiPort := joinArgs.ApiPort; apiPort == nil || *apiPort == 0 {
- return errors.New("api port is empty")
+ if !rpcUrlAvaliable {
+ return errors.New("rpc url is empty")
}
return nil
diff --git a/cmd/frpp/master.go b/cmd/frpp/shared/master.go
similarity index 97%
rename from cmd/frpp/master.go
rename to cmd/frpp/shared/master.go
index 21e969b..0109e1f 100644
--- a/cmd/frpp/master.go
+++ b/cmd/frpp/shared/master.go
@@ -1,4 +1,4 @@
-package main
+package shared
import (
"context"
@@ -35,6 +35,7 @@ type runMasterParam struct {
TaskManager watcher.Client `name:"masterTaskManager"`
WsListener *wsgrpc.WSListener
DefaultServerConfig conf.Config `name:"defaultServerConfig"`
+ PermManager app.PermissionManager
}
func runMaster(param runMasterParam) {
diff --git a/cmd/frpp/modules.go b/cmd/frpp/shared/modules.go
similarity index 81%
rename from cmd/frpp/modules.go
rename to cmd/frpp/shared/modules.go
index 7bcaad6..458762e 100644
--- a/cmd/frpp/modules.go
+++ b/cmd/frpp/shared/modules.go
@@ -1,13 +1,14 @@
-package main
+package shared
import (
"go.uber.org/fx"
)
var (
- clientMod = fx.Module("cmd.client", fx.Provide(
- fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
- ))
+ clientMod = fx.Module("cmd.client",
+ fx.Provide(
+ fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
+ ))
serverMod = fx.Module("cmd.server", fx.Provide(
fx.Annotate(NewServerAPI, fx.ResultTags(`name:"serverApiService"`)),
@@ -16,12 +17,13 @@ var (
))
masterMod = fx.Module("cmd.master", fx.Provide(
+ NewPermissionManager,
+ NewEnforcer,
NewListenerOptions,
NewDBManager,
NewWSListener,
NewMasterTLSConfig,
NewWSUpgrader,
- NewFs,
NewClientLogManager,
fx.Annotate(NewWatcher, fx.ResultTags(`name:"masterTaskManager"`)),
fx.Annotate(NewMasterRouter, fx.ResultTags(`name:"masterRouter"`)),
@@ -34,11 +36,12 @@ var (
))
commonMod = fx.Module("common", fx.Provide(
- NewPatchedConfig,
NewLogHookManager,
NewPTYManager,
NewBaseApp,
NewContext,
NewClientsManager,
+ NewAutoJoin,
+ fx.Annotate(NewPatchedConfig, fx.ResultTags(`name:"argsPatchedConfig"`)),
))
)
diff --git a/cmd/frpp/providers.go b/cmd/frpp/shared/providers.go
similarity index 74%
rename from cmd/frpp/providers.go
rename to cmd/frpp/shared/providers.go
index dce7c92..154f689 100644
--- a/cmd/frpp/providers.go
+++ b/cmd/frpp/shared/providers.go
@@ -1,4 +1,4 @@
-package main
+package shared
import (
"context"
@@ -15,6 +15,7 @@ import (
"github.com/VaalaCat/frp-panel/biz/master/streamlog"
bizserver "github.com/VaalaCat/frp-panel/biz/server"
"github.com/VaalaCat/frp-panel/conf"
+ "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/api"
@@ -22,11 +23,13 @@ import (
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/master"
"github.com/VaalaCat/frp-panel/services/mux"
+ "github.com/VaalaCat/frp-panel/services/rbac"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/services/watcher"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/VaalaCat/frp-panel/utils/wsgrpc"
+ "github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/gorilla/websocket"
@@ -38,9 +41,6 @@ import (
"gorm.io/gorm"
)
-//go:embed all:out
-var fs embed.FS
-
func NewLogHookManager() app.StreamLogHookMgr {
return &bizcommon.HookMgr{}
}
@@ -70,10 +70,6 @@ func NewClientsManager() app.ClientsManager {
return rpc.NewClientsManager()
}
-func NewFs() embed.FS {
- return fs
-}
-
func NewPatchedConfig(param struct {
fx.In
@@ -96,7 +92,7 @@ func NewClientLogManager() app.ClientLogManager {
func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
logger.Logger(ctx).Infof("start to init database, type: %s", appInstance.GetConfig().DB.Type)
- mgr := models.NewDBManager(nil, appInstance.GetConfig().DB.Type)
+ mgr := models.NewDBManager(appInstance.GetConfig().DB.Type)
appInstance.SetDBManager(mgr)
if appInstance.GetConfig().IsDebug {
@@ -104,7 +100,7 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
}
switch appInstance.GetConfig().DB.Type {
- case "sqlite3":
+ case defs.DBTypeSQLite3:
if err := utils.EnsureDirectoryExists(appInstance.GetConfig().DB.DSN); err != nil {
logger.Logger(ctx).WithError(err).Warnf("ensure directory failed, data location: [%s], keep data in current directory",
appInstance.GetConfig().DB.DSN)
@@ -117,27 +113,34 @@ func NewDBManager(ctx *app.Context, appInstance app.Application) app.DBManager {
if sqlitedb, err := gorm.Open(sqlite.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err)
} else {
- appInstance.GetDBManager().SetDB("sqlite3", sqlitedb)
+ appInstance.GetDBManager().SetDB(defs.DBTypeSQLite3, defs.DBRoleDefault, sqlitedb)
logger.Logger(ctx).Infof("init database success, data location: [%s]", appInstance.GetConfig().DB.DSN)
}
- case "mysql":
+ case defs.DBTypeMysql:
if mysqlDB, err := gorm.Open(mysql.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err)
} else {
- appInstance.GetDBManager().SetDB("mysql", mysqlDB)
+ appInstance.GetDBManager().SetDB(defs.DBTypeMysql, defs.DBRoleDefault, mysqlDB)
logger.Logger(ctx).Infof("init database success, data type: [%s]", "mysql")
}
- case "postgres":
+ case defs.DBTypePostgres:
if postgresDB, err := gorm.Open(postgres.Open(appInstance.GetConfig().DB.DSN), &gorm.Config{}); err != nil {
logger.Logger(ctx).Panic(err)
} else {
- appInstance.GetDBManager().SetDB("postgres", postgresDB)
+ appInstance.GetDBManager().SetDB(defs.DBTypePostgres, defs.DBRoleDefault, postgresDB)
logger.Logger(ctx).Infof("init database success, data type: [%s]", "postgres")
}
default:
logger.Logger(ctx).Panicf("currently unsupported database type: %s", appInstance.GetConfig().DB.Type)
}
+ memoryDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
+ if err != nil {
+ logger.Logger(ctx).Panic(err)
+ }
+ appInstance.GetDBManager().SetDB(defs.DBTypeSQLite3, defs.DBRoleRam, memoryDB)
+ logger.Logger(ctx).Infof("init memory database success")
+
appInstance.GetDBManager().Init()
return mgr
}
@@ -279,7 +282,94 @@ func NewDefaultServerConfig(ctx *app.Context) conf.Config {
const splitter = "\n--------------------------------------------\n"
-func NewConfigPrinter(ctx *app.Context, config conf.Config) {
+func NewConfigPrinter(param struct {
+ fx.In
+
+ Ctx *app.Context
+ Config conf.Config
+}) {
+ var (
+ ctx = param.Ctx
+ config = param.Config
+ )
logger.Logger(ctx).Infof("%srunning config is: %s%s", splitter, config.PrintStr(), splitter)
logger.Logger(ctx).Infof("%scurrent version: \n%s%s", splitter, conf.GetVersion().String(), splitter)
}
+
+func NewAutoJoin(param struct {
+ fx.In
+
+ Role defs.AppRole
+ Ctx *app.Context
+ Cfg conf.Config `name:"argsPatchedConfig"`
+ CommonArgs CommonArgs
+}) conf.Config {
+ var (
+ ctx = param.Ctx
+ clientID = param.Cfg.Client.ID
+ clientSecret = param.Cfg.Client.Secret
+ autoJoin = false
+ appInstance = param.Ctx.GetApp()
+ )
+
+ appInstance.SetConfig(param.Cfg)
+
+ if param.Role != defs.AppRole_Client {
+ return param.Cfg
+ }
+
+ // 用户不输入clientID和clientSecret时,使用autoJoin
+ if len(clientSecret) == 0 || len(clientID) == 0 {
+ if param.CommonArgs.JoinToken != nil && len(*param.CommonArgs.JoinToken) > 0 {
+ autoJoin = true
+ } else {
+ if len(clientSecret) == 0 {
+ logger.Logger(ctx).Fatal("client secret cannot be empty")
+ }
+
+ if len(clientID) == 0 {
+ logger.Logger(ctx).Fatal("client id cannot be empty")
+ }
+ }
+ }
+
+ if autoJoin {
+ logger.Logger(ctx).Infof("start to try join master, clientID: [%s], clientSecret: [%s]", clientID, clientSecret)
+ cli, err := JoinMaster(param.Cfg, param.CommonArgs)
+ if err != nil {
+ logger.Logger(ctx).Fatalf("join master failed: %s", err.Error())
+ }
+ logger.Logger(ctx).Infof("join master success, clientID: [%s], clientInfo: [%s]", cli.GetId(), cli.String())
+ tmpCfg := appInstance.GetConfig()
+ tmpCfg.Client.ID = cli.GetId()
+ tmpCfg.Client.Secret = cli.GetSecret()
+ appInstance.SetConfig(tmpCfg)
+ }
+ return appInstance.GetConfig()
+}
+
+func NewPermissionManager(param struct {
+ fx.In
+
+ Enforcer *casbin.Enforcer
+ AppInstance app.Application
+}) app.PermissionManager {
+ permMgr := rbac.NewPermManager(param.Enforcer)
+ param.AppInstance.SetPermManager(permMgr)
+ return permMgr
+}
+
+func NewEnforcer(param struct {
+ fx.In
+
+ Ctx *app.Context
+ DBmanager app.DBManager
+ AppInstance app.Application
+}) *casbin.Enforcer {
+ e, err := rbac.InitializeCasbin(param.Ctx, param.DBmanager.GetDefaultDB())
+ if err != nil {
+ logger.Logger(param.Ctx).WithError(err).Fatal("initialize casbin failed")
+ }
+ param.AppInstance.SetEnforcer(e)
+ return e
+}
diff --git a/cmd/frpp/server.go b/cmd/frpp/shared/server.go
similarity index 99%
rename from cmd/frpp/server.go
rename to cmd/frpp/shared/server.go
index d1cc289..c7e20aa 100644
--- a/cmd/frpp/server.go
+++ b/cmd/frpp/shared/server.go
@@ -1,4 +1,4 @@
-package main
+package shared
import (
"context"
diff --git a/cmd/frpp/test.go b/cmd/frpp/shared/test.go
similarity index 97%
rename from cmd/frpp/test.go
rename to cmd/frpp/shared/test.go
index 9f38051..51e2532 100644
--- a/cmd/frpp/test.go
+++ b/cmd/frpp/shared/test.go
@@ -1,4 +1,4 @@
-package main
+package shared
// func serverThings() {
// time.Sleep(5 * time.Second)
diff --git a/cmd/frppc/client.go b/cmd/frppc/client.go
deleted file mode 100644
index b6aa307..0000000
--- a/cmd/frppc/client.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package main
-
-import (
- "context"
-
- bizclient "github.com/VaalaCat/frp-panel/biz/client"
- "github.com/VaalaCat/frp-panel/defs"
- "github.com/VaalaCat/frp-panel/pb"
- "github.com/VaalaCat/frp-panel/services/app"
- "github.com/VaalaCat/frp-panel/services/rpc"
- "github.com/VaalaCat/frp-panel/services/rpcclient"
- "github.com/VaalaCat/frp-panel/services/tunnel"
- "github.com/VaalaCat/frp-panel/services/watcher"
- "github.com/VaalaCat/frp-panel/utils"
- "github.com/VaalaCat/frp-panel/utils/logger"
- "github.com/sourcegraph/conc"
-)
-
-func runClient(appInstance app.Application) {
- var (
- c = context.Background()
- clientID = appInstance.GetConfig().Client.ID
- clientSecret = appInstance.GetConfig().Client.Secret
- ctx = context.Background()
- )
- logger.Logger(c).Infof("start to run client")
- if len(clientSecret) == 0 {
- logger.Logger(ctx).Fatal("client secret cannot be empty")
- }
-
- if len(clientID) == 0 {
- logger.Logger(ctx).Fatal("client id cannot be empty")
- }
-
- cred, err := utils.TLSClientCertNoValidate(rpc.GetClientCert(appInstance, clientID, clientSecret, pb.ClientType_CLIENT_TYPE_FRPC))
- if err != nil {
- logger.Logger(ctx).Fatal(err)
- }
-
- appInstance.SetRPCCred(cred)
- appInstance.SetMasterCli(rpc.NewMasterCli(appInstance))
- appInstance.SetClientController(tunnel.NewClientController())
-
- r := rpcclient.NewClientRPCHandler(
- appInstance,
- clientID,
- clientSecret,
- pb.Event_EVENT_REGISTER_CLIENT,
- bizclient.HandleServerMessage,
- )
- appInstance.SetClientRPCHandler(r)
-
- w := watcher.NewClient()
- w.AddDurationTask(defs.PullConfigDuration, bizclient.PullConfig, clientID, clientSecret)
-
- initClientOnce(appInstance, clientID, clientSecret)
-
- defer w.Stop()
- defer r.Stop()
-
- var wg conc.WaitGroup
- wg.Go(r.Run)
- wg.Go(w.Run)
- wg.Wait()
-}
-
-func initClientOnce(appInstance app.Application, clientID, clientSecret string) {
- err := bizclient.PullConfig(appInstance, clientID, clientSecret)
- if err != nil {
- logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client config, wait for retry")
- }
-}
diff --git a/cmd/frppc/cmd.go b/cmd/frppc/cmd.go
deleted file mode 100644
index d2cf9d4..0000000
--- a/cmd/frppc/cmd.go
+++ /dev/null
@@ -1,319 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
-
- "github.com/VaalaCat/frp-panel/conf"
- "github.com/VaalaCat/frp-panel/defs"
- "github.com/VaalaCat/frp-panel/pb"
- "github.com/VaalaCat/frp-panel/services/app"
- "github.com/VaalaCat/frp-panel/services/rpc"
- "github.com/VaalaCat/frp-panel/utils"
- "github.com/VaalaCat/frp-panel/utils/logger"
- "github.com/joho/godotenv"
- "github.com/spf13/cobra"
- "github.com/spf13/pflag"
-)
-
-var (
- clientCmd *cobra.Command
- rootCmd *cobra.Command
-)
-
-func initCommand(appInstance app.Application) {
- rootCmd = &cobra.Command{
- Use: "frp-panel",
- Short: "frp-panel is a frp panel QwQ",
- }
- CmdListWithFlag := initCmdWithFlag(appInstance)
- CmdListWithoutFlag := initCmdWithoutFlag(appInstance)
- rootCmd.AddCommand(CmdListWithFlag...)
- rootCmd.AddCommand(CmdListWithoutFlag...)
-}
-
-func initCmdWithFlag(appInstance app.Application) []*cobra.Command {
- var (
- clientSecret string
- clientID string
- rpcHost string
- apiHost string
- rpcPort int
- apiPort int
- apiScheme string
- joinToken string
- rpcUrl string
- apiUrl string
- )
-
- clientCmd = &cobra.Command{
- Use: "client [-s client secret] [-i client id] [-t api host] [-r rpc host] [-c rpc port] [-p api port]",
- Short: "run managed frpc",
- Run: func(cmd *cobra.Command, args []string) {
- run := func() {
- patchConfig(appInstance,
- apiHost, rpcHost,
- clientID, clientSecret,
- apiScheme, rpcPort, apiPort,
- apiUrl, rpcUrl)
- runClient(appInstance)
- }
- if srv, err := utils.CreateSystemService(args, run); err != nil {
- run()
- } else {
- srv.Run()
- }
- },
- }
-
- joinCmd := &cobra.Command{
- Use: "join [-j join token] [-r rpc host] [-p api port] [-e api scheme]",
- Short: "join to master with token, save param to config",
- Run: func(cmd *cobra.Command, args []string) {
- pullRunConfig(appInstance,
- joinToken, rpcHost, apiScheme, rpcPort, apiPort, clientID, apiHost, apiUrl, rpcUrl)
- },
- }
-
- clientCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "client secret")
- clientCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id")
-
- clientCmd.Flags().StringVar(&rpcUrl, "rpc-url", "", "rpc url, master rpc url, scheme can be grpc/ws/wss://hostname:port")
-
- clientCmd.Flags().StringVar(&apiUrl, "api-url", "", "api url, master api url, scheme can be http/https://hostname:port")
-
- // deprecated start
- clientCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "deprecated, use --rpc-url instead, rpc host, canbe ip or domain")
- clientCmd.Flags().StringVarP(&apiHost, "api", "t", "", "deprecated, use --api-url instead, api host, canbe ip or domain")
- clientCmd.Flags().IntVarP(&rpcPort, "rpc-port", "c", 0, "deprecated, use --rpc-url instead, rpc port, master rpc port, scheme is grpc")
- clientCmd.Flags().IntVarP(&apiPort, "api-port", "p", 0, "deprecated, use --api-url instead, api port, master api port, scheme is http/https")
- clientCmd.Flags().StringVarP(&apiScheme, "api-scheme", "e", "", "deprecated, use --api-url instead, api scheme, master api scheme, scheme is http/https")
- joinCmd.Flags().IntVarP(&rpcPort, "rpc-port", "c", 0, "deprecated, use --rpc-url instead, rpc port, master rpc port, scheme is grpc")
- joinCmd.Flags().IntVarP(&apiPort, "api-port", "p", 0, "deprecated, use --api-url instead, api port, master api port, scheme is http/https")
- joinCmd.Flags().StringVarP(&rpcHost, "rpc", "r", "", "deprecated, use --rpc-url instead, rpc host, canbe ip or domain")
- joinCmd.Flags().StringVarP(&apiHost, "api", "t", "", "deprecated, use --api-url instead, api host, canbe ip or domain")
- joinCmd.Flags().StringVarP(&apiScheme, "api-scheme", "e", "", "deprecated, use --api-url instead, api scheme, master api scheme, scheme is http/https")
- // deprecated end
-
- joinCmd.Flags().StringVarP(&joinToken, "join-token", "j", "", "your token from master")
- joinCmd.Flags().StringVarP(&clientID, "id", "i", "", "client id")
- joinCmd.Flags().StringVar(&rpcUrl, "rpc-url", "", "rpc url, master rpc url, scheme can be grpc/ws/wss://hostname:port")
- joinCmd.Flags().StringVar(&apiUrl, "api-url", "", "api url, master api url, scheme can be http/https://hostname:port")
-
- return []*cobra.Command{clientCmd, joinCmd}
-}
-
-func initCmdWithoutFlag(appInstance app.Application) []*cobra.Command {
-
- installServiceCmd := &cobra.Command{
- Use: "install",
- Short: "install frp-panel as service",
- DisableFlagParsing: true,
- DisableFlagsInUseLine: true,
- Run: func(cmd *cobra.Command, args []string) {
- utils.ControlSystemService(args, "install", func() {})
- },
- }
-
- uninstallServiceCmd := &cobra.Command{
- Use: "uninstall",
- Short: "uninstall frp-panel service",
- DisableFlagParsing: true,
- DisableFlagsInUseLine: true,
- Run: func(cmd *cobra.Command, args []string) {
- utils.ControlSystemService(args, "uninstall", func() {})
- },
- }
-
- startServiceCmd := &cobra.Command{
- Use: "start",
- Short: "start frp-panel service",
- DisableFlagParsing: true,
- DisableFlagsInUseLine: true,
- Run: func(cmd *cobra.Command, args []string) {
- utils.ControlSystemService(args, "start", func() {})
- },
- }
-
- stopServiceCmd := &cobra.Command{
- Use: "stop",
- Short: "stop frp-panel service",
- DisableFlagParsing: true,
- DisableFlagsInUseLine: true,
- Run: func(cmd *cobra.Command, args []string) {
- utils.ControlSystemService(args, "stop", func() {})
- },
- }
-
- restartServiceCmd := &cobra.Command{
- Use: "restart",
- Short: "restart frp-panel service",
- DisableFlagParsing: true,
- DisableFlagsInUseLine: true,
- Run: func(cmd *cobra.Command, args []string) {
- utils.ControlSystemService(args, "restart", func() {})
- },
- }
-
- versionCmd := &cobra.Command{
- Use: "version",
- Short: "Print the version info of frp-panel",
- Long: `All software has versions. This is frp-panel's`,
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println(conf.GetVersion().String())
- },
- }
- return []*cobra.Command{
- installServiceCmd,
- uninstallServiceCmd,
- startServiceCmd,
- stopServiceCmd,
- restartServiceCmd,
- versionCmd,
- }
-}
-
-func patchConfig(appInstance app.Application, apiHost, rpcHost, clientID, clientSecret, apiScheme string, rpcPort, apiPort int, apiUrl, rpcUrl string) {
- c := context.Background()
- tmpCfg := appInstance.GetConfig()
- if len(rpcHost) != 0 {
- tmpCfg.Master.RPCHost = rpcHost
- tmpCfg.Master.APIHost = rpcHost
- }
-
- if len(apiHost) != 0 {
- tmpCfg.Master.APIHost = apiHost
- }
-
- if rpcPort != 0 {
- tmpCfg.Master.RPCPort = rpcPort
- }
- if apiPort != 0 {
- tmpCfg.Master.APIPort = apiPort
- }
- if len(apiScheme) != 0 {
- tmpCfg.Master.APIScheme = apiScheme
- }
- if len(clientID) != 0 {
- tmpCfg.Client.ID = clientID
- }
- if len(clientSecret) != 0 {
- tmpCfg.Client.Secret = clientSecret
- }
-
- if len(apiUrl) != 0 {
- tmpCfg.Client.APIUrl = apiUrl
- }
- if len(rpcUrl) != 0 {
- tmpCfg.Client.RPCUrl = rpcUrl
- }
-
- if rpcPort != 0 || apiPort != 0 || len(apiScheme) != 0 || len(rpcHost) != 0 || len(apiHost) != 0 {
- logger.Logger(c).Warnf("deprecatedenv configs !!! rpc host: %s, rpc port: %d, api host: %s, api port: %d, api scheme: %s",
- tmpCfg.Master.RPCHost, tmpCfg.Master.RPCPort,
- tmpCfg.Master.APIHost, tmpCfg.Master.APIPort,
- tmpCfg.Master.APIScheme)
- }
- logger.Logger(c).Infof("env config, api url: %s, rpc url: %s", tmpCfg.Client.APIUrl, tmpCfg.Client.RPCUrl)
-}
-
-func setMasterCommandIfNonePresent() {
- cmd, _, err := rootCmd.Find(os.Args[1:])
- if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
- args := append([]string{"master"}, os.Args[1:]...)
- rootCmd.SetArgs(args)
- }
-}
-
-func pullRunConfig(appInstance app.Application, joinToken, rpcHost, apiScheme string, rpcPort, apiPort int, clientID, apiHost string, apiUrl, rpcUrl string) {
- c := context.Background()
- if err := checkPullParams(joinToken, apiHost, apiScheme, apiPort, apiUrl); err != nil {
- logger.Logger(c).Errorf("check pull params failed: %s", err.Error())
- return
- }
-
- if err := utils.EnsureDirectoryExists(defs.SysEnvPath); err != nil {
- logger.Logger(c).Errorf("ensure directory failed: %s", err.Error())
- return
- }
-
- if len(clientID) == 0 {
- clientID = utils.GetHostnameWithIP()
- }
-
- clientID = utils.MakeClientIDPermited(clientID)
- patchConfig(appInstance, apiHost, rpcHost, "", "", apiScheme, rpcPort, apiPort, apiUrl, rpcUrl)
-
- initResp, err := rpc.InitClient(appInstance, clientID, joinToken)
- if err != nil {
- logger.Logger(c).Errorf("init client failed: %s", err.Error())
- return
- }
- if initResp == nil {
- logger.Logger(c).Errorf("init resp is nil")
- return
- }
- if initResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
- logger.Logger(c).Errorf("init client failed with status: %s", initResp.GetStatus().GetMessage())
- return
- }
-
- clientID = initResp.GetClientId()
- clientResp, err := rpc.GetClient(appInstance, clientID, joinToken)
- if err != nil {
- logger.Logger(c).Errorf("get client failed: %s", err.Error())
- return
- }
- if clientResp == nil {
- logger.Logger(c).Errorf("client resp is nil")
- return
- }
- if clientResp.GetStatus().GetCode() != pb.RespCode_RESP_CODE_SUCCESS {
- logger.Logger(c).Errorf("client resp code is not success: %s", clientResp.GetStatus().GetMessage())
- return
- }
-
- client := clientResp.GetClient()
- if client == nil {
- logger.Logger(c).Errorf("client is nil")
- return
- }
-
- envMap, err := godotenv.Read(defs.SysEnvPath)
- if err != nil {
- envMap = make(map[string]string)
- logger.Logger(c).Warnf("read env file failed, try to create: %s", err.Error())
- }
-
- envMap[defs.EnvClientID] = clientID
- envMap[defs.EnvClientSecret] = client.GetSecret()
- envMap[defs.EnvClientAPIUrl] = apiUrl
- envMap[defs.EnvClientRPCUrl] = rpcUrl
-
- if err = godotenv.Write(envMap, defs.SysEnvPath); err != nil {
- logger.Logger(c).Errorf("write env file failed: %s", err.Error())
- return
- }
- logger.Logger(c).Infof("config saved to env file: %s, you can use `frp-panel client` without args to run client,\n\nconfig is: [%v]", defs.SysEnvPath, envMap)
-}
-
-func checkPullParams(joinToken, apiHost, apiScheme string, apiPort int, apiUrl string) error {
- if len(joinToken) == 0 {
- return errors.New("join token is empty")
- }
- if len(apiUrl) == 0 {
- if len(apiHost) == 0 {
- return errors.New("api host is empty")
- }
- if len(apiScheme) == 0 {
- return errors.New("api scheme is empty")
- }
- }
-
- if apiPort == 0 {
- return errors.New("api port is empty")
- }
- return nil
-}
diff --git a/cmd/frppc/main.go b/cmd/frppc/main.go
index 0ee52e2..94b1abf 100644
--- a/cmd/frppc/main.go
+++ b/cmd/frppc/main.go
@@ -1,13 +1,8 @@
package main
import (
- "sync"
-
- "github.com/VaalaCat/frp-panel/biz/common"
- "github.com/VaalaCat/frp-panel/biz/master/shell"
+ "github.com/VaalaCat/frp-panel/cmd/frpp/shared"
"github.com/VaalaCat/frp-panel/conf"
- "github.com/VaalaCat/frp-panel/services/app"
- "github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/fatedier/golib/crypto"
"github.com/spf13/cobra"
@@ -15,20 +10,20 @@ import (
func main() {
crypto.DefaultSalt = "frp"
-
logger.InitLogger()
cobra.MousetrapHelpText = ""
- cfg := conf.NewConfig()
- appInstance := app.NewApp()
- appInstance.SetConfig(cfg)
- appInstance.SetClientsManager(rpc.NewClientsManager())
- appInstance.SetStreamLogHookMgr(&common.HookMgr{})
- appInstance.SetShellPTYMgr(shell.NewPTYMgr())
- appInstance.SetClientRecvMap(&sync.Map{})
+ rootCmd := shared.NewRootCmd(
+ shared.NewClientCmd(conf.NewConfig()),
+ shared.NewJoinCmd(),
+ shared.NewInstallServiceCmd(),
+ shared.NewUninstallServiceCmd(),
+ shared.NewStartServiceCmd(),
+ shared.NewStopServiceCmd(),
+ shared.NewRestartServiceCmd(),
+ shared.NewVersionCmd(),
+ )
- initCommand(appInstance)
-
- setMasterCommandIfNonePresent()
+ shared.SetClientCommandIfNonePresent(rootCmd)
rootCmd.Execute()
}
diff --git a/common/context.go b/common/context.go
index 554b0f7..ab3e847 100644
--- a/common/context.go
+++ b/common/context.go
@@ -2,6 +2,7 @@ package common
import (
"context"
+ "encoding/json"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/models"
@@ -20,3 +21,27 @@ func GetUserInfo(c context.Context) models.UserInfo {
return u
}
+
+func GetTokenPermission(c context.Context) ([]defs.APIPermission, error) {
+ val := c.Value(defs.TokenPayloadKey_Permissions)
+ if val == nil {
+ return nil, nil
+ }
+
+ raw, err := json.Marshal(val)
+ if err != nil {
+ return nil, err
+ }
+
+ perms := []defs.APIPermission{}
+ err = json.Unmarshal(raw, &perms)
+ if err != nil {
+ return nil, err
+ }
+
+ return perms, nil
+}
+
+func GetTokenString(c context.Context) string {
+ return c.Value(defs.TokenKey).(string)
+}
diff --git a/common/request.go b/common/request.go
index 24aeae0..ec5dd31 100644
--- a/common/request.go
+++ b/common/request.go
@@ -24,7 +24,7 @@ type ReqType interface {
pb.StartFRPCRequest | pb.StopFRPCRequest | pb.StartFRPSRequest | pb.StopFRPSRequest |
pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest |
pb.CreateProxyConfigRequest | pb.ListProxyConfigsRequest | pb.UpdateProxyConfigRequest |
- pb.DeleteProxyConfigRequest | pb.GetProxyConfigRequest
+ pb.DeleteProxyConfigRequest | pb.GetProxyConfigRequest | pb.SignTokenRequest
}
func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) {
diff --git a/common/response.go b/common/response.go
index 9e2588c..b01bdc2 100644
--- a/common/response.go
+++ b/common/response.go
@@ -25,7 +25,7 @@ type RespType interface {
pb.StartFRPCResponse | pb.StopFRPCResponse | pb.StartFRPSResponse | pb.StopFRPSResponse |
pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse |
pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse |
- pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse
+ pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse | pb.SignTokenResponse
}
func OKResp[T RespType](c *gin.Context, origin *T) {
diff --git a/conf/helper.go b/conf/helper.go
index 892ce19..44b630a 100644
--- a/conf/helper.go
+++ b/conf/helper.go
@@ -58,19 +58,38 @@ func FRPsAuthOption(cfg Config, isDefault bool) v1.HTTPPluginOptions {
}
}
-func GetCommonJWT(cfg Config, uid string) string {
- token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg),
+func GetJWTWithPayload(cfg Config, uid int, payload map[string]interface{}) (string, error) {
+ payload[defs.UserIDKey] = uid
+ return utils.GetJwtTokenFromMap(JWTSecret(cfg),
time.Now().Unix(),
int64(cfg.App.CookieAge),
- map[string]string{defs.UserIDKey: uid})
+ payload)
+}
+
+func GetJWTWithAllPermission(cfg Config, uid int) string {
+ token, _ := GetJWTWithPayload(cfg, uid, map[string]interface{}{
+ defs.TokenPayloadKey_Permissions: AllPermission(),
+ })
return token
}
-func GetCommonJWTWithExpireTime(cfg Config, uid string, expSec int) string {
+func AllPermission() []defs.APIPermission {
+ return []defs.APIPermission{{
+ Method: "*",
+ Path: "*",
+ }}
+}
+
+func GetCommonJWT(cfg Config, uid int) string {
+ token, _ := GetJWTWithPayload(cfg, uid, map[string]interface{}{})
+ return token
+}
+
+func GetCommonJWTWithExpireTime(cfg Config, uid int64, expSec int) string {
token, _ := utils.GetJwtTokenFromMap(JWTSecret(cfg),
time.Now().Unix(),
int64(expSec),
- map[string]string{defs.UserIDKey: uid})
+ map[string]interface{}{defs.UserIDKey: uid})
return token
}
diff --git a/defs/const.go b/defs/const.go
index a55772b..bc97368 100644
--- a/defs/const.go
+++ b/defs/const.go
@@ -54,6 +54,14 @@ const (
CliTypeClient = "client"
)
+type AppRole string
+
+const (
+ AppRole_Client AppRole = CliTypeClient
+ AppRole_Server AppRole = CliTypeServer
+ AppRole_Master AppRole = "master"
+)
+
const (
DefaultServerID = "default"
DefaultAdminUserID = 1
@@ -77,3 +85,27 @@ const (
EnvClientAPIUrl = "CLIENT_API_URL"
EnvClientRPCUrl = "CLIENT_RPC_URL"
)
+
+const (
+ DBRoleDefault = "default"
+ DBRoleRam = "ram"
+)
+
+const (
+ DBTypeSQLite3 = "sqlite3"
+ DBTypeMysql = "mysql"
+ DBTypePostgres = "postgres"
+)
+
+const (
+ UserRole_Admin = "admin"
+ UserRole_Normal = "normal"
+)
+
+type TokenStatus string
+
+const (
+ TokenStatusActive TokenStatus = "active"
+ TokenStatusInactive TokenStatus = "inactive"
+ TokenStatusRevoked TokenStatus = "revoked"
+)
diff --git a/defs/error.go b/defs/error.go
new file mode 100644
index 0000000..d2ca70b
--- /dev/null
+++ b/defs/error.go
@@ -0,0 +1 @@
+package defs
diff --git a/defs/rbac_consts.go b/defs/rbac_consts.go
new file mode 100644
index 0000000..0c35f72
--- /dev/null
+++ b/defs/rbac_consts.go
@@ -0,0 +1,43 @@
+package defs
+
+type RBACAction string
+
+const (
+ RBACActionCreate RBACAction = "create"
+ RBACActionRead RBACAction = "read"
+ RBACActionUpdate RBACAction = "update"
+ RBACActionDelete RBACAction = "delete"
+)
+
+type RBACObj string
+
+const (
+ RBACObjServer RBACObj = "server"
+ RBACObjClient RBACObj = "client"
+ RBACObjUser RBACObj = "user"
+ RBACObjGroup RBACObj = "group"
+ RBACObjAPI RBACObj = "api"
+)
+
+type RBACSubject string
+
+const (
+ RBACSubjectUser RBACSubject = "user"
+ RBACSubjectGroup RBACSubject = "group"
+ RBACSubjectToken RBACSubject = "token"
+)
+
+type RBACDomain string
+
+const (
+ RBACDomainTenant RBACDomain = "tenant"
+)
+
+type APIPermission struct {
+ Method string `json:"method"`
+ Path string `json:"path"`
+}
+
+const (
+ TokenPayloadKey_Permissions = "permissions"
+)
diff --git a/docs/zh/deploy-client.md b/docs/zh/deploy-client.md
index 31524d9..4b7d339 100644
--- a/docs/zh/deploy-client.md
+++ b/docs/zh/deploy-client.md
@@ -19,19 +19,19 @@ Client 推荐使用 docker 部署,直接部署在客户机中,可以通过
点击对应客户端的 `ID (点击查看安装命令)` 一列,弹出不同系统的安装命令,粘贴到对应终端即可安装,这里以 Linux 为例
```
-curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
+curl -fSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
如果你在国内,可以增加github加速到安装脚本前
```
-curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
+curl -fSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- client -s abc -i user.s.client1 -a 123123 -r frpp-rpc.example.com -c 9001 -p 9000 -e http
```
注意,如果你使用 反向代理 TLS,需要修改这行命令类似如下:
```bash
-curl -sSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
+curl -fSL https://ghfast.top/https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s -- frp-panel client -s abc -i user.s.client1 -a 123123 -t frpp.example.com -r frpp-rpc.example.com -c 443 -p 443 -e https
```
## Docker Compose 部署
diff --git a/go.mod b/go.mod
index 1a7c049..eba84e1 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,8 @@ toolchain go1.24.1
require (
github.com/UserExistsError/conpty v0.1.4
+ github.com/casbin/casbin/v2 v2.105.0
+ github.com/casbin/gorm-adapter/v3 v3.32.0
github.com/coocood/freecache v1.2.4
github.com/creack/pty v1.1.24
github.com/fatedier/frp v0.62.0
@@ -37,9 +39,9 @@ require (
golang.org/x/net v0.39.0
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.5
- gorm.io/driver/mysql v1.5.2
- gorm.io/driver/postgres v1.5.4
- gorm.io/gorm v1.25.5
+ gorm.io/driver/mysql v1.5.7
+ gorm.io/driver/postgres v1.5.9
+ gorm.io/gorm v1.25.12
k8s.io/apimachinery v0.28.8
)
@@ -49,7 +51,9 @@ require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
+ github.com/casbin/govaluate v1.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
@@ -68,6 +72,9 @@ require (
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
+ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
+ github.com/golang-sql/sqlexp v0.1.0 // indirect
+ github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
@@ -77,7 +84,8 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
- github.com/jackc/pgx/v5 v5.4.3 // indirect
+ github.com/jackc/pgx/v5 v5.5.5 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
@@ -88,6 +96,7 @@ require (
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/microsoft/go-mssqldb v1.6.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
@@ -110,7 +119,6 @@ require (
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
- github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
@@ -145,6 +153,8 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+ gorm.io/driver/sqlserver v1.5.3 // indirect
+ gorm.io/plugin/dbresolver v1.5.3 // indirect
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.22.5 // indirect
diff --git a/go.sum b/go.sum
index 5931b05..904a663 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,24 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
@@ -13,9 +31,17 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
+github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
+github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
+github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus=
+github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI=
+github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
+github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -38,6 +64,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
+github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
@@ -90,10 +118,20 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
+github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
@@ -117,10 +155,13 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -128,6 +169,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/iamacarpet/go-winpty v1.0.4 h1:r42xaLLRZcUqjX6vHZeHos2haACfWkooOJTnFdogBfI=
@@ -142,10 +185,18 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
-github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackpal/gateway v1.0.16 h1:mTBRuHSW8qviVqX7kXnxKevqlfS/OA01ys6k6fxSX7w=
github.com/jackpal/gateway v1.0.16/go.mod h1:IOn1OUbso/cGYmnCBZbCEqhNCLSz0xxdtIpUpri5/nA=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -170,20 +221,28 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc=
+github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
+github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
@@ -202,6 +261,8 @@ github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouAN
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -297,6 +358,7 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lL
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -318,10 +380,14 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
@@ -331,6 +397,7 @@ golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudw
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
@@ -341,10 +408,14 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
@@ -356,6 +427,7 @@ golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
@@ -369,7 +441,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -385,6 +460,7 @@ golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
@@ -393,10 +469,13 @@ golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
@@ -405,14 +484,18 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
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.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
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=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
@@ -442,6 +525,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -449,13 +533,18 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
-gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
-gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
-gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
-gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
-gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
-gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
+gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
+gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
+gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
+gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/idl/api_auth.proto b/idl/api_auth.proto
index f78a8c2..1651a31 100644
--- a/idl/api_auth.proto
+++ b/idl/api_auth.proto
@@ -22,4 +22,19 @@ message RegisterRequest {
message RegisterResponse {
optional common.Status status = 1;
+}
+
+message APIPermission {
+ optional string method = 1;
+ optional string path = 2;
+}
+
+message SignTokenRequest {
+ optional int64 expires_in = 1;
+ repeated APIPermission permissions = 2;
+}
+
+message SignTokenResponse {
+ optional common.Status status = 1;
+ optional string token = 2;
}
\ No newline at end of file
diff --git a/idl/api_client.proto b/idl/api_client.proto
index 96c67be..24d2769 100644
--- a/idl/api_client.proto
+++ b/idl/api_client.proto
@@ -7,6 +7,7 @@ option go_package="../pb";
message InitClientRequest {
optional string client_id = 1;
+ optional bool ephemeral = 2;
}
message InitClientResponse {
@@ -145,4 +146,4 @@ message GetProxyConfigResponse {
optional common.Status status = 1;
optional common.ProxyConfig proxy_config = 2;
optional common.ProxyWorkingStatus working_status = 3;
-}
\ No newline at end of file
+}
diff --git a/idl/common.proto b/idl/common.proto
index 17a352f..9aa88fb 100644
--- a/idl/common.proto
+++ b/idl/common.proto
@@ -43,6 +43,8 @@ message Client {
repeated string client_ids = 8; // some client can connected to more than one server, make a shadow client to handle this
optional string origin_client_id = 9;
optional string frps_url = 10; // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
+ optional bool ephemeral = 11; // 是否临时节点
+ optional int64 last_seen_at = 12; // 最后一次心跳时间戳
}
message Server {
@@ -93,4 +95,4 @@ message ProxyWorkingStatus {
optional string status = 3;
optional string err = 4;
optional string remote_addr = 5;
-}
\ No newline at end of file
+}
diff --git a/middleware/auth.go b/middleware/auth.go
index d557871..a4c3d91 100644
--- a/middleware/auth.go
+++ b/middleware/auth.go
@@ -6,9 +6,9 @@ import (
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
- "github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/gin-gonic/gin"
+ "github.com/spf13/cast"
)
func AuthCtx(appInstance app.Application) func(*gin.Context) {
@@ -22,9 +22,9 @@ func AuthCtx(appInstance app.Application) func(*gin.Context) {
logger.Logger(c).Info("finish auth user middleware")
}()
- userID, ok := utils.GetValue[int](c, defs.UserIDKey)
- if !ok {
- logger.Logger(c).Errorf("invalid user id")
+ userID, err := cast.ToIntE(c.Value(defs.UserIDKey))
+ if err != nil {
+ logger.Logger(c).WithError(err).Errorf("invalid user id: %v", c.Value(defs.UserIDKey))
common.ErrUnAuthorized(c, "token invalid")
c.Abort()
return
@@ -61,7 +61,7 @@ func AuthCtx(appInstance app.Application) func(*gin.Context) {
func AuthAdmin(c *gin.Context) {
u := common.GetUserInfo(c)
- if u != nil && u.GetRole() == models.ROLE_ADMIN {
+ if u != nil && u.GetRole() == defs.UserRole_Admin {
common.ErrUnAuthorized(c, "permission denied")
c.Abort()
return
diff --git a/middleware/jwt.go b/middleware/jwt.go
index ba31476..2ac1402 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -13,6 +13,7 @@ import (
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
+ "github.com/spf13/cast"
)
// JWTMAuth check if authed and set uid to context
@@ -30,14 +31,14 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) {
c.Set(k, v)
}
logger.Logger(c).Infof("query auth success")
- if err = resignAndPatchCtxJWT(c, appInstance, t, tokenStr); err != nil {
+ if err = resignAndPatchCtxJWT(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t, tokenStr); err != nil {
logger.Logger(c).WithError(err).Errorf("resign jwt error")
common.ErrUnAuthorized(c, "resign jwt error")
c.Abort()
return
}
c.Next()
- SetToken(c, appInstance, utils.ToStr(t[defs.UserIDKey]))
+ SetToken(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t)
return
}
logger.Logger(c).Infof("query auth failed")
@@ -50,7 +51,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) {
c.Set(k, v)
}
logger.Logger(c).Infof("cookie auth success")
- if err = resignAndPatchCtxJWT(c, appInstance, t, cookieToken); err != nil {
+ if err = resignAndPatchCtxJWT(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t, cookieToken); err != nil {
logger.Logger(c).WithError(err).Errorf("resign jwt error")
common.ErrUnAuthorized(c, "resign jwt error")
c.Abort()
@@ -80,7 +81,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) {
c.Set(k, v)
}
logger.Logger(c).Infof("header auth success")
- if err = resignAndPatchCtxJWT(c, appInstance, t, tokenStr); err != nil {
+ if err = resignAndPatchCtxJWT(c, appInstance, cast.ToInt(t[defs.UserIDKey]), t, tokenStr); err != nil {
logger.Logger(c).WithError(err).Errorf("resign jwt error")
common.ErrUnAuthorized(c, "resign jwt error")
c.Abort()
@@ -94,7 +95,7 @@ func JWTAuth(appInstance app.Application) func(c *gin.Context) {
}
}
-func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, t jwt.MapClaims, tokenStr string) error {
+func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, userID int, t jwt.MapClaims, tokenStr string) error {
tokenExpire, _ := t.GetExpirationTime()
now := time.Now().Add(time.Duration(appInstance.GetConfig().App.CookieAge/2) * time.Second)
if now.Before(tokenExpire.Time) {
@@ -103,47 +104,32 @@ func resignAndPatchCtxJWT(c *gin.Context, appInstance app.Application, t jwt.Map
return nil
}
- token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(appInstance.GetConfig()),
- time.Now().Unix(),
- int64(appInstance.GetConfig().App.CookieAge),
- map[string]string{defs.UserIDKey: utils.ToStr(t[defs.UserIDKey])})
+ tokenStr, err := SetToken(c, appInstance, userID, t)
if err != nil {
- c.Set(defs.TokenKey, tokenStr)
logger.Logger(c).WithError(err).Errorf("resign jwt error")
return err
}
+ PushTokenStr(c, appInstance, tokenStr)
+
logger.Logger(c).Infof("jwt going to expire, resigning token")
- c.Header(defs.SetAuthorizationKey, token)
- c.SetCookie(appInstance.GetConfig().App.CookieName,
- token,
- appInstance.GetConfig().App.CookieAge,
- appInstance.GetConfig().App.CookiePath,
- appInstance.GetConfig().App.CookieDomain,
- appInstance.GetConfig().App.CookieSecure,
- appInstance.GetConfig().App.CookieHTTPOnly)
- c.Set(defs.TokenKey, token)
return nil
}
-func SetToken(c *gin.Context, appInstance app.Application, uid string) (string, error) {
- logger.Logger(c).Infof("set token for uid:[%s]", uid)
- token, err := utils.GetJwtTokenFromMap(conf.JWTSecret(appInstance.GetConfig()),
- time.Now().Unix(),
- int64(appInstance.GetConfig().App.CookieAge),
- map[string]string{defs.UserIDKey: uid})
+// SetToken 设置新token并写入ctx
+func SetToken(c *gin.Context, appInstance app.Application, userID int, payload jwt.MapClaims) (string, error) {
+ logger.Logger(c).Debugf("set token for userID:[%d]", userID)
+ if payload == nil {
+ payload = make(map[string]interface{})
+ }
+
+ payload[defs.UserIDKey] = userID
+
+ token, err := conf.GetJWTWithPayload(appInstance.GetConfig(), userID, payload)
if err != nil {
return "", err
}
- c.SetCookie(appInstance.GetConfig().App.CookieName,
- token,
- appInstance.GetConfig().App.CookieAge,
- appInstance.GetConfig().App.CookiePath,
- appInstance.GetConfig().App.CookieDomain,
- appInstance.GetConfig().App.CookieSecure,
- appInstance.GetConfig().App.CookieHTTPOnly)
c.Set(defs.TokenKey, token)
- c.Header(defs.SetAuthorizationKey, token)
return token, nil
}
diff --git a/middleware/rbac.go b/middleware/rbac.go
new file mode 100644
index 0000000..22f1048
--- /dev/null
+++ b/middleware/rbac.go
@@ -0,0 +1,82 @@
+package middleware
+
+import (
+ "regexp"
+
+ "github.com/VaalaCat/frp-panel/common"
+ "github.com/VaalaCat/frp-panel/pb"
+ "github.com/VaalaCat/frp-panel/services/app"
+ "github.com/VaalaCat/frp-panel/utils"
+ "github.com/VaalaCat/frp-panel/utils/logger"
+ "github.com/gin-gonic/gin"
+)
+
+func RBAC(appInstance app.Application) func(*gin.Context) {
+ return func(c *gin.Context) {
+ // appCtx := app.NewContext(c, appInstance)
+ perms, err := common.GetTokenPermission(c)
+ userInfo := common.GetUserInfo(c)
+ token := common.GetTokenString(c)
+ path := c.Request.URL.Path
+ method := c.Request.Method
+
+ if err != nil {
+ logger.Logger(c).WithError(err).Errorf("get token permission error, token: [%s], userInfo:[%s]", token, utils.MarshalForJson(userInfo.GetSafeUserInfo()))
+ common.ErrResp(c, &pb.CommonResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: err.Error()}}, err.Error())
+ c.Abort()
+ return
+ }
+
+ if len(perms) == 0 {
+ logger.Logger(c).WithError(err).Errorf("user has no permission, token: [%s], userInfo:[%s]", token, utils.MarshalForJson(userInfo.GetSafeUserInfo()))
+ common.ErrResp(c, &pb.CommonResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "user has no permission"}}, "user has no permission")
+ c.Abort()
+ return
+ }
+
+ for _, perm := range perms {
+ if ruleMatched(ruleMatchParam{
+ RuleMethod: perm.Method,
+ RulePath: perm.Path,
+ RequestPath: path,
+ RequestMethod: method,
+ }) {
+ logger.Logger(c).Infof("user has api permission, continue")
+ c.Next()
+ return
+ }
+ }
+
+ logger.Logger(c).Errorf("user has no permission, perms: %s, userInfo: [%s], ", utils.MarshalForJson(perms), utils.MarshalForJson(userInfo.GetSafeUserInfo()))
+ common.ErrResp(c, &pb.CommonResponse{Status: &pb.Status{Code: pb.RespCode_RESP_CODE_INVALID, Message: "user has no permission"}}, "user has no permission")
+ c.Abort()
+ return
+ }
+}
+
+type ruleMatchParam struct {
+ RuleMethod string
+ RulePath string
+ RequestPath string
+ RequestMethod string
+}
+
+func ruleMatched(param ruleMatchParam) bool {
+ methodMatch := false
+ if param.RuleMethod == param.RequestMethod || param.RuleMethod == "*" {
+ methodMatch = true
+ }
+
+ if !methodMatch {
+ return false
+ }
+
+ pathMatch := false
+ if param.RulePath == "*" {
+ pathMatch = true
+ } else {
+ pathMatch = regexp.MustCompile(param.RulePath).MatchString(param.RequestPath)
+ }
+
+ return pathMatch
+}
diff --git a/models/cert.go b/models/cert.go
index ee05f90..60704bb 100644
--- a/models/cert.go
+++ b/models/cert.go
@@ -4,7 +4,7 @@ import "gorm.io/gorm"
type Cert struct {
gorm.Model
- Name string `gorm:"uniqueIndex"`
+ Name string `gorm:"type:varchar(255);uniqueIndex"`
CertFile []byte
KeyFile []byte
CaFile []byte
diff --git a/models/client.go b/models/client.go
index 26120f0..e93d7e8 100644
--- a/models/client.go
+++ b/models/client.go
@@ -25,10 +25,13 @@ type ClientEntity struct {
Comment string `json:"comment"`
IsShadow bool `json:"is_shadow" gorm:"index"`
OriginClientID string `json:"origin_client_id" gorm:"index"`
- FRPsUrl string `json:"frps_url" gorm:"index"`
- CreatedAt time.Time
- UpdatedAt time.Time
- DeletedAt gorm.DeletedAt `gorm:"index"`
+ FrpsUrl string `json:"frps_url" gorm:"index"`
+ Ephemeral bool `json:"ephemeral" gorm:"index"`
+
+ LastSeenAt *time.Time `json:"last_seen_at" gorm:"index"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (*Client) TableName() string {
diff --git a/models/db.go b/models/db.go
index c1ec611..e52b7a8 100644
--- a/models/db.go
+++ b/models/db.go
@@ -3,79 +3,80 @@ package models
import (
"context"
+ "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/utils/logger"
"gorm.io/gorm"
)
-type DBManager interface {
- GetDB(dbType string) *gorm.DB
- GetDefaultDB() *gorm.DB
- SetDB(dbType string, db *gorm.DB)
- RemoveDB(dbType string)
- SetDebug(bool)
- Init()
-}
-
type dbManagerImpl struct {
- DBs map[string]*gorm.DB // key: db type
+ DBs map[string]map[string]*gorm.DB // map[db type]map[db role]*gorm.DB
defaultDBType string
debug bool
}
func (dbm *dbManagerImpl) Init() {
- for _, db := range dbm.DBs {
- if err := db.AutoMigrate(&Client{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Client{}).TableName())
- }
- if err := db.AutoMigrate(&User{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&User{}).TableName())
- }
- if err := db.AutoMigrate(&Server{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Server{}).TableName())
- }
- if err := db.AutoMigrate(&Cert{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Cert{}).TableName())
- }
- if err := db.AutoMigrate(&ProxyStats{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyStats{}).TableName())
- }
- if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&HistoryProxyStats{}).TableName())
- }
- if err := db.AutoMigrate(&ProxyConfig{}); err != nil {
- logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName())
+ for _, dbGroup := range dbm.DBs {
+ for _, db := range dbGroup {
+ if err := db.AutoMigrate(&Client{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Client{}).TableName())
+ }
+ if err := db.AutoMigrate(&User{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&User{}).TableName())
+ }
+ if err := db.AutoMigrate(&Server{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Server{}).TableName())
+ }
+ if err := db.AutoMigrate(&Cert{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Cert{}).TableName())
+ }
+ if err := db.AutoMigrate(&ProxyStats{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyStats{}).TableName())
+ }
+ if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&HistoryProxyStats{}).TableName())
+ }
+ if err := db.AutoMigrate(&ProxyConfig{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName())
+ }
+ if err := db.AutoMigrate(&UserGroup{}); err != nil {
+ logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&UserGroup{}).TableName())
+ }
}
}
}
-func NewDBManager(dbs map[string]*gorm.DB, defaultDBType string) *dbManagerImpl {
- if dbs == nil {
- dbs = make(map[string]*gorm.DB)
- }
+func NewDBManager(defaultDBType string) *dbManagerImpl {
+ dbs := map[string]map[string]*gorm.DB{}
return &dbManagerImpl{
DBs: dbs,
defaultDBType: defaultDBType,
}
}
-func (dbm *dbManagerImpl) GetDB(dbType string) *gorm.DB {
- return dbm.DBs[dbType]
+func (dbm *dbManagerImpl) GetDB(dbType string, dbRole string) *gorm.DB {
+ return dbm.DBs[dbType][dbRole]
}
-func (dbm *dbManagerImpl) SetDB(dbType string, db *gorm.DB) {
- dbm.DBs[dbType] = db
+func (dbm *dbManagerImpl) SetDB(dbType string, dbRole string, db *gorm.DB) {
+ if dbm.DBs[dbType] == nil {
+ dbm.DBs[dbType] = map[string]*gorm.DB{}
+ }
+ dbm.DBs[dbType][dbRole] = db
}
-func (dbm *dbManagerImpl) RemoveDB(dbType string) {
- delete(dbm.DBs, dbType)
+func (dbm *dbManagerImpl) RemoveDB(dbType string, dbRole string) {
+ if dbm.DBs[dbType] == nil {
+ return
+ }
+ delete(dbm.DBs[dbType], dbRole)
}
func (dbm *dbManagerImpl) GetDefaultDB() *gorm.DB {
- db := dbm.DBs[dbm.defaultDBType]
+ dbGroup := dbm.DBs[dbm.defaultDBType]
if dbm.debug {
- return db.Debug()
+ return dbGroup[defs.DBRoleDefault].Debug()
}
- return db
+ return dbGroup[defs.DBRoleDefault]
}
func (dbm *dbManagerImpl) SetDebug(debug bool) {
diff --git a/models/enums.go b/models/enums.go
index dc29fc4..7c58b39 100644
--- a/models/enums.go
+++ b/models/enums.go
@@ -1,9 +1,6 @@
package models
-const (
- ROLE_ADMIN = "admin"
- ROLE_NORMAL = "normal"
-)
+
const (
STATUS_UNKNOWN = iota
diff --git a/models/server.go b/models/server.go
index 4dd755e..2e051bc 100644
--- a/models/server.go
+++ b/models/server.go
@@ -16,13 +16,13 @@ type Server struct {
type ServerEntity struct {
ServerID string `json:"client_id" gorm:"uniqueIndex;not null;primaryKey"`
- TenantID int `json:"tenant_id" gorm:"not null"`
+ TenantID int `json:"tenant_id" gorm:"not null,index"`
UserID int `json:"user_id" gorm:"not null"`
ServerIP string `json:"server_ip"`
ConfigContent []byte `json:"config_content"`
ConnectSecret string `json:"connect_secret" gorm:"not null"`
Comment string `json:"comment"`
- FRPsUrls GormArray[string] `json:"frps_urls"`
+ FrpsUrls GormArray[string] `json:"frps_urls"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
diff --git a/models/user.go b/models/user.go
index d5c1483..d97edf4 100644
--- a/models/user.go
+++ b/models/user.go
@@ -4,6 +4,7 @@ import (
"fmt"
"time"
+ "github.com/VaalaCat/frp-panel/defs"
"gorm.io/gorm"
)
@@ -30,9 +31,9 @@ var _ UserInfo = (*UserEntity)(nil)
type UserEntity struct {
UserID int `json:"user_id" gorm:"primaryKey"`
- UserName string `json:"user_name" gorm:"uniqueIndex;not null"`
+ UserName string `json:"user_name" gorm:"type:varchar(255);uniqueIndex;not null"`
Password string `json:"password"`
- Email string `json:"email" gorm:"uniqueIndex;not null"`
+ Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;not null"`
Status int `json:"status"`
Role string `json:"role"`
TenantID int `json:"tenant_id"`
@@ -40,6 +41,8 @@ type UserEntity struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
+
+ Groups []*UserGroup `json:"groups,omitempty" gorm:"many2many:user_group_memberships;"`
}
func (u *UserEntity) GetUserID() int {
@@ -99,7 +102,7 @@ func (u *UserEntity) Valid() bool {
}
func (u *UserEntity) IsAdmin() bool {
- return u.Role == ROLE_ADMIN
+ return u.Role == defs.UserRole_Admin
}
func (u *User) TableName() string {
diff --git a/models/user_group.go b/models/user_group.go
new file mode 100644
index 0000000..9f00364
--- /dev/null
+++ b/models/user_group.go
@@ -0,0 +1,18 @@
+package models
+
+import "time"
+
+type UserGroup struct {
+ GroupID string `json:"group_id" gorm:"primaryKey"`
+ GroupName string `json:"group_name" gorm:"type:varchar(255);uniqueIndex:idx_group_tenant_name;not null"`
+ TenantID int `json:"tenant_id" gorm:"uniqueIndex:idx_group_tenant_name;not null"`
+ Comment string `json:"comment"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+
+ Users []*User `json:"users,omitempty" gorm:"many2many:user_group_memberships;"`
+}
+
+func (u *UserGroup) TableName() string {
+ return "user_groups"
+}
diff --git a/pb/api_auth.pb.go b/pb/api_auth.pb.go
index 08d79a7..fcc993c 100644
--- a/pb/api_auth.pb.go
+++ b/pb/api_auth.pb.go
@@ -229,6 +229,162 @@ func (x *RegisterResponse) GetStatus() *Status {
return nil
}
+type APIPermission struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Method *string `protobuf:"bytes,1,opt,name=method,proto3,oneof" json:"method,omitempty"`
+ Path *string `protobuf:"bytes,2,opt,name=path,proto3,oneof" json:"path,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *APIPermission) Reset() {
+ *x = APIPermission{}
+ mi := &file_api_auth_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *APIPermission) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*APIPermission) ProtoMessage() {}
+
+func (x *APIPermission) ProtoReflect() protoreflect.Message {
+ mi := &file_api_auth_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use APIPermission.ProtoReflect.Descriptor instead.
+func (*APIPermission) Descriptor() ([]byte, []int) {
+ return file_api_auth_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *APIPermission) GetMethod() string {
+ if x != nil && x.Method != nil {
+ return *x.Method
+ }
+ return ""
+}
+
+func (x *APIPermission) GetPath() string {
+ if x != nil && x.Path != nil {
+ return *x.Path
+ }
+ return ""
+}
+
+type SignTokenRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ExpiresIn *int64 `protobuf:"varint,1,opt,name=expires_in,json=expiresIn,proto3,oneof" json:"expires_in,omitempty"`
+ Permissions []*APIPermission `protobuf:"bytes,2,rep,name=permissions,proto3" json:"permissions,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SignTokenRequest) Reset() {
+ *x = SignTokenRequest{}
+ mi := &file_api_auth_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SignTokenRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SignTokenRequest) ProtoMessage() {}
+
+func (x *SignTokenRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_auth_proto_msgTypes[5]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SignTokenRequest.ProtoReflect.Descriptor instead.
+func (*SignTokenRequest) Descriptor() ([]byte, []int) {
+ return file_api_auth_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *SignTokenRequest) GetExpiresIn() int64 {
+ if x != nil && x.ExpiresIn != nil {
+ return *x.ExpiresIn
+ }
+ return 0
+}
+
+func (x *SignTokenRequest) GetPermissions() []*APIPermission {
+ if x != nil {
+ return x.Permissions
+ }
+ return nil
+}
+
+type SignTokenResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
+ Token *string `protobuf:"bytes,2,opt,name=token,proto3,oneof" json:"token,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SignTokenResponse) Reset() {
+ *x = SignTokenResponse{}
+ mi := &file_api_auth_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SignTokenResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SignTokenResponse) ProtoMessage() {}
+
+func (x *SignTokenResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_auth_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SignTokenResponse.ProtoReflect.Descriptor instead.
+func (*SignTokenResponse) Descriptor() ([]byte, []int) {
+ return file_api_auth_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *SignTokenResponse) GetStatus() *Status {
+ if x != nil {
+ return x.Status
+ }
+ return nil
+}
+
+func (x *SignTokenResponse) GetToken() string {
+ if x != nil && x.Token != nil {
+ return *x.Token
+ }
+ return ""
+}
+
var File_api_auth_proto protoreflect.FileDescriptor
const file_api_auth_proto_rawDesc = "" +
@@ -253,7 +409,22 @@ const file_api_auth_proto_rawDesc = "" +
"\x06_email\"J\n" +
"\x10RegisterResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01B\t\n" +
- "\a_statusB\aZ\x05../pbb\x06proto3"
+ "\a_status\"Y\n" +
+ "\rAPIPermission\x12\x1b\n" +
+ "\x06method\x18\x01 \x01(\tH\x00R\x06method\x88\x01\x01\x12\x17\n" +
+ "\x04path\x18\x02 \x01(\tH\x01R\x04path\x88\x01\x01B\t\n" +
+ "\a_methodB\a\n" +
+ "\x05_path\"\x80\x01\n" +
+ "\x10SignTokenRequest\x12\"\n" +
+ "\n" +
+ "expires_in\x18\x01 \x01(\x03H\x00R\texpiresIn\x88\x01\x01\x129\n" +
+ "\vpermissions\x18\x02 \x03(\v2\x17.api_auth.APIPermissionR\vpermissionsB\r\n" +
+ "\v_expires_in\"p\n" +
+ "\x11SignTokenResponse\x12+\n" +
+ "\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x19\n" +
+ "\x05token\x18\x02 \x01(\tH\x01R\x05token\x88\x01\x01B\t\n" +
+ "\a_statusB\b\n" +
+ "\x06_tokenB\aZ\x05../pbb\x06proto3"
var (
file_api_auth_proto_rawDescOnce sync.Once
@@ -267,22 +438,27 @@ func file_api_auth_proto_rawDescGZIP() []byte {
return file_api_auth_proto_rawDescData
}
-var file_api_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_api_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_api_auth_proto_goTypes = []any{
- (*LoginRequest)(nil), // 0: api_auth.LoginRequest
- (*LoginResponse)(nil), // 1: api_auth.LoginResponse
- (*RegisterRequest)(nil), // 2: api_auth.RegisterRequest
- (*RegisterResponse)(nil), // 3: api_auth.RegisterResponse
- (*Status)(nil), // 4: common.Status
+ (*LoginRequest)(nil), // 0: api_auth.LoginRequest
+ (*LoginResponse)(nil), // 1: api_auth.LoginResponse
+ (*RegisterRequest)(nil), // 2: api_auth.RegisterRequest
+ (*RegisterResponse)(nil), // 3: api_auth.RegisterResponse
+ (*APIPermission)(nil), // 4: api_auth.APIPermission
+ (*SignTokenRequest)(nil), // 5: api_auth.SignTokenRequest
+ (*SignTokenResponse)(nil), // 6: api_auth.SignTokenResponse
+ (*Status)(nil), // 7: common.Status
}
var file_api_auth_proto_depIdxs = []int32{
- 4, // 0: api_auth.LoginResponse.status:type_name -> common.Status
- 4, // 1: api_auth.RegisterResponse.status:type_name -> common.Status
- 2, // [2:2] is the sub-list for method output_type
- 2, // [2:2] is the sub-list for method input_type
- 2, // [2:2] is the sub-list for extension type_name
- 2, // [2:2] is the sub-list for extension extendee
- 0, // [0:2] is the sub-list for field type_name
+ 7, // 0: api_auth.LoginResponse.status:type_name -> common.Status
+ 7, // 1: api_auth.RegisterResponse.status:type_name -> common.Status
+ 4, // 2: api_auth.SignTokenRequest.permissions:type_name -> api_auth.APIPermission
+ 7, // 3: api_auth.SignTokenResponse.status:type_name -> common.Status
+ 4, // [4:4] is the sub-list for method output_type
+ 4, // [4:4] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
}
func init() { file_api_auth_proto_init() }
@@ -295,13 +471,16 @@ func file_api_auth_proto_init() {
file_api_auth_proto_msgTypes[1].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[2].OneofWrappers = []any{}
file_api_auth_proto_msgTypes[3].OneofWrappers = []any{}
+ file_api_auth_proto_msgTypes[4].OneofWrappers = []any{}
+ file_api_auth_proto_msgTypes[5].OneofWrappers = []any{}
+ file_api_auth_proto_msgTypes[6].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_auth_proto_rawDesc), len(file_api_auth_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 4,
+ NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/pb/api_client.pb.go b/pb/api_client.pb.go
index 2270bd9..f34a4dc 100644
--- a/pb/api_client.pb.go
+++ b/pb/api_client.pb.go
@@ -24,6 +24,7 @@ const (
type InitClientRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClientId *string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3,oneof" json:"client_id,omitempty"`
+ Ephemeral *bool `protobuf:"varint,2,opt,name=ephemeral,proto3,oneof" json:"ephemeral,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -65,6 +66,13 @@ func (x *InitClientRequest) GetClientId() string {
return ""
}
+func (x *InitClientRequest) GetEphemeral() bool {
+ if x != nil && x.Ephemeral != nil {
+ return *x.Ephemeral
+ }
+ return false
+}
+
type InitClientResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3,oneof" json:"status,omitempty"`
@@ -1498,11 +1506,14 @@ var File_api_client_proto protoreflect.FileDescriptor
const file_api_client_proto_rawDesc = "" +
"\n" +
"\x10api_client.proto\x12\n" +
- "api_client\x1a\fcommon.proto\"C\n" +
+ "api_client\x1a\fcommon.proto\"t\n" +
"\x11InitClientRequest\x12 \n" +
- "\tclient_id\x18\x01 \x01(\tH\x00R\bclientId\x88\x01\x01B\f\n" +
+ "\tclient_id\x18\x01 \x01(\tH\x00R\bclientId\x88\x01\x01\x12!\n" +
+ "\tephemeral\x18\x02 \x01(\bH\x01R\tephemeral\x88\x01\x01B\f\n" +
"\n" +
- "_client_id\"|\n" +
+ "_client_idB\f\n" +
+ "\n" +
+ "_ephemeral\"|\n" +
"\x12InitClientResponse\x12+\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12 \n" +
"\tclient_id\x18\x02 \x01(\tH\x01R\bclientId\x88\x01\x01B\t\n" +
diff --git a/pb/common.pb.go b/pb/common.pb.go
index 63f3610..6a25013 100644
--- a/pb/common.pb.go
+++ b/pb/common.pb.go
@@ -289,7 +289,9 @@ type Client struct {
Stopped *bool `protobuf:"varint,7,opt,name=stopped,proto3,oneof" json:"stopped,omitempty"`
ClientIds []string `protobuf:"bytes,8,rep,name=client_ids,json=clientIds,proto3" json:"client_ids,omitempty"` // some client can connected to more than one server, make a shadow client to handle this
OriginClientId *string `protobuf:"bytes,9,opt,name=origin_client_id,json=originClientId,proto3,oneof" json:"origin_client_id,omitempty"`
- FrpsUrl *string `protobuf:"bytes,10,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"` // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
+ FrpsUrl *string `protobuf:"bytes,10,opt,name=frps_url,json=frpsUrl,proto3,oneof" json:"frps_url,omitempty"` // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
+ Ephemeral *bool `protobuf:"varint,11,opt,name=ephemeral,proto3,oneof" json:"ephemeral,omitempty"` // 是否临时节点
+ LastSeenAt *int64 `protobuf:"varint,12,opt,name=last_seen_at,json=lastSeenAt,proto3,oneof" json:"last_seen_at,omitempty"` // 最后一次心跳时间戳
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -387,6 +389,20 @@ func (x *Client) GetFrpsUrl() string {
return ""
}
+func (x *Client) GetEphemeral() bool {
+ if x != nil && x.Ephemeral != nil {
+ return *x.Ephemeral
+ }
+ return false
+}
+
+func (x *Client) GetLastSeenAt() int64 {
+ if x != nil && x.LastSeenAt != nil {
+ return *x.LastSeenAt
+ }
+ return 0
+}
+
type Server struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id *string `protobuf:"bytes,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
@@ -862,7 +878,7 @@ const file_common_proto_rawDesc = "" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusH\x00R\x06status\x88\x01\x01\x12\x17\n" +
"\x04data\x18\x02 \x01(\tH\x01R\x04data\x88\x01\x01B\t\n" +
"\a_statusB\a\n" +
- "\x05_data\"\x8a\x03\n" +
+ "\x05_data\"\xf3\x03\n" +
"\x06Client\x12\x13\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\n" +
"\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x1b\n" +
@@ -874,7 +890,10 @@ const file_common_proto_rawDesc = "" +
"client_ids\x18\b \x03(\tR\tclientIds\x12-\n" +
"\x10origin_client_id\x18\t \x01(\tH\x06R\x0eoriginClientId\x88\x01\x01\x12\x1e\n" +
"\bfrps_url\x18\n" +
- " \x01(\tH\aR\afrpsUrl\x88\x01\x01B\x05\n" +
+ " \x01(\tH\aR\afrpsUrl\x88\x01\x01\x12!\n" +
+ "\tephemeral\x18\v \x01(\bH\bR\tephemeral\x88\x01\x01\x12%\n" +
+ "\flast_seen_at\x18\f \x01(\x03H\tR\n" +
+ "lastSeenAt\x88\x01\x01B\x05\n" +
"\x03_idB\t\n" +
"\a_secretB\t\n" +
"\a_configB\n" +
@@ -885,7 +904,10 @@ const file_common_proto_rawDesc = "" +
"\n" +
"\b_stoppedB\x13\n" +
"\x11_origin_client_idB\v\n" +
- "\t_frps_url\"\xd8\x01\n" +
+ "\t_frps_urlB\f\n" +
+ "\n" +
+ "_ephemeralB\x0f\n" +
+ "\r_last_seen_at\"\xd8\x01\n" +
"\x06Server\x12\x13\n" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x1b\n" +
"\x06secret\x18\x02 \x01(\tH\x01R\x06secret\x88\x01\x01\x12\x13\n" +
diff --git a/services/app/app_impl.go b/services/app/app_impl.go
index 5bfa69b..5eac677 100644
--- a/services/app/app_impl.go
+++ b/services/app/app_impl.go
@@ -4,6 +4,7 @@ import (
"sync"
"github.com/VaalaCat/frp-panel/conf"
+ "github.com/casbin/casbin/v2"
"google.golang.org/grpc/credentials"
)
@@ -22,6 +23,39 @@ type application struct {
serverController ServerController
rpcCred credentials.TransportCredentials
conf conf.Config
+ currentRole string
+ permManager PermissionManager
+ enforcer *casbin.Enforcer
+}
+
+// GetEnforcer implements Application.
+func (a *application) GetEnforcer() *casbin.Enforcer {
+ return a.enforcer
+}
+
+// SetEnforcer implements Application.
+func (a *application) SetEnforcer(c *casbin.Enforcer) {
+ a.enforcer = c
+}
+
+// GetPermManager implements Application.
+func (a *application) GetPermManager() PermissionManager {
+ return a.permManager
+}
+
+// SetPermManager implements Application.
+func (a *application) SetPermManager(p PermissionManager) {
+ a.permManager = p
+}
+
+// GetCurrentRole implements Application.
+func (a *application) GetCurrentRole() string {
+ return a.currentRole
+}
+
+// SetCurrentRole implements Application.
+func (a *application) SetCurrentRole(r string) {
+ a.currentRole = r
}
// GetClientCred implements Application.
diff --git a/services/app/application.go b/services/app/application.go
index 4312c8e..9582411 100644
--- a/services/app/application.go
+++ b/services/app/application.go
@@ -5,6 +5,7 @@ import (
"sync"
"github.com/VaalaCat/frp-panel/conf"
+ "github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/credentials"
)
@@ -36,6 +37,12 @@ type Application interface {
SetConfig(conf.Config)
GetRPCCred() credentials.TransportCredentials
SetRPCCred(credentials.TransportCredentials)
+ GetCurrentRole() string
+ SetCurrentRole(string)
+ GetEnforcer() *casbin.Enforcer
+ SetEnforcer(*casbin.Enforcer)
+ GetPermManager() PermissionManager
+ SetPermManager(PermissionManager)
}
type Context struct {
diff --git a/services/app/provider.go b/services/app/provider.go
index d652fc1..7b2847b 100644
--- a/services/app/provider.go
+++ b/services/app/provider.go
@@ -4,8 +4,10 @@ import (
"sync"
"time"
+ "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils"
+ "github.com/casbin/casbin/v2"
"github.com/fatedier/frp/client/proxy"
v1 "github.com/fatedier/frp/pkg/config/v1"
@@ -66,10 +68,10 @@ type ClientLogManager interface {
// models/db.go
type DBManager interface {
- GetDB(dbType string) *gorm.DB
+ GetDB(dbType string, dbRole string) *gorm.DB
GetDefaultDB() *gorm.DB
- SetDB(dbType string, db *gorm.DB)
- RemoveDB(dbType string)
+ SetDB(dbType string, dbRole string, db *gorm.DB)
+ RemoveDB(dbType string, dbRole string)
SetDebug(bool)
Init()
}
@@ -157,3 +159,15 @@ type ServerHandler interface {
type MasterClient interface {
Call() pb.MasterClient
}
+
+// services/rbac/perm_manager.go
+type PermissionManager interface {
+ AddUserToGroup(userID int, groupID string, tenantID int) (bool, error)
+ CheckPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
+ Enforcer() *casbin.Enforcer
+ GrantGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
+ GrantUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
+ RemoveUserFromGroup(userID int, groupID string, tenantID int) (bool, error)
+ RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
+ RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
+}
diff --git a/services/dao/client.go b/services/dao/client.go
index c1d2ff0..37335f5 100644
--- a/services/dao/client.go
+++ b/services/dao/client.go
@@ -2,6 +2,7 @@ package dao
import (
"fmt"
+ "time"
"github.com/VaalaCat/frp-panel/models"
"github.com/samber/lo"
@@ -175,8 +176,6 @@ func (q *queryImpl) ListClients(userInfo models.UserInfo, page, pageSize int) ([
Where(
db.Where(
normalClientFilter(db),
- ).Or(
- "is_shadow = ?", true,
),
).Offset(offset).Limit(pageSize).Find(&clients).Error
if err != nil {
@@ -331,10 +330,30 @@ func (q *queryImpl) AdminGetClientIDsInShadowByClientID(clientID string) ([]stri
}), nil
}
+func (q *queryImpl) AdminUpdateClientLastSeen(clientID string) error {
+ db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
+ return db.Model(&models.Client{
+ ClientEntity: &models.ClientEntity{
+ ClientID: clientID,
+ }}).Update("last_seen_at", time.Now()).Error
+}
+
func normalClientFilter(db *gorm.DB) *gorm.DB {
// 1. 没shadow过的老client
// 2. shadow过的shadow client
- return db.Where("origin_client_id is NULL").
- Or("is_shadow = ?", true).
- Or("LENGTH(origin_client_id) = ?", 0)
+ // 3. 非临时节点
+ return db.Where(
+ db.Where("origin_client_id is NULL").
+ Or("is_shadow = ?", true).
+ Or("LENGTH(origin_client_id) = ?", 0),
+ ).
+ Where(
+ db.Where(
+ db.Where("ephemeral is NULL").
+ Or("ephemeral = ?", false),
+ ).Or(
+ db.Where("ephemeral = ?", true).
+ Where("last_seen_at > ?", time.Now().Add(-5*time.Minute)),
+ ),
+ )
}
diff --git a/services/dao/user.go b/services/dao/user.go
index f72e4b4..ca80905 100644
--- a/services/dao/user.go
+++ b/services/dao/user.go
@@ -62,6 +62,20 @@ func (q *queryImpl) UpdateUser(userInfo models.UserInfo, user *models.UserEntity
}).Error
}
+func (q *queryImpl) AdminUpdateUser(userInfo models.UserInfo, user *models.UserEntity) error {
+ db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
+ user.UserID = userInfo.GetUserID()
+ return db.Model(&models.User{}).Where(
+ &models.User{
+ UserEntity: &models.UserEntity{
+ UserID: userInfo.GetUserID(),
+ },
+ },
+ ).Save(&models.User{
+ UserEntity: user,
+ }).Error
+}
+
func (q *queryImpl) GetUserByUserName(userName string) (*models.UserEntity, error) {
if userName == "" {
return nil, fmt.Errorf("invalid user name")
diff --git a/services/dao/user_group.go b/services/dao/user_group.go
new file mode 100644
index 0000000..a588194
--- /dev/null
+++ b/services/dao/user_group.go
@@ -0,0 +1,45 @@
+package dao
+
+import (
+ "fmt"
+
+ "github.com/VaalaCat/frp-panel/defs"
+ "github.com/VaalaCat/frp-panel/models"
+)
+
+func (q *queryImpl) CreateGroup(userInfo models.UserInfo, groupID, groupName, comment string) (*models.UserGroup, error) {
+ if groupID == "" || groupName == "" {
+ return nil, fmt.Errorf("invalid group id or group name")
+ }
+
+ if userInfo.GetRole() != defs.UserRole_Admin {
+ return nil, fmt.Errorf("only admin can create group")
+ }
+
+ db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
+
+ g := &models.UserGroup{
+ TenantID: userInfo.GetTenantID(),
+ GroupID: groupID,
+ GroupName: groupName,
+ Comment: comment,
+ }
+ err := db.Create(g).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return g, nil
+}
+
+func (q *queryImpl) DeleteGroup(userInfo models.UserInfo, groupID string) error {
+ if userInfo.GetRole() != defs.UserRole_Admin {
+ return fmt.Errorf("only admin can delete group")
+ }
+
+ db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
+ return db.Unscoped().Where(&models.UserGroup{
+ TenantID: userInfo.GetTenantID(),
+ GroupID: groupID,
+ }).Delete(&models.UserGroup{}).Error
+}
diff --git a/services/master/grpc_server.go b/services/master/grpc_server.go
index 335649e..461a591 100644
--- a/services/master/grpc_server.go
+++ b/services/master/grpc_server.go
@@ -134,6 +134,12 @@ func (s *server) ServerSend(sender pb.Master_ServerSendServer) error {
return fmt.Errorf("invalid secret, %s id: [%s]", req.GetEvent().String(), req.GetClientId())
}
+ if cliType == defs.CliTypeClient {
+ if err := dao.NewQuery(ctx).AdminUpdateClientLastSeen(req.GetClientId()); err != nil {
+ logger.Logger(ctx).Errorf("cannot update client last seen, %s id: [%s]", req.GetEvent().String(), req.GetClientId())
+ }
+ }
+
s.appInstance.GetClientsManager().Set(req.GetClientId(), cliType, sender)
done = rpc.Recv(s.appInstance, req.GetClientId())
sender.Send(&pb.ServerMessage{
diff --git a/services/rbac/init.go b/services/rbac/init.go
new file mode 100644
index 0000000..8f5f92a
--- /dev/null
+++ b/services/rbac/init.go
@@ -0,0 +1,43 @@
+package rbac
+
+import (
+ "github.com/VaalaCat/frp-panel/services/app"
+ "github.com/VaalaCat/frp-panel/utils/logger"
+ "github.com/casbin/casbin/v2"
+ "github.com/casbin/casbin/v2/model"
+ gormadapter "github.com/casbin/gorm-adapter/v3"
+ "gorm.io/gorm"
+)
+
+func InitializeCasbin(ctx *app.Context, db *gorm.DB) (*casbin.Enforcer, error) {
+ adapter, err := gormadapter.NewAdapterByDB(db)
+ if err != nil {
+ logger.Logger(ctx).Fatalf("error creating Casbin GORM adapter: %v", err)
+ return nil, err
+ }
+
+ // Load the Casbin model from file
+ m, err := model.NewModelFromString(RBAC_MODEL)
+ if err != nil {
+ logger.Logger(ctx).Fatalf("error loading Casbin model: %v", err)
+ return nil, err
+ }
+
+ // Create the Casbin enforcer
+ enforcer, err := casbin.NewEnforcer(m, adapter)
+ if err != nil {
+ logger.Logger(ctx).Fatalf("error creating Casbin enforcer: %v", err)
+ return nil, err
+ }
+
+ err = enforcer.LoadPolicy()
+ if err != nil {
+ logger.Logger(ctx).WithError(err).Warnf("could not load Casbin policy from DB")
+ // return nil, err
+ }
+
+ enforcer.EnableAutoSave(true)
+
+ logger.Logger(ctx).Infof("Casbin Enforcer initialized successfully.")
+ return enforcer, nil
+}
diff --git a/services/rbac/perm_manager.go b/services/rbac/perm_manager.go
new file mode 100644
index 0000000..8312a69
--- /dev/null
+++ b/services/rbac/perm_manager.go
@@ -0,0 +1,82 @@
+package rbac
+
+import (
+ "fmt"
+
+ "github.com/VaalaCat/frp-panel/defs"
+ "github.com/casbin/casbin/v2"
+)
+
+type permManager struct {
+ enforcer *casbin.Enforcer
+}
+
+func (pm *permManager) Enforcer() *casbin.Enforcer {
+ return pm.enforcer
+}
+
+func NewPermManager(enforcer *casbin.Enforcer) *permManager {
+ return &permManager{
+ enforcer: enforcer,
+ }
+}
+
+func identity[T defs.RBACObj | defs.RBACSubject | defs.RBACDomain, U string | int | uint | int64](rType T, objID U) string {
+ return string(rType) + ":" + fmt.Sprint(objID)
+}
+
+func (pm *permManager) GrantGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) {
+ groupSubject := identity(defs.RBACSubjectGroup, groupID)
+ objSubject := identity(objType, objID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.AddPolicy(groupSubject, objSubject, string(action), domain)
+}
+
+func (pm *permManager) RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) {
+ groupSubject := identity(defs.RBACSubjectGroup, groupID)
+ objSubject := identity(objType, objID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.RemovePolicy(groupSubject, objSubject, string(action), domain)
+}
+
+func (pm *permManager) GrantUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) {
+ userSubject := identity(defs.RBACSubjectUser, userID)
+ objSubject := identity(objType, objID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.AddPolicy(userSubject, objSubject, string(action), domain)
+}
+
+func (pm *permManager) RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) {
+ userSubject := identity(defs.RBACSubjectUser, userID)
+ objSubject := identity(objType, objID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.RemovePolicy(userSubject, objSubject, string(action), domain)
+}
+
+func (pm *permManager) CheckPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) {
+ userSubject := identity(defs.RBACSubjectUser, userID)
+ objSubject := identity(objType, objID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.Enforce(userSubject, objSubject, string(action), domain)
+}
+
+func (pm *permManager) AddUserToGroup(userID int, groupID string, tenantID int) (bool, error) {
+ userSub := identity(defs.RBACSubjectUser, userID)
+ groupSub := identity(defs.RBACSubjectGroup, groupID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.AddGroupingPolicy(userSub, groupSub, domain)
+}
+
+func (pm *permManager) RemoveUserFromGroup(userID int, groupID string, tenantID int) (bool, error) {
+ userSub := identity(defs.RBACSubjectUser, userID)
+ groupSub := identity(defs.RBACSubjectGroup, groupID)
+ domain := identity(defs.RBACDomainTenant, tenantID)
+
+ return pm.enforcer.RemoveGroupingPolicy(userSub, groupSub, domain)
+}
diff --git a/services/rbac/rbac_models.go b/services/rbac/rbac_models.go
new file mode 100644
index 0000000..b94e4d4
--- /dev/null
+++ b/services/rbac/rbac_models.go
@@ -0,0 +1,17 @@
+package rbac
+
+const RBAC_MODEL = `
+[request_definition]
+r = sub, obj, act, dom
+
+[policy_definition]
+p = sub, obj, act, dom
+
+[role_definition]
+g = _, _, dom
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act`
diff --git a/services/rpc/master.go b/services/rpc/master.go
index 623d9b6..3a61b74 100644
--- a/services/rpc/master.go
+++ b/services/rpc/master.go
@@ -105,13 +105,14 @@ func GetClientCert(appInstance app.Application, clientID, clientSecret string, c
return resp.Cert
}
-func InitClient(appInstance app.Application, clientID, joinToken string) (*pb.InitClientResponse, error) {
- apiEndpoint := conf.GetAPIURL(appInstance.GetConfig())
+func InitClient(cfg conf.Config, clientID, joinToken string, ephemeral bool) (*pb.InitClientResponse, error) {
+ apiEndpoint := conf.GetAPIURL(cfg)
c := httpCli()
rawReq, err := proto.Marshal(&pb.InitClientRequest{
- ClientId: &clientID,
+ ClientId: &clientID,
+ Ephemeral: &ephemeral,
})
if err != nil {
return nil, err
@@ -132,8 +133,8 @@ func InitClient(appInstance app.Application, clientID, joinToken string) (*pb.In
return resp, nil
}
-func GetClient(appInstance app.Application, clientID, joinToken string) (*pb.GetClientResponse, error) {
- apiEndpoint := conf.GetAPIURL(appInstance.GetConfig())
+func GetClient(cfg conf.Config, clientID, joinToken string) (*pb.GetClientResponse, error) {
+ apiEndpoint := conf.GetAPIURL(cfg)
c := httpCli()
rawReq, err := proto.Marshal(&pb.GetClientRequest{
diff --git a/utils/ctx.go b/utils/ctx.go
index 169f39c..3cd8c2a 100644
--- a/utils/ctx.go
+++ b/utils/ctx.go
@@ -18,7 +18,7 @@ func GetValue[T any](c context.Context, key string) (T, bool) {
return v, true
}
-func getValue[T any](c context.Context, key string) (interface{}, bool) {
+func getValue[T any](c context.Context, key string) (any, bool) {
val := c.Value(key)
if val == nil {
return *new(T), false
diff --git a/utils/json.go b/utils/json.go
new file mode 100644
index 0000000..24226cc
--- /dev/null
+++ b/utils/json.go
@@ -0,0 +1,11 @@
+package utils
+
+import "encoding/json"
+
+func MarshalForJson(v any) string {
+ ret, err := json.Marshal(v)
+ if err != nil {
+ return ""
+ }
+ return string(ret)
+}
diff --git a/utils/jwt.go b/utils/jwt.go
index c0d0ed4..85276d4 100644
--- a/utils/jwt.go
+++ b/utils/jwt.go
@@ -24,7 +24,7 @@ func GetJwtToken(secretKey string, iat, seconds int64, payload string) (string,
// @iat: 时间戳
// @seconds: 过期时间,单位秒
// @payload: 数据载体
-func GetJwtTokenFromMap(secretKey string, iat, seconds int64, payload map[string]string) (string, error) {
+func GetJwtTokenFromMap(secretKey string, iat, seconds int64, payload map[string]interface{}) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
diff --git a/www/api/user.ts b/www/api/user.ts
index 64f565f..684f3c4 100644
--- a/www/api/user.ts
+++ b/www/api/user.ts
@@ -1,5 +1,6 @@
import http from '@/api/http'
import { API_PATH } from '@/lib/consts'
+import { SignTokenRequest, SignTokenResponse } from '@/lib/pb/api_auth'
import {
GetUserInfoRequest,
GetUserInfoResponse,
@@ -20,3 +21,8 @@ export const updateUserInfo = async (req: UpdateUserInfoRequest) => {
const res = await http.post(API_PATH + '/user/update', UpdateUserInfoRequest.toJson(req))
return UpdateUserInfoResponse.fromJson((res.data as BaseResponse).body)
}
+
+export const signToken = async (req: SignTokenRequest) => {
+ const res = await http.post(API_PATH + '/user/sign-token', SignTokenRequest.toJson(req))
+ return SignTokenResponse.fromJson((res.data as BaseResponse).body)
+}
\ No newline at end of file
diff --git a/www/components/frpc/client_item.tsx b/www/components/frpc/client_item.tsx
index 8a208da..cf97ac0 100644
--- a/www/components/frpc/client_item.tsx
+++ b/www/components/frpc/client_item.tsx
@@ -227,6 +227,10 @@ export const ClientInfo = ({ client }: { client: ClientTableSchema }) => {
{`需要升级!`}
}
+ {client.originClient.ephemeral &&
+ {`临时`}
+ }
+
)
}
diff --git a/www/components/frpc/client_join_button.tsx b/www/components/frpc/client_join_button.tsx
index 45daa84..c874a56 100644
--- a/www/components/frpc/client_join_button.tsx
+++ b/www/components/frpc/client_join_button.tsx
@@ -1,14 +1,18 @@
import { Button } from '@/components/ui/button'
-import React from 'react'
+import React, { useState } from 'react'
import { JoinCommandStr } from '@/lib/consts'
import { useStore } from '@nanostores/react'
-import { $platformInfo, $token } from '@/store/user'
+import { $platformInfo } from '@/store/user'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useTranslation } from 'react-i18next'
+import { signToken } from '@/api/user'
+import { useQuery } from '@tanstack/react-query'
+import { toast } from 'sonner'
+import { RespCode } from '@/lib/pb/common'
export const ClientJoinButton = () => {
const platformInfo = useStore($platformInfo)
- const token = useStore($token)
+ const [joinToken, setJoinToken] = useState(undefined)
const { t } = useTranslation()
@@ -19,6 +23,25 @@ export const ClientJoinButton = () => {
)
}
+ const handleNewToken = async () => {
+ try {
+ const resp = await signToken({
+ expiresIn: BigInt(1000000000),
+ permissions: [
+ { method: 'POST', path: '/api/v1/client/get', },
+ { method: 'POST', path: '/api/v1/client/init', },
+ ],
+ })
+ if (!resp || !resp.status || resp.status.code !== RespCode.SUCCESS) {
+ toast.error('server error')
+ return
+ }
+ setJoinToken(resp.token)
+ } catch (error) {
+ toast.error(JSON.stringify(error))
+ }
+ }
+
return (
@@ -32,24 +55,36 @@ export const ClientJoinButton = () => {
{t('client.join.description')} ({t('common.download')})
- {token != undefined &&
-
- {JoinCommandStr(platformInfo, token)}
-
+
+ {joinToken != undefined && <>
+
+ {JoinCommandStr(platformInfo, joinToken)}
+
+
+ >
+ }
-
}
+
diff --git a/www/config/notify.ts b/www/config/notify.ts
index 8a8c6d5..22a4aaa 100644
--- a/www/config/notify.ts
+++ b/www/config/notify.ts
@@ -5,5 +5,15 @@ export function NeedUpgrade(version: ClientVersion | undefined) {
if (!version.gitVersion) return false
const versionString = version?.gitVersion
const [a, b, c] = versionString.split('.')
- return Number(b) < 1
+ if (Number(b) < 1) {
+ return true
+ }
+
+ console.log(Number(a), Number(b), Number(c))
+
+ if (a=='v0' && Number(b)<=1 && Number(c) <= 10) {
+ return true
+ }
+
+ return false
}
\ No newline at end of file
diff --git a/www/i18n/locales/en.json b/www/i18n/locales/en.json
index 01df0e6..a072d03 100644
--- a/www/i18n/locales/en.json
+++ b/www/i18n/locales/en.json
@@ -258,7 +258,8 @@
"button": "Batch Configuration",
"title": "Batch Configuration",
"description": "Download the binary files in advance, run the following command, and the client will automatically generate and save the configuration file.",
- "copy": "Copy Command"
+ "copy": "Copy Command",
+ "sign_token": "Generate Token"
},
"start": {
"title": "Start Command",
diff --git a/www/i18n/locales/zh.json b/www/i18n/locales/zh.json
index b416700..93a2e56 100644
--- a/www/i18n/locales/zh.json
+++ b/www/i18n/locales/zh.json
@@ -263,7 +263,8 @@
"button": "批量配置",
"title": "批量配置",
"description": "提前下载好二进制文件,运行以下命令,client 将自动生成配置文件保存",
- "copy": "复制命令"
+ "copy": "复制命令",
+ "sign_token": "生成令牌"
},
"actions_menu": {
"title": "客户端操作",
diff --git a/www/lib/consts.ts b/www/lib/consts.ts
index 0228057..26c6943 100644
--- a/www/lib/consts.ts
+++ b/www/lib/consts.ts
@@ -78,7 +78,7 @@ export const LinuxInstallCommand = (
item: T,
info: GetPlatformInfoResponse,
) => {
- return `curl -sSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}`
+ return `curl -fSL https://raw.githubusercontent.com/VaalaCat/frp-panel/main/install.sh | bash -s --${ExecCommandStr(type, item, info, ' ')}`
}
export const ClientEnvFile = (
diff --git a/www/lib/pb/api_auth.ts b/www/lib/pb/api_auth.ts
index 78f0f1d..d638d20 100644
--- a/www/lib/pb/api_auth.ts
+++ b/www/lib/pb/api_auth.ts
@@ -63,6 +63,45 @@ export interface RegisterResponse {
*/
status?: Status;
}
+/**
+ * @generated from protobuf message api_auth.APIPermission
+ */
+export interface APIPermission {
+ /**
+ * @generated from protobuf field: optional string method = 1;
+ */
+ method?: string;
+ /**
+ * @generated from protobuf field: optional string path = 2;
+ */
+ path?: string;
+}
+/**
+ * @generated from protobuf message api_auth.SignTokenRequest
+ */
+export interface SignTokenRequest {
+ /**
+ * @generated from protobuf field: optional int64 expires_in = 1;
+ */
+ expiresIn?: bigint;
+ /**
+ * @generated from protobuf field: repeated api_auth.APIPermission permissions = 2;
+ */
+ permissions: APIPermission[];
+}
+/**
+ * @generated from protobuf message api_auth.SignTokenResponse
+ */
+export interface SignTokenResponse {
+ /**
+ * @generated from protobuf field: optional common.Status status = 1;
+ */
+ status?: Status;
+ /**
+ * @generated from protobuf field: optional string token = 2;
+ */
+ token?: string;
+}
// @generated message type with reflection information, may provide speed optimized methods
class LoginRequest$Type extends MessageType {
constructor() {
@@ -275,3 +314,163 @@ class RegisterResponse$Type extends MessageType {
* @generated MessageType for protobuf message api_auth.RegisterResponse
*/
export const RegisterResponse = new RegisterResponse$Type();
+// @generated message type with reflection information, may provide speed optimized methods
+class APIPermission$Type extends MessageType {
+ constructor() {
+ super("api_auth.APIPermission", [
+ { no: 1, name: "method", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
+ { no: 2, name: "path", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
+ ]);
+ }
+ create(value?: PartialMessage): APIPermission {
+ const message = globalThis.Object.create((this.messagePrototype!));
+ if (value !== undefined)
+ reflectionMergePartial(this, message, value);
+ return message;
+ }
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: APIPermission): APIPermission {
+ let message = target ?? this.create(), end = reader.pos + length;
+ while (reader.pos < end) {
+ let [fieldNo, wireType] = reader.tag();
+ switch (fieldNo) {
+ case /* optional string method */ 1:
+ message.method = reader.string();
+ break;
+ case /* optional string path */ 2:
+ message.path = reader.string();
+ break;
+ default:
+ let u = options.readUnknownField;
+ if (u === "throw")
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
+ let d = reader.skip(wireType);
+ if (u !== false)
+ (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
+ }
+ }
+ return message;
+ }
+ internalBinaryWrite(message: APIPermission, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ /* optional string method = 1; */
+ if (message.method !== undefined)
+ writer.tag(1, WireType.LengthDelimited).string(message.method);
+ /* optional string path = 2; */
+ if (message.path !== undefined)
+ writer.tag(2, WireType.LengthDelimited).string(message.path);
+ let u = options.writeUnknownFields;
+ if (u !== false)
+ (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
+ return writer;
+ }
+}
+/**
+ * @generated MessageType for protobuf message api_auth.APIPermission
+ */
+export const APIPermission = new APIPermission$Type();
+// @generated message type with reflection information, may provide speed optimized methods
+class SignTokenRequest$Type extends MessageType {
+ constructor() {
+ super("api_auth.SignTokenRequest", [
+ { no: 1, name: "expires_in", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
+ { no: 2, name: "permissions", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => APIPermission }
+ ]);
+ }
+ create(value?: PartialMessage): SignTokenRequest {
+ const message = globalThis.Object.create((this.messagePrototype!));
+ message.permissions = [];
+ if (value !== undefined)
+ reflectionMergePartial(this, message, value);
+ return message;
+ }
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SignTokenRequest): SignTokenRequest {
+ let message = target ?? this.create(), end = reader.pos + length;
+ while (reader.pos < end) {
+ let [fieldNo, wireType] = reader.tag();
+ switch (fieldNo) {
+ case /* optional int64 expires_in */ 1:
+ message.expiresIn = reader.int64().toBigInt();
+ break;
+ case /* repeated api_auth.APIPermission permissions */ 2:
+ message.permissions.push(APIPermission.internalBinaryRead(reader, reader.uint32(), options));
+ break;
+ default:
+ let u = options.readUnknownField;
+ if (u === "throw")
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
+ let d = reader.skip(wireType);
+ if (u !== false)
+ (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
+ }
+ }
+ return message;
+ }
+ internalBinaryWrite(message: SignTokenRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ /* optional int64 expires_in = 1; */
+ if (message.expiresIn !== undefined)
+ writer.tag(1, WireType.Varint).int64(message.expiresIn);
+ /* repeated api_auth.APIPermission permissions = 2; */
+ for (let i = 0; i < message.permissions.length; i++)
+ APIPermission.internalBinaryWrite(message.permissions[i], writer.tag(2, WireType.LengthDelimited).fork(), options).join();
+ let u = options.writeUnknownFields;
+ if (u !== false)
+ (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
+ return writer;
+ }
+}
+/**
+ * @generated MessageType for protobuf message api_auth.SignTokenRequest
+ */
+export const SignTokenRequest = new SignTokenRequest$Type();
+// @generated message type with reflection information, may provide speed optimized methods
+class SignTokenResponse$Type extends MessageType {
+ constructor() {
+ super("api_auth.SignTokenResponse", [
+ { no: 1, name: "status", kind: "message", T: () => Status },
+ { no: 2, name: "token", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
+ ]);
+ }
+ create(value?: PartialMessage): SignTokenResponse {
+ const message = globalThis.Object.create((this.messagePrototype!));
+ if (value !== undefined)
+ reflectionMergePartial(this, message, value);
+ return message;
+ }
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SignTokenResponse): SignTokenResponse {
+ let message = target ?? this.create(), end = reader.pos + length;
+ while (reader.pos < end) {
+ let [fieldNo, wireType] = reader.tag();
+ switch (fieldNo) {
+ case /* optional common.Status status */ 1:
+ message.status = Status.internalBinaryRead(reader, reader.uint32(), options, message.status);
+ break;
+ case /* optional string token */ 2:
+ message.token = reader.string();
+ break;
+ default:
+ let u = options.readUnknownField;
+ if (u === "throw")
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
+ let d = reader.skip(wireType);
+ if (u !== false)
+ (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
+ }
+ }
+ return message;
+ }
+ internalBinaryWrite(message: SignTokenResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
+ /* optional common.Status status = 1; */
+ if (message.status)
+ Status.internalBinaryWrite(message.status, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
+ /* optional string token = 2; */
+ if (message.token !== undefined)
+ writer.tag(2, WireType.LengthDelimited).string(message.token);
+ let u = options.writeUnknownFields;
+ if (u !== false)
+ (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
+ return writer;
+ }
+}
+/**
+ * @generated MessageType for protobuf message api_auth.SignTokenResponse
+ */
+export const SignTokenResponse = new SignTokenResponse$Type();
diff --git a/www/lib/pb/api_client.ts b/www/lib/pb/api_client.ts
index 46fc627..729ea02 100644
--- a/www/lib/pb/api_client.ts
+++ b/www/lib/pb/api_client.ts
@@ -23,6 +23,10 @@ export interface InitClientRequest {
* @generated from protobuf field: optional string client_id = 1;
*/
clientId?: string;
+ /**
+ * @generated from protobuf field: optional bool ephemeral = 2;
+ */
+ ephemeral?: boolean;
}
/**
* @generated from protobuf message api_client.InitClientResponse
@@ -391,7 +395,8 @@ export interface GetProxyConfigResponse {
class InitClientRequest$Type extends MessageType {
constructor() {
super("api_client.InitClientRequest", [
- { no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
+ { no: 1, name: "client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
+ { no: 2, name: "ephemeral", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }
]);
}
create(value?: PartialMessage): InitClientRequest {
@@ -408,6 +413,9 @@ class InitClientRequest$Type extends MessageType {
case /* optional string client_id */ 1:
message.clientId = reader.string();
break;
+ case /* optional bool ephemeral */ 2:
+ message.ephemeral = reader.bool();
+ break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -423,6 +431,9 @@ class InitClientRequest$Type extends MessageType {
/* optional string client_id = 1; */
if (message.clientId !== undefined)
writer.tag(1, WireType.LengthDelimited).string(message.clientId);
+ /* optional bool ephemeral = 2; */
+ if (message.ephemeral !== undefined)
+ writer.tag(2, WireType.Varint).bool(message.ephemeral);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
diff --git a/www/lib/pb/common.ts b/www/lib/pb/common.ts
index bf2aa74..a442a3f 100644
--- a/www/lib/pb/common.ts
+++ b/www/lib/pb/common.ts
@@ -85,6 +85,14 @@ export interface Client {
* @generated from protobuf field: optional string frps_url = 10;
*/
frpsUrl?: string; // 客户端用于连接frps的url,解决 frp 在 CDN 后的问题,格式类似 [tcp/ws/wss/quic/kcp]://example.com:7000
+ /**
+ * @generated from protobuf field: optional bool ephemeral = 11;
+ */
+ ephemeral?: boolean; // 是否临时节点
+ /**
+ * @generated from protobuf field: optional int64 last_seen_at = 12;
+ */
+ lastSeenAt?: bigint; // 最后一次心跳时间戳
}
/**
* @generated from protobuf message common.Server
@@ -467,7 +475,9 @@ class Client$Type extends MessageType {
{ no: 7, name: "stopped", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ },
{ no: 8, name: "client_ids", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ },
{ no: 9, name: "origin_client_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
- { no: 10, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
+ { no: 10, name: "frps_url", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
+ { no: 11, name: "ephemeral", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ },
+ { no: 12, name: "last_seen_at", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
]);
}
create(value?: PartialMessage): Client {
@@ -509,6 +519,12 @@ class Client$Type extends MessageType {
case /* optional string frps_url */ 10:
message.frpsUrl = reader.string();
break;
+ case /* optional bool ephemeral */ 11:
+ message.ephemeral = reader.bool();
+ break;
+ case /* optional int64 last_seen_at */ 12:
+ message.lastSeenAt = reader.int64().toBigInt();
+ break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -548,6 +564,12 @@ class Client$Type extends MessageType {
/* optional string frps_url = 10; */
if (message.frpsUrl !== undefined)
writer.tag(10, WireType.LengthDelimited).string(message.frpsUrl);
+ /* optional bool ephemeral = 11; */
+ if (message.ephemeral !== undefined)
+ writer.tag(11, WireType.Varint).bool(message.ephemeral);
+ /* optional int64 last_seen_at = 12; */
+ if (message.lastSeenAt !== undefined)
+ writer.tag(12, WireType.Varint).int64(message.lastSeenAt);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);