diff --git a/controllers/group_upload.go b/controllers/group_upload.go
new file mode 100644
index 00000000..fdb883a1
--- /dev/null
+++ b/controllers/group_upload.go
@@ -0,0 +1,56 @@
+// Copyright 2025 The Casdoor Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package controllers
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/casdoor/casdoor/object"
+ "github.com/casdoor/casdoor/util"
+)
+
+func (c *ApiController) UploadGroups() {
+ userId := c.GetSessionUsername()
+ owner, user := util.GetOwnerAndNameFromId(userId)
+
+ file, header, err := c.Ctx.Request.FormFile("file")
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
+ path := util.GetUploadXlsxPath(fileId)
+ defer os.Remove(path)
+
+ err = saveFile(path, &file)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ affected, err := object.UploadGroups(owner, path)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ if affected {
+ c.ResponseOk()
+ } else {
+ c.ResponseError(c.T("group_upload:Failed to import groups"))
+ }
+}
diff --git a/object/group.go b/object/group.go
index 22992433..2a125b80 100644
--- a/object/group.go
+++ b/object/group.go
@@ -181,6 +181,41 @@ func AddGroups(groups []*Group) (bool, error) {
return affected != 0, nil
}
+func AddGroupsInBatch(groups []*Group) (bool, error) {
+ if len(groups) == 0 {
+ return false, nil
+ }
+
+ session := ormer.Engine.NewSession()
+ defer session.Close()
+ err := session.Begin()
+ if err != nil {
+ return false, err
+ }
+
+ for _, group := range groups {
+ err = checkGroupName(group.Name)
+ if err != nil {
+ return false, err
+ }
+
+ affected, err := session.Insert(group)
+ if err != nil {
+ return false, err
+ }
+ if affected == 0 {
+ return false, nil
+ }
+ }
+
+ err = session.Commit()
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
func deleteGroup(group *Group) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {
diff --git a/object/group_upload.go b/object/group_upload.go
new file mode 100644
index 00000000..55bf385d
--- /dev/null
+++ b/object/group_upload.go
@@ -0,0 +1,61 @@
+// Copyright 2025 The Casdoor Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package object
+
+import (
+ "github.com/casdoor/casdoor/xlsx"
+)
+
+func getGroupMap(owner string) (map[string]*Group, error) {
+ m := map[string]*Group{}
+
+ groups, err := GetGroups(owner)
+ if err != nil {
+ return m, err
+ }
+
+ for _, group := range groups {
+ m[group.GetId()] = group
+ }
+
+ return m, nil
+}
+
+func UploadGroups(owner string, path string) (bool, error) {
+ table := xlsx.ReadXlsxFile(path)
+
+ oldGroupMap, err := getGroupMap(owner)
+ if err != nil {
+ return false, err
+ }
+
+ transGroups, err := StringArrayToStruct[Group](table)
+ if err != nil {
+ return false, err
+ }
+
+ newGroups := []*Group{}
+ for _, group := range transGroups {
+ if _, ok := oldGroupMap[group.GetId()]; !ok {
+ newGroups = append(newGroups, group)
+ }
+ }
+
+ if len(newGroups) == 0 {
+ return false, nil
+ }
+
+ return AddGroupsInBatch(newGroups)
+}
diff --git a/object/user_upload.go b/object/user_upload.go
index fcbcb074..c675c34b 100644
--- a/object/user_upload.go
+++ b/object/user_upload.go
@@ -81,7 +81,7 @@ func UploadUsers(owner string, path string) (bool, error) {
return false, err
}
- transUsers, err := StringArrayToUser(table)
+ transUsers, err := StringArrayToStruct[User](table)
if err != nil {
return false, err
}
diff --git a/object/user_util.go b/object/user_util.go
index 019ba7be..1b7f3bed 100644
--- a/object/user_util.go
+++ b/object/user_util.go
@@ -724,14 +724,14 @@ func setReflectAttr[T any](fieldValue *reflect.Value, fieldString string) error
return nil
}
-func StringArrayToUser(stringArray [][]string) ([]*User, error) {
+func StringArrayToStruct[T any](stringArray [][]string) ([]*T, error) {
fieldNames := stringArray[0]
excelMap := []map[string]string{}
- userFieldMap := map[string]int{}
+ structFieldMap := map[string]int{}
- reflectedUser := reflect.TypeOf(User{})
- for i := 0; i < reflectedUser.NumField(); i++ {
- userFieldMap[strings.ToLower(reflectedUser.Field(i).Name)] = i
+ reflectedStruct := reflect.TypeOf(*new(T))
+ for i := 0; i < reflectedStruct.NumField(); i++ {
+ structFieldMap[strings.ToLower(reflectedStruct.Field(i).Name)] = i
}
for idx, field := range stringArray {
@@ -746,22 +746,23 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) {
excelMap = append(excelMap, tempMap)
}
- users := []*User{}
+ instances := []*T{}
var err error
- for _, u := range excelMap {
- user := User{}
- reflectedUser := reflect.ValueOf(&user).Elem()
- for k, v := range u {
+ for _, m := range excelMap {
+ instance := new(T)
+ reflectedInstance := reflect.ValueOf(instance).Elem()
+
+ for k, v := range m {
if v == "" || v == "null" || v == "[]" || v == "{}" {
continue
}
fName := strings.ToLower(strings.ReplaceAll(k, "_", ""))
- fieldIdx, ok := userFieldMap[fName]
+ fieldIdx, ok := structFieldMap[fName]
if !ok {
continue
}
- fv := reflectedUser.Field(fieldIdx)
+ fv := reflectedInstance.Field(fieldIdx)
if !fv.IsValid() {
continue
}
@@ -806,8 +807,8 @@ func StringArrayToUser(stringArray [][]string) ([]*User, error) {
return nil, err
}
}
- users = append(users, &user)
+ instances = append(instances, instance)
}
- return users, nil
+ return instances, nil
}
diff --git a/routers/router.go b/routers/router.go
index 28dc427b..1fa219e8 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -81,6 +81,7 @@ func initAPI() {
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
+ beego.Router("/api/upload-groups", &controllers.ApiController{}, "POST:UploadGroups")
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")
diff --git a/web/src/GroupListPage.js b/web/src/GroupListPage.js
index 97c14455..02dea913 100644
--- a/web/src/GroupListPage.js
+++ b/web/src/GroupListPage.js
@@ -14,7 +14,8 @@
import React from "react";
import {Link} from "react-router-dom";
-import {Button, Table, Tooltip} from "antd";
+import {Button, Table, Tooltip, Upload} from "antd";
+import {UploadOutlined} from "@ant-design/icons";
import moment from "moment";
import * as Setting from "./Setting";
import * as GroupBackend from "./backend/GroupBackend";
@@ -87,6 +88,42 @@ class GroupListPage extends BaseListPage {
});
}
+ uploadFile(info) {
+ const {status, response: res} = info.file;
+ if (status === "done") {
+ if (res.status === "ok") {
+ Setting.showMessage("success", "Groups uploaded successfully, refreshing the page");
+ const {pagination} = this.state;
+ this.fetch({pagination});
+ } else {
+ Setting.showMessage("error", `Groups failed to upload: ${res.msg}`);
+ }
+ } else if (status === "error") {
+ Setting.showMessage("error", "File failed to upload");
+ }
+ }
+
+ renderUpload() {
+ const props = {
+ name: "file",
+ accept: ".xlsx",
+ method: "post",
+ action: `${Setting.ServerUrl}/api/upload-groups`,
+ withCredentials: true,
+ onChange: (info) => {
+ this.uploadFile(info);
+ },
+ };
+
+ return (
+