diff --git a/controllers/organization.go b/controllers/organization.go new file mode 100644 index 00000000..86f1fa9f --- /dev/null +++ b/controllers/organization.go @@ -0,0 +1,70 @@ +// Copyright 2020 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 ( + "encoding/json" + + "github.com/casdoor/casdoor/object" +) + +func (c *ApiController) GetOrganizations() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetOrganizations(owner) + c.ServeJSON() +} + +func (c *ApiController) GetOrganization() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetOrganization(id) + c.ServeJSON() +} + +func (c *ApiController) UpdateOrganization() { + id := c.Input().Get("id") + + var organization object.Organization + err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization) + if err != nil { + panic(err) + } + + c.Data["json"] = object.UpdateOrganization(id, &organization) + c.ServeJSON() +} + +func (c *ApiController) AddOrganization() { + var organization object.Organization + err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization) + if err != nil { + panic(err) + } + + c.Data["json"] = object.AddOrganization(&organization) + c.ServeJSON() +} + +func (c *ApiController) DeleteOrganization() { + var organization object.Organization + err := json.Unmarshal(c.Ctx.Input.RequestBody, &organization) + if err != nil { + panic(err) + } + + c.Data["json"] = object.DeleteOrganization(&organization) + c.ServeJSON() +} diff --git a/object/adapter.go b/object/adapter.go index dccece8d..541e1b6f 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -93,4 +93,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.engine.Sync2(new(Organization)) + if err != nil { + panic(err) + } } diff --git a/object/organization.go b/object/organization.go new file mode 100644 index 00000000..3e3a274d --- /dev/null +++ b/object/organization.go @@ -0,0 +1,92 @@ +// Copyright 2020 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/casdoor/casdoor/util" + "xorm.io/core" +) + +type Organization struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + + DisplayName string `xorm:"varchar(100)" json:"displayName"` + WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"` + Members []string `xorm:"varchar(100)" json:"members"` +} + +func GetOrganizations(owner string) []*Organization { + organizations := []*Organization{} + err := adapter.engine.Desc("created_time").Find(&organizations, &Organization{Owner: owner}) + if err != nil { + panic(err) + } + + return organizations +} + +func getOrganization(owner string, name string) *Organization { + organization := Organization{Owner: owner, Name: name} + existed, err := adapter.engine.Get(&organization) + if err != nil { + panic(err) + } + + if existed { + return &organization + } else { + return nil + } +} + +func GetOrganization(id string) *Organization { + owner, name := util.GetOwnerAndNameFromId(id) + return getOrganization(owner, name) +} + +func UpdateOrganization(id string, organization *Organization) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getOrganization(owner, name) == nil { + return false + } + + _, err := adapter.engine.Id(core.PK{owner, name}).AllCols().Update(organization) + if err != nil { + panic(err) + } + + //return affected != 0 + return true +} + +func AddOrganization(organization *Organization) bool { + affected, err := adapter.engine.Insert(organization) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteOrganization(organization *Organization) bool { + affected, err := adapter.engine.Id(core.PK{organization.Owner, organization.Name}).Delete(&Organization{}) + if err != nil { + panic(err) + } + + return affected != 0 +} diff --git a/routers/router.go b/routers/router.go index 91647a76..74ea5e50 100644 --- a/routers/router.go +++ b/routers/router.go @@ -38,4 +38,10 @@ 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/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations") + beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization") + beego.Router("/api/update-organization", &controllers.ApiController{}, "POST:UpdateOrganization") + beego.Router("/api/add-organization", &controllers.ApiController{}, "POST:AddOrganization") + beego.Router("/api/delete-organization", &controllers.ApiController{}, "POST:DeleteOrganization") } diff --git a/web/src/App.js b/web/src/App.js index 3c87d9d3..853de942 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -19,6 +19,8 @@ import {DownOutlined, LogoutOutlined, SettingOutlined} from '@ant-design/icons'; import {Avatar, BackTop, Dropdown, Layout, Menu} from 'antd'; import {Switch, Route, withRouter, Redirect} from 'react-router-dom' import * as AccountBackend from "./backend/AccountBackend"; +import OrganizationListPage from "./OrganizationListPage"; +import OrganizationEditPage from "./OrganizationEditPage"; import UserListPage from "./UserListPage"; import UserEditPage from "./UserEditPage"; @@ -46,8 +48,10 @@ class App extends Component { const uri = location.pathname; if (uri === '/') { this.setState({ selectedMenuKey: 0 }); - } else if (uri.includes('users')) { + } else if (uri.includes('organizations')) { this.setState({ selectedMenuKey: 1 }); + } else if (uri.includes('users')) { + this.setState({ selectedMenuKey: 2 }); } else { this.setState({ selectedMenuKey: -1 }); } @@ -189,6 +193,13 @@ class App extends Component { ); res.push( + + Organizations + + + ); + res.push( + Users @@ -245,6 +256,8 @@ class App extends Component { + + diff --git a/web/src/OrganizationEditPage.js b/web/src/OrganizationEditPage.js new file mode 100644 index 00000000..42f81905 --- /dev/null +++ b/web/src/OrganizationEditPage.js @@ -0,0 +1,137 @@ +import React from "react"; +import {Button, Card, Col, Input, Row} from 'antd'; +import {LinkOutlined} from "@ant-design/icons"; +import * as OrganizationBackend from "./backend/OrganizationBackend"; +import * as Setting from "./Setting"; + +class OrganizationEditPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + organizationName: props.match.params.organizationName, + organization: null, + tasks: [], + resources: [], + }; + } + + componentWillMount() { + this.getOrganization(); + } + + getOrganization() { + OrganizationBackend.getOrganization("admin", this.state.organizationName) + .then((organization) => { + this.setState({ + organization: organization, + }); + }); + } + + parseOrganizationField(key, value) { + // if ([].includes(key)) { + // value = Setting.myParseInt(value); + // } + return value; + } + + updateOrganizationField(key, value) { + value = this.parseOrganizationField(key, value); + + let organization = this.state.organization; + organization[key] = value; + this.setState({ + organization: organization, + }); + } + + renderOrganization() { + return ( + + Edit Organization     + + + } style={{marginLeft: '5px'}} type="inner"> + + + Name: + + + { + this.updateOrganizationField('name', e.target.value); + }} /> + + + + + Display Name: + + + { + this.updateOrganizationField('displayName', e.target.value); + }} /> + + + + + Website Url: + + + { + this.updateOrganizationField('websiteUrl', e.target.value); + }} /> + + + + ) + } + + submitOrganizationEdit() { + let organization = Setting.deepCopy(this.state.organization); + OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization) + .then((res) => { + if (res) { + Setting.showMessage("success", `Successfully saved`); + this.setState({ + organizationName: this.state.organization.name, + }); + this.props.history.push(`/organizations/${this.state.organization.name}`); + } else { + Setting.showMessage("error", `failed to save: server side failure`); + this.updateOrganizationField('name', this.state.organizationName); + } + }) + .catch(error => { + Setting.showMessage("error", `failed to save: ${error}`); + }); + } + + render() { + return ( +
+ + + + + { + this.state.organization !== null ? this.renderOrganization() : null + } + + + + + + + + + + + +
+ ); + } +} + +export default OrganizationEditPage; diff --git a/web/src/OrganizationListPage.js b/web/src/OrganizationListPage.js new file mode 100644 index 00000000..85eb8ee4 --- /dev/null +++ b/web/src/OrganizationListPage.js @@ -0,0 +1,161 @@ +import React from "react"; +import {Button, Col, Popconfirm, Row, Table} from 'antd'; +import moment from "moment"; +import * as Setting from "./Setting"; +import * as OrganizationBackend from "./backend/OrganizationBackend"; + +class OrganizationListPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + organizations: null, + }; + } + + componentWillMount() { + this.getOrganizations(); + } + + getOrganizations() { + OrganizationBackend.getOrganizations("admin") + .then((res) => { + this.setState({ + organizations: res, + }); + }); + } + + newOrganization() { + return { + owner: "admin", // this.props.account.organizationname, + name: `organization_${this.state.organizations.length}`, + createdTime: moment().format(), + displayName: `New Organization - ${this.state.organizations.length}`, + websiteUrl: "https://example.com", + } + } + + addOrganization() { + const newOrganization = this.newOrganization(); + OrganizationBackend.addOrganization(newOrganization) + .then((res) => { + Setting.showMessage("success", `Organization added successfully`); + this.setState({ + organizations: Setting.prependRow(this.state.organizations, newOrganization), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Organization failed to add: ${error}`); + }); + } + + deleteOrganization(i) { + OrganizationBackend.deleteOrganization(this.state.organizations[i]) + .then((res) => { + Setting.showMessage("success", `Organization deleted successfully`); + this.setState({ + organizations: Setting.deleteRow(this.state.organizations, i), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Organization failed to delete: ${error}`); + }); + } + + renderTable(organizations) { + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + width: '120px', + sorter: (a, b) => a.name.localeCompare(b.name), + render: (text, record, index) => { + return ( + {text} + ) + } + }, + { + title: 'Created Time', + dataIndex: 'createdTime', + key: 'createdTime', + width: '160px', + sorter: (a, b) => a.createdTime.localeCompare(b.createdTime), + render: (text, record, index) => { + return Setting.getFormattedDate(text); + } + }, + { + title: 'Display Name', + dataIndex: 'displayName', + key: 'displayName', + // width: '100px', + sorter: (a, b) => a.displayName.localeCompare(b.displayName), + }, + { + title: 'Website Url', + dataIndex: 'websiteUrl', + key: 'websiteUrl', + width: '300px', + sorter: (a, b) => a.websiteUrl.localeCompare(b.websiteUrl), + }, + { + title: 'Action', + dataIndex: '', + key: 'op', + width: '170px', + render: (text, record, index) => { + return ( +
+ + this.deleteOrganization(index)} + > + + +
+ ) + } + }, + ]; + + return ( +
+ ( +
+ Organizations     + +
+ )} + loading={organizations === null} + /> + + ); + } + + render() { + return ( +
+ +
+ + + { + this.renderTable(this.state.organizations) + } + + + + + + ); + } +} + +export default OrganizationListPage; diff --git a/web/src/backend/OrganizationBackend.js b/web/src/backend/OrganizationBackend.js new file mode 100644 index 00000000..693052fd --- /dev/null +++ b/web/src/backend/OrganizationBackend.js @@ -0,0 +1,42 @@ +import * as Setting from "../Setting"; + +export function getOrganizations(owner) { + return fetch(`${Setting.ServerUrl}/api/get-organizations?owner=${owner}`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function getOrganization(owner, name) { + return fetch(`${Setting.ServerUrl}/api/get-organization?id=${owner}/${encodeURIComponent(name)}`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function updateOrganization(owner, name, organization) { + let newOrganization = Setting.deepCopy(organization); + return fetch(`${Setting.ServerUrl}/api/update-organization?id=${owner}/${encodeURIComponent(name)}`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newOrganization), + }).then(res => res.json()); +} + +export function addOrganization(organization) { + let newOrganization = Setting.deepCopy(organization); + return fetch(`${Setting.ServerUrl}/api/add-organization`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newOrganization), + }).then(res => res.json()); +} + +export function deleteOrganization(organization) { + let newOrganization = Setting.deepCopy(organization); + return fetch(`${Setting.ServerUrl}/api/delete-organization`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newOrganization), + }).then(res => res.json()); +}