From 4b65320a96840b3e947c52f9ac8b2c9d96b8b610 Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Fri, 31 Dec 2021 12:56:19 +0800 Subject: [PATCH] Support user uploading via xlsx. --- controllers/user_upload.go | 60 ++++++++++++++++++ go.mod | 1 + go.sum | 2 + object/user_upload.go | 114 +++++++++++++++++++++++++++++++++++ routers/router.go | 1 + util/path.go | 19 ++++++ util/setting.go | 21 +++++++ web/src/UserListPage.js | 47 ++++++++++++++- web/src/locales/de/data.json | 1 + web/src/locales/en/data.json | 1 + web/src/locales/fr/data.json | 1 + web/src/locales/ja/data.json | 1 + web/src/locales/ko/data.json | 1 + web/src/locales/ru/data.json | 1 + web/src/locales/zh/data.json | 1 + xlsx/xlsx.go | 43 +++++++++++++ xlsx/xlsx_test.go | 22 +++++++ 17 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 controllers/user_upload.go create mode 100644 object/user_upload.go create mode 100644 util/setting.go create mode 100644 xlsx/xlsx.go create mode 100644 xlsx/xlsx_test.go diff --git a/controllers/user_upload.go b/controllers/user_upload.go new file mode 100644 index 00000000..d937e859 --- /dev/null +++ b/controllers/user_upload.go @@ -0,0 +1,60 @@ +// Copyright 2021 The casbin 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" + "io" + "mime/multipart" + "os" + + "github.com/casbin/casdoor/object" + "github.com/casbin/casdoor/util" +) + +func saveFile(path string, file *multipart.File) { + f, err := os.Create(path) + if err != nil { + panic(err) + } + defer f.Close() + + _, err = io.Copy(f, *file) + if err != nil { + panic(err) + } +} + +func (c *ApiController) UploadUsers() { + userId := c.GetSessionUsername() + owner, user := util.GetOwnerAndNameFromId(userId) + + file, header, err := c.Ctx.Request.FormFile("file") + if err != nil { + panic(err) + } + fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename)) + + path := util.GetUploadXlsxPath(fileId) + util.EnsureFileFolderExists(path) + saveFile(path, &file) + + affected := object.UploadUsers(owner, fileId) + if affected { + c.ResponseOk() + } else { + c.ResponseError("Failed to import users") + } +} diff --git a/go.mod b/go.mod index 2a61bb5c..63b6239b 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/russellhaering/goxmldsig v1.1.1 github.com/satori/go.uuid v1.2.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/tealeg/xlsx v1.0.5 github.com/thanhpk/randstr v1.0.4 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 diff --git a/go.sum b/go.sum index e0962233..ead49aa9 100644 --- a/go.sum +++ b/go.sum @@ -349,6 +349,8 @@ github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2K github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE= +github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM= github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL3sLrm+HJ3Dk+ye/lMCI= github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= diff --git a/object/user_upload.go b/object/user_upload.go new file mode 100644 index 00000000..91dca324 --- /dev/null +++ b/object/user_upload.go @@ -0,0 +1,114 @@ +// Copyright 2021 The casbin 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/casbin/casdoor/util" + "github.com/casbin/casdoor/xlsx" +) + +func getUserMap(owner string) map[string]*User { + m := map[string]*User{} + + users := GetUsers(owner) + for _, user := range users { + m[user.GetId()] = user + } + + return m +} + +func parseLineItem(line *[]string, i int) string { + if i >= len(*line) { + return "" + } else { + return (*line)[i] + } +} + +func parseLineItemInt(line *[]string, i int) int { + s := parseLineItem(line, i) + return util.ParseInt(s) +} + +func parseLineItemBool(line *[]string, i int) bool { + return parseLineItemInt(line, i) != 0 +} + +func UploadUsers(owner string, fileId string) bool { + table := xlsx.ReadXlsxFile(fileId) + + oldUserMap := getUserMap(owner) + newUsers := []*User{} + for _, line := range table { + if parseLineItem(&line, 0) == "" { + continue + } + + user := &User{ + Owner: parseLineItem(&line, 0), + Name: parseLineItem(&line, 1), + CreatedTime: parseLineItem(&line, 2), + UpdatedTime: parseLineItem(&line, 3), + Id: parseLineItem(&line, 4), + Type: parseLineItem(&line, 5), + Password: parseLineItem(&line, 6), + PasswordSalt: parseLineItem(&line, 7), + DisplayName: parseLineItem(&line, 8), + Avatar: parseLineItem(&line, 9), + PermanentAvatar: "", + Email: parseLineItem(&line, 10), + Phone: parseLineItem(&line, 11), + Location: parseLineItem(&line, 12), + Address: []string{parseLineItem(&line, 13)}, + Affiliation: parseLineItem(&line, 14), + Title: parseLineItem(&line, 15), + IdCardType: parseLineItem(&line, 16), + IdCard: parseLineItem(&line, 17), + Homepage: parseLineItem(&line, 18), + Bio: parseLineItem(&line, 19), + Tag: parseLineItem(&line, 20), + Region: parseLineItem(&line, 21), + Language: parseLineItem(&line, 22), + Gender: parseLineItem(&line, 23), + Birthday: parseLineItem(&line, 24), + Education: parseLineItem(&line, 25), + Score: parseLineItemInt(&line, 26), + Ranking: parseLineItemInt(&line, 27), + IsDefaultAvatar: false, + IsOnline: parseLineItemBool(&line, 28), + IsAdmin: parseLineItemBool(&line, 29), + IsGlobalAdmin: parseLineItemBool(&line, 30), + IsForbidden: parseLineItemBool(&line, 31), + IsDeleted: parseLineItemBool(&line, 32), + SignupApplication: parseLineItem(&line, 33), + Hash: "", + PreHash: "", + CreatedIp: parseLineItem(&line, 34), + LastSigninTime: parseLineItem(&line, 35), + LastSigninIp: parseLineItem(&line, 36), + Properties: map[string]string{}, + } + + if _, ok := oldUserMap[user.GetId()]; !ok { + newUsers = append(newUsers, user) + } + } + + if len(newUsers) == 0 { + return false + } + return AddUsersInBatch(newUsers) +} diff --git a/routers/router.go b/routers/router.go index d0cf384b..598c51c0 100644 --- a/routers/router.go +++ b/routers/router.go @@ -68,6 +68,7 @@ func initAPI() { beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser") beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser") beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") + beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers") beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") diff --git a/util/path.go b/util/path.go index ce3f83d8..eead5bfd 100644 --- a/util/path.go +++ b/util/path.go @@ -18,6 +18,7 @@ import ( "fmt" "net/url" "os" + "path/filepath" "strings" ) @@ -28,6 +29,24 @@ func FileExist(path string) bool { return true } +func GetPath(path string) string { + return filepath.Dir(path) +} + +func EnsureFileFolderExists(path string) { + p := GetPath(path) + if !FileExist(p) { + err := os.MkdirAll(p, os.ModePerm) + if err != nil { + panic(err) + } + } +} + +func RemoveExt(filename string) string { + return filename[:len(filename)-len(filepath.Ext(filename))] +} + func UrlJoin(base string, path string) string { res := fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(path, "/")) return res diff --git a/util/setting.go b/util/setting.go new file mode 100644 index 00000000..844be1f4 --- /dev/null +++ b/util/setting.go @@ -0,0 +1,21 @@ +// Copyright 2021 The casbin 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 util + +import "fmt" + +func GetUploadXlsxPath(fileId string) string { + return fmt.Sprintf("tmpFiles/%s.xlsx", fileId) +} diff --git a/web/src/UserListPage.js b/web/src/UserListPage.js index 2967a08b..725f9097 100644 --- a/web/src/UserListPage.js +++ b/web/src/UserListPage.js @@ -14,7 +14,8 @@ import React from "react"; import {Link} from "react-router-dom"; -import {Button, Popconfirm, Switch, Table} from 'antd'; +import {Button, Popconfirm, Switch, Table, Upload} from 'antd'; +import {UploadOutlined} from "@ant-design/icons"; import moment from "moment"; import * as Setting from "./Setting"; import * as UserBackend from "./backend/UserBackend"; @@ -92,6 +93,43 @@ class UserListPage extends BaseListPage { }); } + uploadFile(info) { + const { status, response: res } = info.file; + if (status === 'done') { + if (res.status === 'ok') { + Setting.showMessage("success", `Users uploaded successfully, refreshing the page`); + + const { pagination } = this.state; + this.fetch({ pagination }); + } else { + Setting.showMessage("error", `Users 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-users`, + withCredentials: true, + onChange: (info) => { + this.uploadFile(info); + }, + }; + + return ( + + + + ) + } + renderTable(users) { // transfer country code to name based on selected language var countries = require("i18n-iso-countries"); @@ -316,8 +354,11 @@ class UserListPage extends BaseListPage { (
- {i18next.t("general:Users")}     - + {i18next.t("general:Users")}     + + { + this.renderUpload() + }
)} loading={this.state.loading} diff --git a/web/src/locales/de/data.json b/web/src/locales/de/data.json index e8ce8f91..e3b1d8a4 100644 --- a/web/src/locales/de/data.json +++ b/web/src/locales/de/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "Title - Tooltip", "Two passwords you typed do not match.": "Two passwords you typed do not match.", "Unlink": "Unlink", + "Upload (.xlsx)": "Upload (.xlsx)", "Upload a photo": "Upload a photo", "input password": "input password" }, diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index a07dc955..06781490 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "Title - Tooltip", "Two passwords you typed do not match.": "Two passwords you typed do not match.", "Unlink": "Unlink", + "Upload (.xlsx)": "Upload (.xlsx)", "Upload a photo": "Upload a photo", "input password": "input password" }, diff --git a/web/src/locales/fr/data.json b/web/src/locales/fr/data.json index e8ce8f91..e3b1d8a4 100644 --- a/web/src/locales/fr/data.json +++ b/web/src/locales/fr/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "Title - Tooltip", "Two passwords you typed do not match.": "Two passwords you typed do not match.", "Unlink": "Unlink", + "Upload (.xlsx)": "Upload (.xlsx)", "Upload a photo": "Upload a photo", "input password": "input password" }, diff --git a/web/src/locales/ja/data.json b/web/src/locales/ja/data.json index e8ce8f91..e3b1d8a4 100644 --- a/web/src/locales/ja/data.json +++ b/web/src/locales/ja/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "Title - Tooltip", "Two passwords you typed do not match.": "Two passwords you typed do not match.", "Unlink": "Unlink", + "Upload (.xlsx)": "Upload (.xlsx)", "Upload a photo": "Upload a photo", "input password": "input password" }, diff --git a/web/src/locales/ko/data.json b/web/src/locales/ko/data.json index e8ce8f91..e3b1d8a4 100644 --- a/web/src/locales/ko/data.json +++ b/web/src/locales/ko/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "Title - Tooltip", "Two passwords you typed do not match.": "Two passwords you typed do not match.", "Unlink": "Unlink", + "Upload (.xlsx)": "Upload (.xlsx)", "Upload a photo": "Upload a photo", "input password": "input password" }, diff --git a/web/src/locales/ru/data.json b/web/src/locales/ru/data.json index e8ce8f91..e3b1d8a4 100644 --- a/web/src/locales/ru/data.json +++ b/web/src/locales/ru/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "Title - Tooltip", "Two passwords you typed do not match.": "Two passwords you typed do not match.", "Unlink": "Unlink", + "Upload (.xlsx)": "Upload (.xlsx)", "Upload a photo": "Upload a photo", "input password": "input password" }, diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index 3702ce11..53136d44 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -437,6 +437,7 @@ "Title - Tooltip": "在单位/公司的职务", "Two passwords you typed do not match.": "两次输入的密码不匹配。", "Unlink": "解绑", + "Upload (.xlsx)": "上传(.xlsx)", "Upload a photo": "上传头像", "input password": "输入密码" }, diff --git a/xlsx/xlsx.go b/xlsx/xlsx.go new file mode 100644 index 00000000..d15b9950 --- /dev/null +++ b/xlsx/xlsx.go @@ -0,0 +1,43 @@ +// Copyright 2021 The casbin 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 xlsx + +import ( + "github.com/casbin/casdoor/util" + "github.com/tealeg/xlsx" +) + +func ReadXlsxFile(fileId string) [][]string { + path := util.GetUploadXlsxPath(fileId) + file, err := xlsx.OpenFile(path) + if err != nil { + panic(err) + } + + res := [][]string{} + for _, sheet := range file.Sheets { + for _, row := range sheet.Rows { + line := []string{} + for _, cell := range row.Cells { + text := cell.String() + line = append(line, text) + } + res = append(res, line) + } + break + } + + return res +} diff --git a/xlsx/xlsx_test.go b/xlsx/xlsx_test.go new file mode 100644 index 00000000..5383b902 --- /dev/null +++ b/xlsx/xlsx_test.go @@ -0,0 +1,22 @@ +// Copyright 2021 The casbin 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 xlsx + +import "testing" + +func TestReadSheet(t *testing.T) { + ticket := ReadXlsxFile("../../tmpFiles/example") + println(ticket) +}