diff --git a/internal/configloaders/admin_module.go b/internal/configloaders/admin_module.go index 602083f5..9058555f 100644 --- a/internal/configloaders/admin_module.go +++ b/internal/configloaders/admin_module.go @@ -13,7 +13,8 @@ const ( AdminModuleCodeServer AdminModuleCode = "server" AdminModuleCodeNode AdminModuleCode = "node" AdminModuleCodeDNS AdminModuleCode = "dns" - AdminModuleCodeAdmin AdminModuleCode = "admin" + AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户 + AdminModuleCodeUser AdminModuleCode = "user" // 平台用户 AdminModuleCodeLog AdminModuleCode = "log" AdminModuleCodeSetting AdminModuleCode = "setting" AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块 @@ -118,6 +119,11 @@ func AllModuleMaps() []maps.Map { "code": AdminModuleCodeDNS, "url": "/dns", }, + { + "name": "平台用户", + "code": AdminModuleCodeUser, + "url": "/users", + }, { "name": "系统用户", "code": AdminModuleCodeAdmin, diff --git a/internal/rpc/rpc_client.go b/internal/rpc/rpc_client.go index 3a71cf36..cc48b201 100644 --- a/internal/rpc/rpc_client.go +++ b/internal/rpc/rpc_client.go @@ -224,6 +224,10 @@ func (this *RPCClient) ACMETaskRPC() pb.ACMETaskServiceClient { return pb.NewACMETaskServiceClient(this.pickConn()) } +func (this *RPCClient) UserRPC() pb.UserServiceClient { + return pb.NewUserServiceClient(this.pickConn()) +} + // 构造Admin上下文 func (this *RPCClient) Context(adminId int64) context.Context { ctx := context.Background() diff --git a/internal/web/actions/default/servers/server/settings/https/requestCertPopup.go b/internal/web/actions/default/servers/server/settings/https/requestCertPopup.go index 0b17aa63..12c6f255 100644 --- a/internal/web/actions/default/servers/server/settings/https/requestCertPopup.go +++ b/internal/web/actions/default/servers/server/settings/https/requestCertPopup.go @@ -113,7 +113,7 @@ func (this *RequestCertPopupAction) RunPost(params struct { this.ErrorPage(err) return } - defer this.CreateLogInfo("创建ACME用户", createUserResp.AcmeUserId) + defer this.CreateLogInfo("创建ACME用户 %d", createUserResp.AcmeUserId) acmeUserId = createUserResp.AcmeUserId this.Data["acmeUser"] = maps.Map{ diff --git a/internal/web/actions/default/users/createPopup.go b/internal/web/actions/default/users/createPopup.go new file mode 100644 index 00000000..c99aa429 --- /dev/null +++ b/internal/web/actions/default/users/createPopup.go @@ -0,0 +1,91 @@ +package users + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" +) + +type CreatePopupAction struct { + actionutils.ParentAction +} + +func (this *CreatePopupAction) Init() { + this.Nav("", "", "") +} + +func (this *CreatePopupAction) RunGet(params struct{}) { + this.Show() +} + +func (this *CreatePopupAction) RunPost(params struct { + Username string + Pass1 string + Pass2 string + Fullname string + Mobile string + Tel string + Email string + Remark string + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + params.Must. + Field("username", params.Username). + Require("请输入用户名"). + Match(`^[a-zA-Z0-9_]+$`, "用户名中只能含有英文、数字和下划线") + + checkUsernameResp, err := this.RPC().UserRPC().CheckUsername(this.AdminContext(), &pb.CheckUsernameRequest{ + UserId: 0, + Username: params.Username, + }) + if err != nil { + this.ErrorPage(err) + return + } + if checkUsernameResp.Exists { + this.FailField("username", "此用户名已经被占用,请换一个") + } + + params.Must. + Field("pass1", params.Pass1). + Require("请输入密码"). + Field("pass2", params.Pass2). + Require("请再次输入确认密码"). + Equal(params.Pass1, "两次输入的密码不一致") + + params.Must. + Field("fullname", params.Fullname). + Require("请输入全名") + + if len(params.Mobile) > 0 { + params.Must. + Field("mobile", params.Mobile). + Mobile("请输入正确的手机号") + } + if len(params.Email) > 0 { + params.Must. + Field("email", params.Email). + Email("请输入正确的电子邮箱") + } + + createResp, err := this.RPC().UserRPC().CreateUser(this.AdminContext(), &pb.CreateUserRequest{ + Username: params.Username, + Password: params.Pass1, + Fullname: params.Fullname, + Mobile: params.Mobile, + Tel: params.Tel, + Email: params.Email, + Remark: params.Remark, + Source: "admin:" + numberutils.FormatInt64(this.AdminId()), + }) + if err != nil { + this.ErrorPage(err) + return + } + defer this.CreateLogInfo("创建用户 %d", createResp.UserId) + + this.Success() +} diff --git a/internal/web/actions/default/users/delete.go b/internal/web/actions/default/users/delete.go new file mode 100644 index 00000000..10c1769d --- /dev/null +++ b/internal/web/actions/default/users/delete.go @@ -0,0 +1,26 @@ +package users + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" +) + +type DeleteAction struct { + actionutils.ParentAction +} + +func (this *DeleteAction) RunPost(params struct { + UserId int64 +}) { + defer this.CreateLogInfo("删除用户 %d", params.UserId) + + // TODO 检查用户是否有未完成的业务 + + _, err := this.RPC().UserRPC().DeleteUser(this.AdminContext(), &pb.DeleteUserRequest{UserId: params.UserId}) + if err != nil { + this.ErrorPage(err) + return + } + + this.Success() +} diff --git a/internal/web/actions/default/users/index.go b/internal/web/actions/default/users/index.go new file mode 100644 index 00000000..46d14b65 --- /dev/null +++ b/internal/web/actions/default/users/index.go @@ -0,0 +1,51 @@ +package users + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/maps" + timeutil "github.com/iwind/TeaGo/utils/time" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("", "", "") +} + +func (this *IndexAction) RunGet(params struct { + Keyword string +}) { + countResp, err := this.RPC().UserRPC().CountAllEnabledUsers(this.AdminContext(), &pb.CountAllEnabledUsersRequest{Keyword: params.Keyword}) + if err != nil { + this.ErrorPage(err) + return + } + count := countResp.Count + page := this.NewPage(count) + this.Data["page"] = page.AsHTML() + + usersResp, err := this.RPC().UserRPC().ListEnabledUsers(this.AdminContext(), &pb.ListEnabledUsersRequest{Keyword: params.Keyword}) + if err != nil { + this.ErrorPage(err) + return + } + userMaps := []maps.Map{} + for _, user := range usersResp.Users { + userMaps = append(userMaps, maps.Map{ + "id": user.Id, + "username": user.Username, + "isOn": user.IsOn, + "fullname": user.Fullname, + "email": user.Email, + "mobile": user.Mobile, + "tel": user.Tel, + "createdTime": timeutil.FormatTime("Y-m-d H:i:s", user.CreatedAt), + }) + } + this.Data["users"] = userMaps + + this.Show() +} diff --git a/internal/web/actions/default/users/init.go b/internal/web/actions/default/users/init.go new file mode 100644 index 00000000..53a4a83e --- /dev/null +++ b/internal/web/actions/default/users/init.go @@ -0,0 +1,22 @@ +package users + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeUser)). + Data("teaMenu", "users"). + Prefix("/users"). + Get("", new(IndexAction)). + GetPost("/createPopup", new(CreatePopupAction)). + Get("/user", new(UserAction)). + GetPost("/update", new(UpdateAction)). + Post("/delete", new(DeleteAction)). + EndAll() + }) +} diff --git a/internal/web/actions/default/users/update.go b/internal/web/actions/default/users/update.go new file mode 100644 index 00000000..aa40d5f8 --- /dev/null +++ b/internal/web/actions/default/users/update.go @@ -0,0 +1,128 @@ +package users + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users/userutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/maps" +) + +type UpdateAction struct { + actionutils.ParentAction +} + +func (this *UpdateAction) Init() { + this.Nav("", "", "update") +} + +func (this *UpdateAction) RunGet(params struct { + UserId int64 +}) { + err := userutils.InitUser(this.Parent(), params.UserId) + if err != nil { + this.ErrorPage(err) + return + } + + userResp, err := this.RPC().UserRPC().FindEnabledUser(this.AdminContext(), &pb.FindEnabledUserRequest{UserId: params.UserId}) + if err != nil { + this.ErrorPage(err) + return + } + user := userResp.User + if user == nil { + this.NotFound("user", params.UserId) + return + } + + this.Data["user"] = maps.Map{ + "id": user.Id, + "username": user.Username, + "fullname": user.Fullname, + "email": user.Email, + "tel": user.Tel, + "remark": user.Remark, + "mobile": user.Mobile, + "isOn": user.IsOn, + } + + this.Show() +} + +func (this *UpdateAction) RunPost(params struct { + UserId int64 + Username string + Pass1 string + Pass2 string + Fullname string + Mobile string + Tel string + Email string + Remark string + IsOn bool + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + defer this.CreateLogInfo("修改用户 %d", params.UserId) + + params.Must. + Field("username", params.Username). + Require("请输入用户名"). + Match(`^[a-zA-Z0-9_]+$`, "用户名中只能含有英文、数字和下划线") + + checkUsernameResp, err := this.RPC().UserRPC().CheckUsername(this.AdminContext(), &pb.CheckUsernameRequest{ + UserId: params.UserId, + Username: params.Username, + }) + if err != nil { + this.ErrorPage(err) + return + } + if checkUsernameResp.Exists { + this.FailField("username", "此用户名已经被占用,请换一个") + } + + if len(params.Pass1) > 0 { + params.Must. + Field("pass1", params.Pass1). + Require("请输入密码"). + Field("pass2", params.Pass2). + Require("请再次输入确认密码"). + Equal(params.Pass1, "两次输入的密码不一致") + } + + params.Must. + Field("fullname", params.Fullname). + Require("请输入全名") + + if len(params.Mobile) > 0 { + params.Must. + Field("mobile", params.Mobile). + Mobile("请输入正确的手机号") + } + if len(params.Email) > 0 { + params.Must. + Field("email", params.Email). + Email("请输入正确的电子邮箱") + } + + _, err = this.RPC().UserRPC().UpdateUser(this.AdminContext(), &pb.UpdateUserRequest{ + UserId: params.UserId, + Username: params.Username, + Password: params.Pass1, + Fullname: params.Fullname, + Mobile: params.Mobile, + Tel: params.Tel, + Email: params.Email, + Remark: params.Remark, + IsOn: params.IsOn, + }) + if err != nil { + this.ErrorPage(err) + return + } + + this.Success() +} diff --git a/internal/web/actions/default/users/user.go b/internal/web/actions/default/users/user.go new file mode 100644 index 00000000..9966b2aa --- /dev/null +++ b/internal/web/actions/default/users/user.go @@ -0,0 +1,50 @@ +package users + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users/userutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/maps" +) + +type UserAction struct { + actionutils.ParentAction +} + +func (this *UserAction) Init() { + this.Nav("", "", "index") +} + +func (this *UserAction) RunGet(params struct { + UserId int64 +}) { + err := userutils.InitUser(this.Parent(), params.UserId) + if err != nil { + this.ErrorPage(err) + return + } + + userResp, err := this.RPC().UserRPC().FindEnabledUser(this.AdminContext(), &pb.FindEnabledUserRequest{UserId: params.UserId}) + if err != nil { + this.ErrorPage(err) + return + } + user := userResp.User + if user == nil { + this.NotFound("user", params.UserId) + return + } + + this.Data["user"] = maps.Map{ + "id": user.Id, + "username": user.Username, + "fullname": user.Fullname, + "email": user.Email, + "tel": user.Tel, + "remark": user.Remark, + "mobile": user.Mobile, + "isOn": user.IsOn, + } + + this.Show() +} diff --git a/internal/web/actions/default/users/userutils/utils.go b/internal/web/actions/default/users/userutils/utils.go new file mode 100644 index 00000000..e66c9220 --- /dev/null +++ b/internal/web/actions/default/users/userutils/utils.go @@ -0,0 +1,25 @@ +package userutils + +import ( + "errors" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/maps" +) + +// 查找用户基本信息 +func InitUser(p *actionutils.ParentAction, userId int64) error { + resp, err := p.RPC().UserRPC().FindEnabledUser(p.AdminContext(), &pb.FindEnabledUserRequest{UserId: userId}) + if err != nil { + return err + } + if resp.User == nil { + return errors.New("not found user") + } + p.Data["user"] = maps.Map{ + "id": userId, + "fullname": resp.User.Fullname, + "username": resp.User.Username, + } + return nil +} diff --git a/internal/web/helpers/user_must_auth.go b/internal/web/helpers/user_must_auth.go index 18c16b3e..c56b2c81 100644 --- a/internal/web/helpers/user_must_auth.go +++ b/internal/web/helpers/user_must_auth.go @@ -215,11 +215,17 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map { }, }, }, + { + "code": "users", + "module": configloaders.AdminModuleCodeUser, + "name": "平台用户", + "icon": "users", + }, { "code": "admins", "module": configloaders.AdminModuleCodeAdmin, "name": "系统用户", - "icon": "users", + "icon": "user secret", }, { "code": "log", diff --git a/internal/web/import.go b/internal/web/import.go index 76d8e86b..0dce5748 100644 --- a/internal/web/import.go +++ b/internal/web/import.go @@ -83,4 +83,5 @@ import ( _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/upgrade" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ui" + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users" ) diff --git a/web/views/@default/users/@user_menu.html b/web/views/@default/users/@user_menu.html new file mode 100644 index 00000000..f43bfbf9 --- /dev/null +++ b/web/views/@default/users/@user_menu.html @@ -0,0 +1,6 @@ + + 用户列表 + | + {{user.fullname}}  ({{user.username}}) + 修改 + \ No newline at end of file diff --git a/web/views/@default/users/createPopup.html b/web/views/@default/users/createPopup.html new file mode 100644 index 00000000..1ed538c2 --- /dev/null +++ b/web/views/@default/users/createPopup.html @@ -0,0 +1,65 @@ +{$layout "layout_popup"} + +

创建用户

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
用户名 * + +

用户名只能是英文、数字、下划线的组合。

+
密码 * + +
确认密码 * + +
全名 * + +

用户姓名或者公司名称等等。

+
手机号 + +
联系电话 + +
电子邮箱 + +
备注 + +
+ +
\ No newline at end of file diff --git a/web/views/@default/users/index.html b/web/views/@default/users/index.html new file mode 100644 index 00000000..38f87d2e --- /dev/null +++ b/web/views/@default/users/index.html @@ -0,0 +1,37 @@ +{$layout} + + + 创建 + + +

暂时还没有用户。

+ + + + + + + + + + + + + + + + + + + +
用户名全名手机号注册时间状态操作
{{user.username}}{{user.fullname}} + {{user.mobile}} + - + {{user.createdTime}} + + + 详情   + 删除 +
+ +
\ No newline at end of file diff --git a/web/views/@default/users/index.js b/web/views/@default/users/index.js new file mode 100644 index 00000000..494be81e --- /dev/null +++ b/web/views/@default/users/index.js @@ -0,0 +1,22 @@ +Tea.context(function () { + this.createUser = function () { + teaweb.popup(Tea.url(".createPopup"), { + callback: function () { + teaweb.success("保存成功", function () { + teaweb.reload() + }) + } + }) + } + + this.deleteUser = function (userId) { + let that = this + teaweb.confirm("确定要删除这个用户吗?", function () { + that.$post(".delete") + .params({ + userId: userId + }) + .refresh() + }) + } +}) \ No newline at end of file diff --git a/web/views/@default/users/update.html b/web/views/@default/users/update.html new file mode 100644 index 00000000..07d6807a --- /dev/null +++ b/web/views/@default/users/update.html @@ -0,0 +1,78 @@ +{$layout} +{$template "user_menu"} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
用户名 * + +

用户名只能是英文、数字、下划线的组合。

+
密码 + + +
+ +

留空表示不修改。

+
+
确认密码 + +
全名 * + +

用户姓名或者公司名称等等。

+
手机号 + +
联系电话 + +
电子邮箱 + +
备注 + +
是否启用 + +
+ +
\ No newline at end of file diff --git a/web/views/@default/users/update.js b/web/views/@default/users/update.js new file mode 100644 index 00000000..b93c8e99 --- /dev/null +++ b/web/views/@default/users/update.js @@ -0,0 +1,9 @@ +Tea.context(function () { + this.success = NotifySuccess("保存成功", "/users/user?userId=" + this.user.id) + + this.passwordEditing = false + + this.changePasswordEditing = function () { + this.passwordEditing = !this.passwordEditing + } +}) \ No newline at end of file diff --git a/web/views/@default/users/user.html b/web/views/@default/users/user.html new file mode 100644 index 00000000..ccb78242 --- /dev/null +++ b/web/views/@default/users/user.html @@ -0,0 +1,51 @@ +{$layout} +{$template "user_menu"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态 + +
用户名 + {{user.username}} +
全名 + {{user.fullname}} +
手机号 + {{user.mobile}} + - +
联系电话 + {{user.tel}} + - +
电子邮箱 + {{user.email}} + - +
备注 + {{user.remark}} + - +
\ No newline at end of file