From 7dda7fbcb2d283f44fee9cf892a92b34eee53e74 Mon Sep 17 00:00:00 2001 From: hippoz Date: Sat, 21 Aug 2021 21:30:02 +0300 Subject: [PATCH] Hosting improvements: more config options (policies to restrict certain actions, improved CORS documentation and default), no more default frontend, improved defaults, ... --- brainlet/api/v1/content.js | 4 + brainlet/api/v1/index.js | 3 +- brainlet/api/v1/users.js | 23 +- brainlet/api/v2/gateway/index.js | 2 + brainlet/app/app.html | 207 ---------- brainlet/app/auth.html | 84 ---- brainlet/app/gatewaytest/app.js | 210 ---------- brainlet/app/gatewaytest/index.html | 45 --- brainlet/app/gatewaytest/style.css | 121 ------ brainlet/app/index.html | 24 ++ brainlet/app/resources/css/base.css | 0 brainlet/app/resources/js/app.js | 577 ---------------------------- brainlet/app/resources/js/auth.js | 265 ------------- brainlet/config.js | 51 ++- 14 files changed, 69 insertions(+), 1547 deletions(-) delete mode 100755 brainlet/app/app.html delete mode 100755 brainlet/app/auth.html delete mode 100644 brainlet/app/gatewaytest/app.js delete mode 100644 brainlet/app/gatewaytest/index.html delete mode 100644 brainlet/app/gatewaytest/style.css create mode 100644 brainlet/app/index.html delete mode 100755 brainlet/app/resources/css/base.css delete mode 100755 brainlet/app/resources/js/app.js delete mode 100755 brainlet/app/resources/js/auth.js diff --git a/brainlet/api/v1/content.js b/brainlet/api/v1/content.js index e471753..1a01e4a 100755 --- a/brainlet/api/v1/content.js +++ b/brainlet/api/v1/content.js @@ -21,6 +21,8 @@ app.post("/channel/create", [ createLimiter, body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape() ], authenticateEndpoint(async (req, res, user) => { + if (!config.policies.allowChannelCreation) return res.status(403).json({ error: true, message: "ERROR_FORBIDDEN_BY_POLICY" }); + const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); @@ -47,6 +49,8 @@ app.post("/post/create", [ body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(), body("body").not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(), ], authenticateEndpoint(async (req, res, user) => { + if (!config.policies.allowPostCreation) return res.status(403).json({ error: true, message: "ERROR_FORBIDDEN_BY_POLICY" }); + const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); diff --git a/brainlet/api/v1/index.js b/brainlet/api/v1/index.js index d81acae..14f7990 100755 --- a/brainlet/api/v1/index.js +++ b/brainlet/api/v1/index.js @@ -9,8 +9,7 @@ app.use("/users", usersAPI); app.use("/content", contentAPI); app.get("/", (req, res) => { - // TODO: Add more checks for this, or maybe remove - res.json({ apiStatus: "OK", apis: [ "users", "content" ] }); + res.json({ error: false }); }); module.exports = app; \ No newline at end of file diff --git a/brainlet/api/v1/users.js b/brainlet/api/v1/users.js index fb7fb9d..75b7823 100755 --- a/brainlet/api/v1/users.js +++ b/brainlet/api/v1/users.js @@ -46,6 +46,8 @@ app.post("/account/create", [ body("password").not().isEmpty().isLength({ min: 8, max: 128 }), body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric() ], async (req, res) => { + if (!config.policies.allowAccountCreation) return res.status(403).json({ error: true, message: "ERROR_FORBIDDEN_BY_POLICY" }); + try { const errors = validationResult(req); if (!errors.isEmpty()) { @@ -114,6 +116,8 @@ app.post("/token/create", [ body("username").not().isEmpty().trim().isAlphanumeric(), body("password").not().isEmpty() ], async (req, res) => { + if (!config.policies.allowLogin) return res.status(403).json({ error: true, message: "ERROR_FORBIDDEN_BY_POLICY" }); + const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" }); @@ -141,7 +145,7 @@ app.post("/token/create", [ return; } - jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: "3h" }, async (err, token) => { + jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: config.tokenExpiresIn }, async (err, token) => { if (err) { res.status(500).json({ error: true, @@ -150,13 +154,6 @@ app.post("/token/create", [ return; } - // TODO: Ugly fix for setting httponly cookies - if (req.body.alsoSetCookie) { - res.cookie("token", token, { - maxAge: 3 * 60 * 60 * 1000, httpOnly: true, domain: config.address, - }); - } - const userObject = await existingUser.getPublicObject(); console.log("[*] [logger] [users] [token create] Token created", userObject); @@ -176,10 +173,7 @@ app.get("/current/info", authenticateEndpoint(async (req, res, user) => { res.status(200).json({ error: false, message: "SUCCESS_USER_DATA_FETCHED", - user: { - token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure - ...userObject - }, + user: userObject }); }, undefined, 0)); @@ -209,9 +203,4 @@ app.get("/user/:userid/info", [ }); }, undefined, config.roleMap.USER)); -app.post("/browser/token/clear", authenticateEndpoint((req, res) => { - res.clearCookie("token"); - res.sendStatus(200); -})); - module.exports = app; diff --git a/brainlet/api/v2/gateway/index.js b/brainlet/api/v2/gateway/index.js index 868e415..8742e36 100644 --- a/brainlet/api/v2/gateway/index.js +++ b/brainlet/api/v2/gateway/index.js @@ -3,6 +3,7 @@ const EventEmitter = require("events"); const uuid = require("uuid"); const werift = require("werift"); +const { policies } = require("../../../config"); const { experiments } = require("../../../experiments"); const User = require("../../../models/User"); const Channel = require("../../../models/Channel"); @@ -61,6 +62,7 @@ class GatewayServer extends EventEmitter { }); this.wss.on("connection", (ws) => { + if (!policies.allowGatewayConnection) return ws.close(4007, "Disallowed by policy."); // Send HELLO message as soon as the client connects ws.send(packet("HELLO", {})); ws.session = { diff --git a/brainlet/app/app.html b/brainlet/app/app.html deleted file mode 100755 index ed2ba9d..0000000 --- a/brainlet/app/app.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - App - - - - - - - - - - - - - -
-
- - Debug info and shit - - -

gateway.isConnected: {{ gateway.isConnected }}

-

gateway.socket.id: {{ gateway.socket.id }}

-

gateway.debugInfo: {{ JSON.stringify(gateway.debugInfo) }}

-

userLoggedIn: true

-

userLoggedIn: false

-
-

loggedInUser.username: {{ loggedInUser.username }}

-

loggedInUser._id: {{ loggedInUser._id }}

-

loggedInUser.permissionLevel: {{ loggedInUser.permissionLevel }}

-

loggedInUser.role: {{ loggedInUser.role }}

-
- - - Dump - Close - -
-
- - - Create channel - - - - - - - - - Close - Create - - - - - - Create post for {{ selection.channel.title }} - - - - - - - - - - - - - - Close - Create - - - - - - {{ viewingProfile.username }} - - -

Role: {{ viewingProfile.role }}

- - - Close - -
-
- - -

Brainlet

- - {{ loggedInUser.username }} - - Manage accountperson - Debug info and shitcode - - -
- - -

Browsing channel: {{ selection.channel.title }}

-

Browsing {{ selection.channel.title }}

-

- Browsing {{ selection.channel.title }} with - {{ user.user.username }} -

- arrow_back - refresh -
- -
- - -
- by {{ post.creator.username }} -
- - - - - {{ button.text }} - -
-
- - - {{ post.author.username }} - {{ post.nickAuthor.username }} (from bot "{{ post.author.username }}") - - - {{ post.content }} - -
-
-
- - - - add - edit - - - - - add - Create a new post - - - - category - Create a new channel - - - - - - - - -
- - {{ snackbarNotification }} - {{ snackbarButtonText }} - -
- - \ No newline at end of file diff --git a/brainlet/app/auth.html b/brainlet/app/auth.html deleted file mode 100755 index 4f76fb9..0000000 --- a/brainlet/app/auth.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - Auth - - - - - - - - - -
-
- -

Brainlet

- Home -
-
-
- - -
{{ modeName }}
-
- - -
-
- - - - -
-
- - - - -
-
- - - - -
-
-
-
-

- The owner of this Brainlet instance has made it so that signing up requires a special code. -

- - - - -
-
-
- - - Log in instead - Sign up instead - - Go back - Sign up - - Log in - - Log out - -
- -
- - - {{ snackbarNotification }} - {{ snackbarButtonText }} - -
- - \ No newline at end of file diff --git a/brainlet/app/gatewaytest/app.js b/brainlet/app/gatewaytest/app.js deleted file mode 100644 index 90fa8f0..0000000 --- a/brainlet/app/gatewaytest/app.js +++ /dev/null @@ -1,210 +0,0 @@ -const opcodes = { - 0: { name: "HELLO", data: "JSON" }, - 1: { name: "YOO", data: "JSON" }, - 2: { name: "YOO_ACK", data: "JSON" }, - 3: { name: "ACTION_CREATE_MESSAGE", data: "JSON" }, - 4: { name: "EVENT_CREATE_MESSAGE", data: "JSON" }, - 21: { name: "ACTION_VOICE_REQUEST_SESSION", data: "JSON" }, - 22: { name: "EVENT_VOICE_ASSIGN_SERVER", data: "JSON" }, - 23: { name: "ACTION_VOICE_CONNECTION_REQUEST", data: "JSON" }, - 24: { name: "EVENT_VOICE_CONNECTION_ANSWER", data: "JSON" } -}; - -const opcodeSeparator = "@"; - -const parseMessage = (message) => { - if (typeof message !== "string") throw new Error("msg: message not a string"); - const stringParts = message.split(opcodeSeparator); - if (stringParts < 2) throw new Error("msg: message does not split into more than 2 parts"); - const components = [ stringParts.shift(), stringParts.join(opcodeSeparator) ]; - const op = parseInt(components[0]); - if (isNaN(op)) throw new Error(`msg: message does not contain valid opcode: ${op}`); - - const opcodeData = opcodes[op]; - let data = components[1]; - if (!opcodeData) throw new Error(`msg: message contains unknown opcode ${op}`); - if (opcodeData.data === "JSON") { - data = JSON.parse(data); - } else if (opcodeData.data === "string") { - data = data.toString(); // NOTE: This isnt needed lol - } else { - throw new Error(`msg: invalid data type on opcode ${op}`); - } - - return { - opcode: op, - data: data, - dataType: opcodeData.data, - opcodeType: opcodeData.name || null - }; -}; - -const getOpcodeByName = (name) => { - for (const [key, value] of Object.entries(opcodes)) if (value.name === name) return key; -}; - - -class GatewayConnection { - constructor(token, gatewayUrl) { - this.ws = new WebSocket(gatewayUrl); - - this.handshakeCompleted = false; - this.sessionInformation = null; - - this.ws.onopen = () => console.log("gateway: open"); - this.ws.onclose = (e) => { - this.handshakeCompleted = false; - console.log(`gateway: close: ${e.code}:${e.reason}`); - this.fire("onclose", e); - }; - this.ws.onmessage = (message) => { - try { - const packet = parseMessage(message.data); - if (!packet) return console.error("gateway: invalid packet from server"); - - switch (packet.opcodeType) { - case "HELLO": { - // Got HELLO from server, send YOO as soon as possible - console.log("gateway: got HELLO", packet.data); - console.log("gateway: sending YOO"); - this.ws.send(this.packet("YOO", { token })); - break; - } - case "YOO_ACK": { - // Server accepted connection - console.log("gateway: got YOO_ACK", packet.data); - this.handshakeCompleted = true; - this.sessionInformation = packet.data; - console.log("gateway: handshake complete"); - this.fire("onopen", packet.data); - break; - } - case "EVENT_CREATE_MESSAGE": { - // New message - // console.log("gateway: got new message", packet.data); - this.fire("onmessage", packet.data); - break; - } - default: { - console.log("gateway: got unknown packet", message.data); - break; - } - } - } catch(e) { - return console.error("gateway:", e); - } - }; - } -} - -GatewayConnection.prototype.sendMessage = function(content, channelId) { - if (!this.sessionInformation) throw new Error("gateway: tried to send message before handshake completion"); - - this.ws.send(this.packet("ACTION_CREATE_MESSAGE", { - content, - channel: { - _id: channelId - } - })); -}; - - -GatewayConnection.prototype.packet = function(op, data) { - if (typeof op === "string") op = getOpcodeByName(op); - return `${op}${opcodeSeparator}${JSON.stringify(data)}`; -}; - -GatewayConnection.prototype.fire = function(eventName, ...args) { - if (this[eventName]) return this[eventName](...args); -}; - - -class AppState { - constructor(token, gatewayUrl) { - this.connection = new GatewayConnection(token, gatewayUrl); - this.tc = new Tricarbon(); - - this.elements = { - messagesContainer: "#messages", - messageInput: "#message-input", - channels: "#channels", - topBar: "#top-bar" - }; - - this.tcEvents = this.tc.useEvents(); - this.messageObserver = new MutationObserver((mutationsList) => { - for (const mutation of mutationsList) { - if (mutation.type === "childList") { - const messages = this.tc.A(this.elements.messagesContainer); - messages.scrollTop = messages.scrollHeight; - } - } - }); - - this.messageStore = {}; - this.selectedChannelId = null; - - this.Sidebar = (channels) => (ev) => ` - ${Object.keys(channels).map(k => ` - - `).join("")} - `; - - this.ChannelMessages = (messages) => () => ` - ${Object.keys(messages).map(k => ` -
- ${messages[k].author.username} - ${messages[k].content} -
- `).join("")} - `; - - this.ChannelTopBar = (channel) => () => ` - ${channel.title} - `; - - this.connection.onopen = (sessionInfo) => { - this.renderSidebar(sessionInfo.channels); - this.messageObserver.observe(this.tc.A(this.elements.messagesContainer), { childList: true }); - this.tc.A(this.elements.messageInput).addEventListener("keydown", (e) => { - if (e.code === "Enter") { - if (!this.selectedChannelId) return; - const messageContent = this.tc.A(this.elements.messageInput).value; - if (!messageContent) return; - this.connection.sendMessage(messageContent, this.selectedChannelId); - this.tc.A(this.elements.messageInput).value = ""; - } - }); - }; - this.connection.onmessage = (message) => { - this.appendMessage(message); - }; - } -} - -AppState.prototype.appendMessage = function(message) { - if (!this.messageStore[message.channel._id]) this.messageStore[message.channel._id] = []; - this.messageStore[message.channel._id].push({ content: message.content, author: message.author }); - if (this.selectedChannelId === message.channel._id) { - this.tc.push(this.ChannelMessages(this.messageStore[message.channel._id] || []), this.elements.messagesContainer, false, this.tcEvents(this.elements.messagesContainer)); - } -}; - -AppState.prototype.navigateToChannel = function(channel) { - console.log("app: navigating to channel", channel); - if (this.selectedChannelId !== channel._id) { - this.selectedChannelId = channel._id; - this.tc.push(this.ChannelTopBar(channel), this.elements.topBar, false); - this.tc.push(this.ChannelMessages(this.messageStore[channel._id] || []), this.elements.messagesContainer, false, this.tcEvents(this.elements.messagesContainer)); - } -}; - -AppState.prototype.renderSidebar = function(channels) { - this.tc.push(this.Sidebar(channels), this.elements.channels, false, this.tcEvents(this.elements.channels)); -}; - -let app; -document.addEventListener("DOMContentLoaded", () => { - if (!localStorage.getItem("gatewayUrl")) localStorage.setItem("gatewayUrl", `ws://${window.location.hostname}/gateway?v=2`); - app = new AppState(localStorage.getItem("token"), localStorage.getItem("gatewayUrl")); -}); \ No newline at end of file diff --git a/brainlet/app/gatewaytest/index.html b/brainlet/app/gatewaytest/index.html deleted file mode 100644 index 7ecfe06..0000000 --- a/brainlet/app/gatewaytest/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - -
- -
-
- █████ -
-
-
- █████ - ██████████ -
-
- ████ - ██████ -
-
- █████ - ████████ -
-
- ███ - █████████████ -
-
- -
-
- - diff --git a/brainlet/app/gatewaytest/style.css b/brainlet/app/gatewaytest/style.css deleted file mode 100644 index e7b8c9d..0000000 --- a/brainlet/app/gatewaytest/style.css +++ /dev/null @@ -1,121 +0,0 @@ -/* This CSS is very hacky and extremely inefficient. No human being, alive or dead, shall go through the pain of reading or maintaining this code */ - -:root { - --bg: #000000; - --fg: #ffffff; -} - -body { - align-items: center; - justify-content: center; - display: flex; - margin: 0; - height: 100%; - font-family: Verdana, Geneva, Tahoma, sans-serif; - - background-color: var(--bg); - color: var(--fg); -} - -main { - display: flex; - flex-direction: row; - flex-wrap: wrap; - flex-grow: 0; - width: 800px; - height: 600px; - margin: 10px; - - background-color: var(--bg); - color: var(--fg); - border: 1px solid var(--fg); -} - -.channel-view { - display: flex; - flex-direction: column; - flex: 10; - height: 100%; -} - -.sidebar { - min-width: 150px; - max-width: 150px; - max-height: 100%; - display: flex; - flex-direction: column; - overflow-y: auto; - - border-right: 1px solid var(--fg); -} - -.sidebar-button { - max-width: inherit; - border: 0; - padding: 8px; - margin-right: 0; - margin-left: 0; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - text-align: left; - min-height: 34px; - max-height: 34px; - - border-bottom: 1px solid var(--fg); - background-color: var(--bg); - color: var(--fg); -} - -.sidebar-button:hover, .sidebar-button.selected { - font-weight: bold; -} - -.top-bar { - display: flex; - min-height: 33px; - max-height: 33px; - flex: 1; - align-items: center; - justify-content: center; - - background-color: var(--bg); - border-bottom: 1px solid var(--fg); -} - -.bottom-bar { - min-height: 33px; - max-height: 33px; - flex: 1; - flex-wrap: wrap; - - background-color: var(--bg); - border-top: 1px solid var(--fg); -} - -.channel-message-container { - padding: 15px; - flex: 1; - flex-wrap: wrap; - overflow-y: auto; - height: 100%; - - background-color: var(--bg); -} - -input { - background-color: var(--bg); - color: var(--fg); - border: none; -} - -@media only screen and (max-width: 800px) { - main { - width: 95%; - height: 95%; - } - .sidebar { - min-width: 100px; - max-width: 100px; - } -} \ No newline at end of file diff --git a/brainlet/app/index.html b/brainlet/app/index.html new file mode 100644 index 0000000..dccdec9 --- /dev/null +++ b/brainlet/app/index.html @@ -0,0 +1,24 @@ + + + + + + + Document + + +

Welcome to the API server!

+ This server is hosting the Brainlet API server. Clients may now use it. +
+

Setting up the React frontend:

+
    +
  1. Clone the repository
  2. +
  3. Enter the frontend folder
  4. +
  5. Install the dependencies using `npm i`
  6. +
  7. Build the static files with `npm run build`
  8. +
  9. Copy the static files from the build folder into the app folder on the server (replacing these files)
  10. +
+
+ This server is running Brainlet. + + \ No newline at end of file diff --git a/brainlet/app/resources/css/base.css b/brainlet/app/resources/css/base.css deleted file mode 100755 index e69de29..0000000 diff --git a/brainlet/app/resources/js/app.js b/brainlet/app/resources/js/app.js deleted file mode 100755 index 64161c9..0000000 --- a/brainlet/app/resources/js/app.js +++ /dev/null @@ -1,577 +0,0 @@ -Vue.use(VueMaterial.default); - -const getCreatePostError = (json) => { - switch (json.message) { - case 'ERROR_REQUEST_INVALID_DATA': { - switch (json.errors[0].param) { - case 'title': { - return 'Invalid title. Must be between 3 and 32 characters.'; - } - case 'body': { - return 'Invalid content. Must be between 3 and 1000 characters'; - } - case 'channel': { - return 'Invalid channel. Something went wrong.'; - } - default: { - return 'Invalid value sent to server. Something went wrong.'; - } - } - } - - case 'ERROR_CATEGORY_NOT_FOUND': { - return 'The channel you tried to post to no longer exists.'; - } - - case 'ERROR_ACCESS_DENIED': { - return 'You are not allowed to perform this action.' - } - - default: { - return 'Unknown error. Something went wrong.'; - } - } -} - -const getCreateChannelError = (json) => { - switch (json.message) { - case 'ERROR_REQUEST_INVALID_DATA': { - switch (json.errors[0].param) { - case 'title': { - return 'Invalid title. Title must be between 3 and 32 characters.'; - } - } - } - - case 'ERROR_ACCESS_DENIED': { - return 'You are not allowed to perform this action.' - } - - default: { - return 'Unknown error. Something went wrong.'; - } - } -} - -class GatewayConnection { - constructor() { - this.isConnected = false; - this.socket = null; - - // TODO: set up proper event listening and such, not this dumb crap - this.onDisconnect = () => {} - this.onConnect = () => {} - } -} - -GatewayConnection.prototype.disconnect = function() { - this.socket?.disconnect(); - this.socket = null; - this.isConnected = false; -}; - -GatewayConnection.prototype.connect = function(token) { - console.log('[*] [gateway] [handshake] Trying to connect to gateway'); - this.socket = io('/gateway', { - query: { - token - }, - transports: ['websocket'] - }); - - this.socket.on('connect', () => { - this.socket.once('hello', (debugInfo) => { - console.log('[*] [gateway] [handshake] Got hello from server, sending yoo...', debugInfo); - this.socket.emit('yoo'); - this.isConnected = true; - this.debugInfo = debugInfo; - this.onConnect('CONNECT_RECEIVED_HELLO'); - console.log('[*] [gateway] [handshake] Assuming that server received yoo and that connection is completed.'); - }); - }) - - this.socket.on('error', (e) => { - console.log('[E] [gateway] Gateway error', e); - this.isConnected = false; - this.socket = null; - this.onDisconnect('DISCONNECT_ERR', e); - }); - this.socket.on('disconnectNotification', (e) => { - console.log('[E] [gateway] Received disconnect notfication', e); - this.isConnected = false; - this.socket = null; - this.onDisconnect('DISCONNECT_NOTIF', e); - }); - this.socket.on('disconnect', (e) => { - console.log('[E] [gateway] Disconnected from gateway: ', e); - this.isConnected = false; - this.onDisconnect('DISCONNECT', e); - }); -}; - -GatewayConnection.prototype.sendMessage = function(channelId, content) { - if (!this.isConnected) return 1; - if (content.length >= 2000) return 1; - - this.socket.emit('message', { - channel: { - _id: channelId - }, - content - }); -}; - -GatewayConnection.prototype.subscribeToChannelChat = function(channelId) { - if (!this.isConnected) return; - - const request = [channelId]; - - console.log('[*] [gateway] Subscribing to channel(s)', request); - - this.socket.emit('subscribe', request); -}; - - -const app = new Vue({ - el: '#app', - data: { - showSnackbarNotification: false, - snackbarNotification: '', - snackbarNotificationDuration: 999999, - snackbarButtonText: 'Ok', - loggedInUser: {}, - showApp: false, - menuVisible: false, - selection: { - channel: { - title: '', - browsing: false, - _id: undefined, - isChannel: false, - isChatContext: false - }, - posts: [] - }, - cardButtons: [], - dialog: { - show: { - createPost: false, - createChannel: false, - debug: false - }, - text: { - createPost: { - title: '', - body: '' - }, - createChannel: { - title: '' - } - } - }, - viewingProfile: { - show: false, - _id: '', - username: '', - role: '' - }, - gateway: new GatewayConnection(), - messages: { - 'X': [ { username: '__SYSTEM', content: 'TEST MSG' } ] - }, - userLists: { - 'X': [ { username: '__SYSTEM', _id: 'INVALID_ID' } ] - }, - message: { - typed: '' - } - }, - mounted: async function() { - const res = await fetch(`${window.location.origin}/api/v1/users/current/info`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - credentials: 'include' - }); - - if (res.ok) { - const json = await res.json(); - if (json.user.permissionLevel >= 1) { - this.loggedInUser = json.user; - this.showApp = true; - this.performGatewayConnection(); - this.browseChannels(); - Notification.requestPermission(); - } else { - this.showApp = false; - this.snackbarEditButton('Manage', () => { - window.location.href = `${window.location.origin}/auth.html`; - this.resetSnackbarButton(); - this.showSnackbarNotification = false; - }); - this.notification('Your account does not have the required permissions to enter this page'); - } - } else { - this.showApp = false; - this.snackbarEditButton('Manage', () => { - window.location.href = `${window.location.origin}/auth.html`; - this.resetSnackbarButton(); - this.showSnackbarNotification = false; - }); - this.notification('You are not logged in or your session is invalid'); - } - }, - methods: { - // Gateway and chat - performGatewayConnection: function() { - // TODO: again, the thing im doing with the token is not very secure, since its being sent by the current user info endpoint and is also being send through query parameters - this.gateway.onDisconnect = (e) => { - this.okNotification('Connection lost.'); - }; - this.gateway.connect(this.loggedInUser.token); - this.gateway.socket.on('message', (e) => { - //console.log('[*] [gateway] Message received', e); - this.processMessage(e); - }); - this.gateway.socket.on('clientListUpdate', (e) => { - console.log('[*] [gateway] Client list update', e); - this.processUserListUpdate(e); - }); - this.gateway.socket.on('refreshClient', (e) => { - console.log('[*] [gateway] Gateway requested refresh', e); - this.gateway.disconnect(); - this.messages = {}; - this.userLists = {}; - this.message.typed = ''; - if (e.reason === 'exit') { - this.showApp = false; - this.okNotification('The server has exited. Sit tight!'); - } else if (e.reason === 'upd') { - this.showApp = false; - this.okNotification('An update has just rolled out! To ensure everything runs smoothly, you need to refresh the page!'); - } else { - this.showApp = false; - this.okNotification('Sorry, but something happened and a refresh is required to keep using the app!'); - } - this.snackbarEditButton('Refresh', () => { - window.location.reload(); - }); - }); - }, - shouldMergeMessage: function(messageObject, channelMessageList) { - const lastMessageIndex = channelMessageList.length-1; - const lastMessage = channelMessageList[lastMessageIndex]; - - if (!lastMessage) return; - if (lastMessage.author._id === messageObject.author._id) { - if (lastMessage.nickAuthor && messageObject.nickAuthor) { - if (lastMessage.nickAuthor.username === messageObject.nickAuthor.username) { - return true; - } else { - return false; - } - } - } - if (lastMessage.author._id === messageObject.author._id) return true; - return false; - }, - processMessage: async function(messageObject) { - if (!this.messages[messageObject.channel._id]) this.$set(this.messages, messageObject.channel._id, []); - const channelMessageList = this.messages[messageObject.channel._id]; - const lastMessageIndex = channelMessageList.length-1; - - if (this.shouldMergeMessage(messageObject, channelMessageList)) { - channelMessageList[lastMessageIndex].content += `\n${messageObject.content}`; - } else { - this.messages[messageObject.channel._id].push(messageObject); - } - - if (messageObject.channel._id === this.selection.channel._id) { - this.$nextTick(() => { - // TODO: When the user presses back, actually undo this scroll cause its annoying to scroll back up in the channel list - const container = this.$el.querySelector('#posts-container'); - container.scrollTop = container.scrollHeight; - }); - } - - if (messageObject.author.username !== this.loggedInUser.username && messageObject.channel._id !== this.selection.channel._id) { - this.okNotification(`${messageObject.channel.title}/${messageObject.author.username}: ${messageObject.content}`); - } - - if (messageObject.author.username !== this.loggedInUser.username) { - if (Notification.permission === 'granted') { - try { - new Notification(`${messageObject.channel.title}/${messageObject.author.username}`, { - body: messageObject.content - }); - } catch(e) { - console.log('[E] [chat] Failed to show notification'); - } - - } - } - }, - processUserListUpdate: async function(e) { - const { channel, clientList } = e; - if (!this.userLists[channel._id]) this.$set(this.userLists, channel._id, []); - this.userLists[channel._id] = clientList; - }, - openChatForChannel: async function(channelId) { - this.gateway.subscribeToChannelChat(channelId); - - this.selection.channel.isChatContext = true; - this.selection.channel.browsing = true; - this.selection.channel.title = 'Chat'; - this.selection.channel._id = channelId; - }, - sendCurrentMessage: async function() { - const status = await this.gateway.sendMessage(this.selection.channel._id, this.message.typed); - if (status === 1) { - this.okNotification('Failed to send message!'); - return; - } - this.message.typed = ''; - }, - - // Debug - toggleDebugDialog: async function() { - this.dialog.show.debug = !this.dialog.show.debug; - }, - debugDump: async function() { - console.log('[DEBUG DUMP] [gateway]', this.gateway); - console.log('[DEBUG DUMP] [loggedInUser] (this contains sensitive information about the current logged in user, do not leak it to other people lol)', this.loggedInUser); - }, - - // Channel and post browsing - browseChannels: async function() { - const res = await fetch(`${window.location.origin}/api/v1/content/channel/list?count=50`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - credentials: 'include' - }); - - if (res.ok) { - const json = await res.json(); - - this.selection.channel.title = 'channels'; - this.selection.channel.browsing = true; - this.selection.channel.isChannel = false; - this.selection.channel.isChatContext = false; - this.selection.channel._id = '__CATEGORY_LIST'; - this.selection.posts = []; - - this.cardButtons = []; - - this.button('chat', (post) => { - if (post._id) { - this.openChatForChannel(post._id); - } - }); - this.button('topic', (post) => { - this.browse(post); - }); - - for (let i = 0; i < json.channels.length; i++) { - const v = json.channels[i]; - this.selection.posts.push({ title: v.title, body: '', _id: v._id, creator: v.creator }); - } - } else { - this.okNotification('Failed to fetch channel list'); - } - }, - browse: async function(channel) { - const { _id, title } = channel; - - const res = await fetch(`${window.location.origin}/api/v1/content/channel/${_id}/info`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - credentials: 'include' - }); - - if (res.ok) { - const json = await res.json(); - - this.selection.channel.title = title; - this.selection.channel._id = _id; - this.selection.channel.browsing = true; - this.selection.channel.isChannel = true; - this.selection.channel.isChatContext = false; - this.selection.posts = []; - - this.cardButtons = []; - - for (let i = 0; i < json.channel.posts.length; i++) { - const v = json.channel.posts[i]; - this.selection.posts.push({ title: v.title, body: v.body, _id: v._id, creator: v.creator }); - } - } else { - this.okNotification('Failed to fetch channel'); - } - }, - refresh: function() { - if (this.selection.channel.title === 'channels' && this.selection.channel.isChannel === false) { - this.browseChannels(); - } else { - this.browse(this.selection.channel); - } - }, - button: function(text, click) { - this.cardButtons.push({ text, click }); - }, - stopBrowsing: function() { - this.selection.channel = { - title: '', - browsing: false, - _id: undefined, - isChannel: false, - isChatContext: false - }; - this.selection.posts = []; - this.cardButtons = []; - }, - viewProfile: async function(id) { - // TODO: this just returns the username for now - const res = await fetch(`${window.location.origin}/api/v1/users/user/${id}/info`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - credentials: 'include' - }); - - if (res.ok) { - const json = await res.json(); - this.viewingProfile.username = json.user.username; - this.viewingProfile._id = json.user._id; - this.viewingProfile.role = json.user.role; - this.viewingProfile.show = true; - } else { - this.okNotification('Failed to fetch user data'); - } - }, - - // Content creation - showCreatePostDialog: function() { - if (!this.selection.channel.isChannel) { - this.okNotification('You are not in a channel'); - return; - } - - this.dialog.show.createPost = true; - }, - createPost: async function() { - if (!this.selection.channel.isChannel) { - this.okNotification('You are not in a channel'); - return; - } - - const channel = this.selection.channel; - const input = this.dialog.text.createPost; - - const res = await fetch(`${window.location.origin}/api/v1/content/post/create`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - credentials: 'include', - body: JSON.stringify({ - channel: channel._id, - title: input.title, - body: input.body - }) - }); - - if (res.ok) { - this.okNotification('Successfully created post'); - this.dialog.show.createPost = false; - this.browse(this.selection.channel); - return; - } else { - if (res.status === 401 || res.status === 403) { - this.okNotification('You are not allowed to do that'); - return; - } - if (res.status === 429) { - this.okNotification('Chill! You are posting too much!'); - return; - } - - const json = await res.json(); - this.okNotification(getCreatePostError(json)); - return; - } - }, - createChannel: async function() { - const input = this.dialog.text.createChannel; - - const res = await fetch(`${window.location.origin}/api/v1/content/channel/create`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - credentials: 'include', - body: JSON.stringify({ - title: input.title - }) - }); - - if (res.ok) { - this.okNotification('Successfully created channel'); - this.dialog.show.createChannel = false; - this.browseChannels(); - return; - } else { - if (res.status === 401 || res.status === 403) { - this.okNotification('You are not allowed to do that'); - return; - } - if (res.status === 429) { - this.okNotification('Chill! You are posting too much!'); - return; - } - - const json = await res.json(); - this.okNotification(getCreateChannelError(json)); - return; - } - }, - - // Navigation - navigateToAccountManager() { - window.location.href = `${window.location.origin}/auth.html`; - }, - - // Snackbar - snackbarButtonAction: function() { - this.showSnackbarNotification = false; - }, - snackbarButtonClick: function() { - this.snackbarButtonAction(); - }, - snackbarEditButton: function(buttonText="Ok", action) { - this.snackbarButtonText = buttonText; - this.snackbarButtonAction = action; - }, - resetSnackbarButton: function() { - this.snackbarButtonText = 'Ok'; - this.snackbarButtonAction = () => { - this.showSnackbarNotification = false; - }; - }, - notification: function(text) { - this.snackbarNotification = text; - this.showSnackbarNotification = true; - }, - okNotification: function(text) { - this.resetSnackbarButton(); - this.notification(text); - } - } -}); diff --git a/brainlet/app/resources/js/auth.js b/brainlet/app/resources/js/auth.js deleted file mode 100755 index f1e6788..0000000 --- a/brainlet/app/resources/js/auth.js +++ /dev/null @@ -1,265 +0,0 @@ -Vue.use(VueMaterial.default); - -const getLoginMessageFromError = (json) => { - switch (json.message) { - case 'ERROR_REQUEST_LOGIN_INVALID': { - return 'Invalid username or password.'; - } - - case 'ERROR_ACCESS_DENIED': { - return 'You are not allowed to perform this action.' - } - - default: { - return 'Unknown error. Something went wrong.' - } - } -} - -const getSignupMessageFromError = (json) => { - switch (json.message) { - case 'ERROR_REQUEST_INVALID_DATA': { - - switch (json.errors[0].param) { - case 'username': { - return 'Invalid username. Username must be between 3 and 32 characters long, and be alphanumeric.'; - } - case 'password': { - return 'Invalid password. Password must be at least 8 characters long and at most 128 characters.'; - } - case 'email': { - return 'Invalid email.'; - } - case 'specialCode': { - return 'Invalid special code.'; - } - - default: { - return 'Invalid value sent to server. Something went wrong.'; - } - } - - } - - case 'ERROR_ACCESS_DENIED': { - return 'You are not allowed to perform this action.' - } - - case 'ERROR_REQUEST_USERNAME_EXISTS': { - return 'That username is taken.'; - } - - default: { - return 'Unknown error. Something went wrong.' - } - } -}; - -const app = new Vue({ - el: '#app', - data: { - usernameInput: '', - emailInput: '', - passwordInput: '', - specialCodeInput: '', - mode: 'SIGNUP', - showSnackbarNotification: false, - snackbarNotification: '', - snackbarNotificationDuration: 999999, - snackbarButtonText: 'Ok', - successfulLogin: false, - loggedInUser: {}, - requiresSpecialCode: null - }, - mounted: async function() { - const res = await fetch(`${window.location.origin}/api/v1/users/current/info`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - }, - credentials: 'include' - }); - - if (res.status === 200) { - const json = await res.json(); - if (json.user.permissionLevel >= 1) { - this.loggedInUser = json.user; - this.mode = 'MANAGE'; - } else { - this.resetSnackbarButton(); - this.notification('Your account has been suspended'); - this.successfulLogin = true; - } - } else { - const resInfo = await fetch(`${window.location.origin}/api/v1/users/account/create/info`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - } - }); - - if (resInfo.ok) { - const json = await resInfo.json(); - this.requiresSpecialCode = json.requiresSpecialCode; - - this.mode = 'SIGNUP'; - } else { - this.mode = '_ERROR'; - } - } - }, - computed: { - modeName: function() { - switch (this.mode) { - case 'SIGNUP': { - return 'Sign up'; - } - case 'LOGIN': { - return 'Log in'; - } - case 'LOADING': { - return 'Loading...'; - } - case 'MANAGE': { - return this.loggedInUser.username || 'Unknown account'; - } - case 'SPECIAL_CODE': { - return 'Just one more step' - } - case 'NONE': { - return ''; - } - default: { - return 'Something went wrong.'; - } - } - } - }, - methods: { - snackbarButtonAction: function() { - this.showSnackbarNotification = false; - }, - snackbarButtonClick: function() { - this.snackbarButtonAction(); - }, - snackbarEditButton: function(buttonText="Ok", action) { - this.snackbarButtonText = buttonText; - this.snackbarButtonAction = action; - }, - resetSnackbarButton: function() { - this.snackbarButtonText = 'Ok'; - this.snackbarButtonAction = () => { - this.showSnackbarNotification = false; - }; - }, - notification: function(text) { - this.snackbarNotification = text; - this.showSnackbarNotification = true; - }, - performTokenRemoval: async function() { - const res = await fetch(`${window.location.origin}/api/v1/users/browser/token/clear`, { - method: 'POST', - credentials: 'include' - }); - - if (res.status === 200) { - this.loggedInUser = {}; - this.snackbarEditButton('Home', () => { - window.location.href = `${window.location.origin}`; - this.resetSnackbarButton(); - this.showSnackbarNotification = false; - }); - this.successfulLogin = true; - this.notification('Successfully logged out'); - } else { - this.resetSnackbarButton(); - this.notification('Could not log out'); - } - }, - navigateHome: function() { - window.location.href = `${window.location.origin}`; - }, - performTokenCreation: async function() { - const res = await fetch(`${window.location.origin}/api/v1/users/token/create`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - credentials: 'include', - body: JSON.stringify({ - username: this.usernameInput, - password: this.passwordInput, - alsoSetCookie: true - }) - }); - - const json = await res.json(); - - if (json.error || res.status !== 200) { - this.resetSnackbarButton(); - this.notification(getLoginMessageFromError(json)); - return; - } else { - document.cookie = `token=${json.token}; HTTPOnly=true; max-age=10800`; - this.successfulLogin = true; - this.snackbarEditButton('Home', () => { - this.navigateHome(); - this.resetSnackbarButton(); - this.showSnackbarNotification = false; - }); - this.notification(`Successfully logged into account "${json.user.username}"`); - this.loggedInUser = { username: json.user.username, _id: json.user._id }; - return; - } - }, - performAccountCreation: async function() { - let jsonData = { - username: this.usernameInput, - email: this.emailInput, - password: this.passwordInput - }; - - if (this.requiresSpecialCode) { - if (this.mode === 'SIGNUP') { - this.mode = 'SPECIAL_CODE'; - return; - } else if (this.mode !== 'SPECIAL_CODE') { - return; - } - - jsonData = { - specialCode: this.specialCodeInput, - ...jsonData - } - } - - const res = await fetch(`${window.location.origin}/api/v1/users/account/create`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(jsonData) - }); - - const json = await res.json(); - - console.log(json); - - if (json.error || res.status !== 200) { - this.resetSnackbarButton(); - this.notification(getSignupMessageFromError(json)); - return; - } else { - this.snackbarEditButton('Login', () => { - this.mode = 'LOGIN'; - this.resetSnackbarButton(); - this.showSnackbarNotification = false; - }); - this.notification(`Account "${json.user.username}" successfully created`); - return; - } - } - } -}); \ No newline at end of file diff --git a/brainlet/config.js b/brainlet/config.js index 7d5a62c..71c47ff 100755 --- a/brainlet/config.js +++ b/brainlet/config.js @@ -1,14 +1,37 @@ module.exports = { - ports: { - mainServerPort: 3005, - }, - address: "localhost", - //restrictions: { - // signup: { - // specialCode: '' - // } - //}, mongoUrl: "mongodb://127.0.0.1:27017/app", + ports: {mainServerPort: 3005}, + corsAllowList: [ + // This is the development corsAllowList. Modify it according to your setup and domains. + // Please note that the protocol (http://, https://) matters here. If you use https, make sure to add it as such. + // Ports also matter. (and obviously the domain matters too) + + // EXAMPLE: If my domain is example.com and I'm hosting brainlet (and brainlet-react's static files in the app folder) with HTTPS: + // "https://example.com" + + "http://localhost:3005", // Allow the server itself (provided it's listening on 3005) + //"http://localhost:3000" // Optionally allow the react app development server (which listens on 3000 by default) + ], + policies: { + // Currently, policies apply to all users - no matter the role. + allowChannelCreation: true, + allowPostCreation: false, + allowAccountCreation: true, + allowLogin: true, + allowGatewayConnection: true, + }, + /* + --- Adding a special code requirement for account creation + + - uncomment the code below and fill in the specialCode string with *A 12 CHARACTER* string + restrictions: { + signup: { + specialCode: "" + } + }, + */ + address: "localhost", + tokenExpiresIn: "8h", bcryptRounds: 10, roleMap: { "BANNED": 0, @@ -17,14 +40,4 @@ module.exports = { "BOT": 3, "ADMIN": 4 }, - gatewayStillNotConnectedTimeoutMS: 15*1000 }; -module.exports.corsAllowList = [ - // Allow the normal web interface - `http://${module.exports.address}:${module.exports.ports.mainServerPort}`, - `https://${module.exports.address}:${module.exports.ports.mainServerPort}`, - - // Allow brainet-react - `http://${module.exports.address}:3000`, - `https://${module.exports.address}:3000` -];