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 ( + + + + ); + } + renderTable(data) { const columns = [ { @@ -231,7 +268,10 @@ class GroupListPage extends BaseListPage { title={() => (
{i18next.t("general:Groups")}     - + + { + this.renderUpload() + }
)} loading={this.state.loading} diff --git a/xlsx/group_test.xlsx b/xlsx/group_test.xlsx new file mode 100644 index 00000000..7f4161b3 Binary files /dev/null and b/xlsx/group_test.xlsx differ