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());
+}