diff --git a/controllers/message.go b/controllers/message.go
new file mode 100644
index 0000000..33e220d
--- /dev/null
+++ b/controllers/message.go
@@ -0,0 +1,108 @@
+// Copyright 2023 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/casbin/casibase/object"
+)
+
+func (c *ApiController) GetGlobalMessages() {
+ messages, err := object.GetGlobalMessages()
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(messages)
+}
+
+func (c *ApiController) GetMessages() {
+ owner := c.Input().Get("owner")
+
+ messages, err := object.GetMessages(owner)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(messages)
+}
+
+func (c *ApiController) GetMessage() {
+ id := c.Input().Get("id")
+
+ message, err := object.GetMessage(id)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(message)
+}
+
+func (c *ApiController) UpdateMessage() {
+ id := c.Input().Get("id")
+
+ var message object.Message
+ err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ success, err := object.UpdateMessage(id, &message)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(success)
+}
+
+func (c *ApiController) AddMessage() {
+ var message object.Message
+ err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ success, err := object.AddMessage(&message)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(success)
+}
+
+func (c *ApiController) DeleteMessage() {
+ var message object.Message
+ err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ success, err := object.DeleteMessage(&message)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(success)
+}
diff --git a/object/adapter.go b/object/adapter.go
index 95066fa..132e7ab 100644
--- a/object/adapter.go
+++ b/object/adapter.go
@@ -132,4 +132,9 @@ func (a *Adapter) createTable() {
if err != nil {
panic(err)
}
+
+ err = a.engine.Sync2(new(Message))
+ if err != nil {
+ panic(err)
+ }
}
diff --git a/object/message.go b/object/message.go
new file mode 100644
index 0000000..333c05b
--- /dev/null
+++ b/object/message.go
@@ -0,0 +1,113 @@
+// Copyright 2023 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 (
+ "fmt"
+
+ "github.com/casbin/casibase/util"
+ "xorm.io/core"
+)
+
+type Message 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"`
+
+ //Organization string `xorm:"varchar(100)" json:"organization"`
+ Chat string `xorm:"varchar(100) index" json:"chat"`
+ ReplyTo string `xorm:"varchar(100) index" json:"replyTo"`
+ Author string `xorm:"varchar(100)" json:"author"`
+ Text string `xorm:"mediumtext" json:"text"`
+}
+
+func GetGlobalMessages() ([]*Message, error) {
+ messages := []*Message{}
+ err := adapter.engine.Asc("owner").Desc("created_time").Find(&messages)
+ if err != nil {
+ return messages, err
+ }
+
+ return messages, nil
+}
+
+func GetMessages(owner string) ([]*Message, error) {
+ messages := []*Message{}
+ err := adapter.engine.Desc("created_time").Find(&messages, &Message{Owner: owner})
+ if err != nil {
+ return messages, err
+ }
+
+ return messages, nil
+}
+
+func getMessage(owner, name string) (*Message, error) {
+ message := Message{Owner: owner, Name: name}
+ existed, err := adapter.engine.Get(&message)
+ if err != nil {
+ return &message, err
+ }
+
+ if existed {
+ return &message, nil
+ } else {
+ return nil, nil
+ }
+}
+
+func GetMessage(id string) (*Message, error) {
+ owner, name := util.GetOwnerAndNameFromId(id)
+ return getMessage(owner, name)
+}
+
+func UpdateMessage(id string, message *Message) (bool, error) {
+ owner, name := util.GetOwnerAndNameFromId(id)
+ _, err := getMessage(owner, name)
+ if err != nil {
+ return false, err
+ }
+ if message == nil {
+ return false, nil
+ }
+
+ _, err = adapter.engine.ID(core.PK{owner, name}).AllCols().Update(message)
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
+func AddMessage(message *Message) (bool, error) {
+ affected, err := adapter.engine.Insert(message)
+ if err != nil {
+ return false, err
+ }
+
+ return affected != 0, nil
+}
+
+func DeleteMessage(message *Message) (bool, error) {
+ affected, err := adapter.engine.ID(core.PK{message.Owner, message.Name}).Delete(&Message{})
+ if err != nil {
+ return false, err
+ }
+
+ return affected != 0, nil
+}
+
+func (message *Message) GetId() string {
+ return fmt.Sprintf("%s/%s", message.Owner, message.Name)
+}
diff --git a/routers/router.go b/routers/router.go
index f8a3f57..c6452fa 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -81,6 +81,13 @@ func initAPI() {
beego.Router("/api/add-chat", &controllers.ApiController{}, "POST:AddChat")
beego.Router("/api/delete-chat", &controllers.ApiController{}, "POST:DeleteChat")
+ beego.Router("/api/get-global-messages", &controllers.ApiController{}, "GET:GetGlobalMessages")
+ beego.Router("/api/get-messages", &controllers.ApiController{}, "GET:GetMessages")
+ beego.Router("/api/get-message", &controllers.ApiController{}, "GET:GetMessage")
+ beego.Router("/api/update-message", &controllers.ApiController{}, "POST:UpdateMessage")
+ beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage")
+ beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage")
+
beego.Router("/api/update-file", &controllers.ApiController{}, "POST:UpdateFile")
beego.Router("/api/add-file", &controllers.ApiController{}, "POST:AddFile")
beego.Router("/api/delete-file", &controllers.ApiController{}, "POST:DeleteFile")
diff --git a/web/src/App.js b/web/src/App.js
index 834576a..c3af26a 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -40,6 +40,8 @@ import i18next from "i18next";
import LanguageSelect from "./LanguageSelect";
import ChatEditPage from "./ChatEditPage";
import ChatListPage from "./ChatListPage";
+import MessageListPage from "./MessageListPage";
+import MessageEditPage from "./MessageEditPage";
const {Header, Footer} = Layout;
@@ -92,6 +94,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/providers"});
} else if (uri.includes("/chats")) {
this.setState({selectedMenuKey: "/chats"});
+ } else if (uri.includes("/messages")) {
+ this.setState({selectedMenuKey: "/messages"});
} else {
this.setState({selectedMenuKey: "null"});
}
@@ -331,6 +335,13 @@ class App extends Component {
);
+ res.push(
+
+
+ {i18next.t("general:Messages")}
+
+
+ );
res.push(
@@ -418,6 +429,8 @@ class App extends Component {
this.renderSigninIfNotSignedIn()} />
this.renderSigninIfNotSignedIn()} />
this.renderSigninIfNotSignedIn()} />
+ this.renderSigninIfNotSignedIn()} />
+ this.renderSigninIfNotSignedIn()} />
);
diff --git a/web/src/ChatEditPage.js b/web/src/ChatEditPage.js
index f807b70..b0cf313 100644
--- a/web/src/ChatEditPage.js
+++ b/web/src/ChatEditPage.js
@@ -27,11 +27,13 @@ class ChatEditPage extends React.Component {
classes: props,
chatName: props.match.params.chatName,
chat: null,
+ // users: [],
};
}
UNSAFE_componentWillMount() {
this.getChat();
+ // this.getUser();
}
getChat() {
diff --git a/web/src/MessageEditPage.js b/web/src/MessageEditPage.js
new file mode 100644
index 0000000..4409b67
--- /dev/null
+++ b/web/src/MessageEditPage.js
@@ -0,0 +1,277 @@
+// Copyright 2023 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.
+
+import React from "react";
+import {Button, Card, Col, Input, Row, Select} from "antd";
+import * as Setting from "./Setting";
+import i18next from "i18next";
+import * as MessageBackend from "./backend/MessageBackend";
+import TextArea from "antd/es/input/TextArea";
+import * as ChatBackend from "./backend/ChatBackend";
+
+class MessageEditPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ classes: props,
+ messageName: props.match.params.messageName,
+ messages: [],
+ message: null,
+ chats: [],
+ // users: [],
+ chat: null,
+ };
+ }
+
+ UNSAFE_componentWillMount() {
+ this.getMessage();
+ this.getMessages();
+ this.getChats();
+ }
+
+ getChats() {
+ ChatBackend.getChats(this.props.account.name)
+ .then((chats) => {
+ if (chats.status === "ok") {
+ this.setState({
+ chats: chats.data,
+ });
+ } else {
+ Setting.showMessage("error", `Failed to get chat: ${chats.msg}`);
+ }
+ });
+ }
+
+ getChat(chatName) {
+ ChatBackend.getChat(this.props.account.name, chatName)
+ .then((chat) => {
+ if (chat.status === "ok") {
+ this.setState({
+ chat: chat.data,
+ });
+ } else {
+ Setting.showMessage("error", `Failed to get chat: ${chat.msg}`);
+ }
+ });
+ }
+
+ getMessage() {
+ MessageBackend.getMessage(this.props.account.name, this.state.messageName)
+ .then((message) => {
+ if (message.status === "ok") {
+ this.setState({
+ message: message.data,
+ });
+ } else {
+ Setting.showMessage("error", `Failed to get message: ${message.msg}`);
+ }
+ });
+ }
+
+ getMessages() {
+ MessageBackend.getMessages(this.props.account.name)
+ .then((messages) => {
+ if (messages.status === "ok") {
+ this.setState({
+ messages: messages.data,
+ });
+ } else {
+ Setting.showMessage("error", `Failed to get messages: ${messages.msg}`);
+ }
+ });
+ }
+
+ parseMessageField(key, value) {
+ if ([""].includes(key)) {
+ value = Setting.myParseInt(value);
+ }
+ return value;
+ }
+
+ updateMessageField(key, value) {
+ value = this.parseMessageField(key, value);
+
+ const message = this.state.message;
+ message[key] = value;
+ this.setState({
+ message: message,
+ });
+ }
+
+ renderMessage() {
+ return (
+
+ {i18next.t("message:Edit Chat")}
+
+
+ } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
+ {/* */}
+ {/* */}
+ {/* {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :*/}
+ {/* */}
+ {/* */}
+ {/*
*/}
+
+
+ {i18next.t("general:Name")}:
+
+
+ {
+ this.updateMessageField("name", e.target.value);
+ }}
+ />
+
+
+
+
+ {i18next.t("message:Chat")}:
+
+
+
+
+
+ {i18next.t("message:Author")}:
+
+
+
+
+
+ {i18next.t("message:replyTo")}:
+
+
+
+
+
+ {i18next.t("message:Text")}:
+
+
+
+
+ );
+ }
+
+ submitMessageEdit(exitAfterSave) {
+ const message = Setting.deepCopy(this.state.message);
+ MessageBackend.updateMessage(this.state.message.owner, this.state.messageName, message)
+ .then((res) => {
+ if (res.status === "ok") {
+ if (res.data) {
+ Setting.showMessage("success", "Successfully saved");
+ this.setState({
+ messageName: this.state.message.name,
+ });
+ if (exitAfterSave) {
+ this.props.history.push(`/messages/${this.state.message.name}`);
+ }
+ } else {
+ Setting.showMessage("error", "failed to save: server side failure");
+ this.updateMessageField("name", this.state.messageName);
+ }
+ } else {
+ Setting.showMessage("error", `failed to save: ${res.msg}`);
+ }
+ })
+ .catch((error) => {
+ Setting.showMessage("error", `failed to save: ${error}`);
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ {this.state.message !== null ? this.renderMessage() : null}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default MessageEditPage;
diff --git a/web/src/MessageListPage.js b/web/src/MessageListPage.js
new file mode 100644
index 0000000..df93974
--- /dev/null
+++ b/web/src/MessageListPage.js
@@ -0,0 +1,246 @@
+// Copyright 2023 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.
+
+import React from "react";
+import {Link} from "react-router-dom";
+import {Button, Col, Popconfirm, Row, Table} from "antd";
+import * as Setting from "./Setting";
+import * as MessageBackend from "./backend/MessageBackend";
+import moment from "moment";
+import i18next from "i18next";
+
+class MessageListPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ classes: props,
+ messages: null,
+ };
+ }
+
+ UNSAFE_componentWillMount() {
+ this.getMessages();
+ }
+
+ getMessages() {
+ MessageBackend.getMessages(this.props.account.name)
+ .then((res) => {
+ if (res.status === "ok") {
+ this.setState({
+ messages: res.data,
+ });
+ } else {
+ Setting.showMessage("error", `Failed to get messages: ${res.msg}`);
+ }
+ });
+ }
+
+ newMessage() {
+ const randomName = Setting.getRandomName();
+ return {
+ owner: this.props.account.name,
+ name: `message_${randomName}`,
+ createdTime: moment().format(),
+ // organization: "Message Organization - 1",
+ chat: "",
+ replyTo: "",
+ author: `${this.props.account.owner}/${this.props.account.name}`,
+ text: "",
+ };
+ }
+
+ addMessage() {
+ const newMessage = this.newMessage();
+ MessageBackend.addMessage(newMessage)
+ .then((res) => {
+ if (res.status === "ok") {
+ Setting.showMessage("success", "Message added successfully");
+ this.setState({
+ messages: Setting.prependRow(this.state.messages, newMessage),
+ });
+ } else {
+ Setting.showMessage("error", `Failed to add Message: ${res.msg}`);
+ }
+ })
+ .catch(error => {
+ Setting.showMessage("error", `Message failed to add: ${error}`);
+ });
+ }
+
+ deleteMessage(i) {
+ MessageBackend.deleteMessage(this.state.messages[i])
+ .then((res) => {
+ if (res.status === "ok") {
+ Setting.showMessage("success", "Message deleted successfully");
+ this.setState({
+ messages: Setting.deleteRow(this.state.messages, i),
+ });
+ } else {
+ Setting.showMessage("error", `Failed to delete Message: ${res.msg}`);
+ }
+ })
+ .catch(error => {
+ Setting.showMessage("error", `Message failed to delete: ${error}`);
+ });
+ }
+
+ renderTable(messages) {
+ const columns = [
+ {
+ title: i18next.t("general:Name"),
+ dataIndex: "name",
+ key: "name",
+ width: "140px",
+ sorter: (a, b) => a.name.localeCompare(b.name),
+ render: (text, record, index) => {
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ {
+ title: i18next.t("general:Created time"),
+ dataIndex: "createdTime",
+ key: "createdTime",
+ width: "150px",
+ sorter: true,
+ render: (text, record, index) => {
+ return Setting.getFormattedDate(text);
+ },
+ },
+ {
+ title: i18next.t("message:Chat"),
+ dataIndex: "chat",
+ key: "chat",
+ width: "150px",
+ sorter: (a, b) => a.chat.localeCompare(b.chat),
+ render: (text, record, index) => {
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ {
+ title: i18next.t("message:Reply to"),
+ dataIndex: "replyTo",
+ key: "replyTo",
+ width: "150px",
+ sorter: (a, b) => a.replyTo.localeCompare(b.replyTo),
+ render: (text, record, index) => {
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ {
+ title: i18next.t("message:Author"),
+ dataIndex: "author",
+ key: "author",
+ width: "150px",
+ sorter: (a, b) => a.author.localeCompare(b.author),
+ render: (text, record, index) => {
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ {
+ title: i18next.t("message:Text"),
+ dataIndex: "text",
+ key: "text",
+ width: "200px",
+ sorter: (a, b) => a.text.localeCompare(b.text),
+ },
+ {
+ title: i18next.t("general:Action"),
+ dataIndex: "action",
+ key: "action",
+ width: "130px",
+ render: (text, record, index) => {
+ return (
+
+
+
this.deleteMessage(index)}
+ okText={i18next.t("general:OK")}
+ cancelText={i18next.t("general:Cancel")}
+ >
+
+
+
+ );
+ },
+ },
+ ];
+
+ return (
+
+
(
+
+ {i18next.t("message:Messages")}
+
+
+ )}
+ loading={messages === null}
+ />
+
+ );
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ {
+ this.renderTable(this.state.messages)
+ }
+
+
+
+
+
+ );
+ }
+}
+
+export default MessageListPage;
diff --git a/web/src/backend/MessageBackend.js b/web/src/backend/MessageBackend.js
new file mode 100644
index 0000000..abdbab9
--- /dev/null
+++ b/web/src/backend/MessageBackend.js
@@ -0,0 +1,63 @@
+// Copyright 2023 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.
+
+import * as Setting from "../Setting";
+
+export function getGlobalMessages() {
+ return fetch(`${Setting.ServerUrl}/api/get-global-messages`, {
+ method: "GET",
+ credentials: "include",
+ }).then(res => res.json());
+}
+
+export function getMessages(owner) {
+ return fetch(`${Setting.ServerUrl}/api/get-messages?owner=${owner}`, {
+ method: "GET",
+ credentials: "include",
+ }).then(res => res.json());
+}
+
+export function getMessage(owner, name) {
+ return fetch(`${Setting.ServerUrl}/api/get-message?id=${owner}/${encodeURIComponent(name)}`, {
+ method: "GET",
+ credentials: "include",
+ }).then(res => res.json());
+}
+
+export function updateMessage(owner, name, message) {
+ const newMessage = Setting.deepCopy(message);
+ return fetch(`${Setting.ServerUrl}/api/update-message?id=${owner}/${encodeURIComponent(name)}`, {
+ method: "POST",
+ credentials: "include",
+ body: JSON.stringify(newMessage),
+ }).then(res => res.json());
+}
+
+export function addMessage(message) {
+ const newMessage = Setting.deepCopy(message);
+ return fetch(`${Setting.ServerUrl}/api/add-message`, {
+ method: "POST",
+ credentials: "include",
+ body: JSON.stringify(newMessage),
+ }).then(res => res.json());
+}
+
+export function deleteMessage(message) {
+ const newMessage = Setting.deepCopy(message);
+ return fetch(`${Setting.ServerUrl}/api/delete-message`, {
+ method: "POST",
+ credentials: "include",
+ body: JSON.stringify(newMessage),
+ }).then(res => res.json());
+}