forked from hippoz/brainlet
Hosting improvements: more config options (policies to restrict certain actions, improved CORS documentation and default), no more default frontend, improved defaults, ...
This commit is contained in:
parent
9a4787b1a1
commit
7dda7fbcb2
14 changed files with 69 additions and 1547 deletions
|
@ -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() });
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -1,207 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>App</title>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic|Material+Icons">
|
||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default-dark.css">
|
||||
|
||||
<script defer src="https://unpkg.com/vue-material"></script>
|
||||
<script defer src="/socket.io/socket.io.js"></script>
|
||||
<script defer src="./resources/js/app.js"></script>
|
||||
|
||||
<style>
|
||||
.md-card {
|
||||
width: 312px;
|
||||
margin: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
width: 95%;
|
||||
margin: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-bar {
|
||||
position: fixed !important;
|
||||
left: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 98% !important;
|
||||
margin-left: 1% !important;
|
||||
}
|
||||
|
||||
.as-console-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
overflow-y: auto;
|
||||
height: calc(90vh - 100px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="appContainer" v-if="showApp">
|
||||
<md-dialog id="debug-dialog" :md-active.sync="dialog.show.debug">
|
||||
<md-dialog-title>Debug info and shit</md-dialog-title>
|
||||
|
||||
<md-dialog-content>
|
||||
<p>gateway.isConnected: {{ gateway.isConnected }}</p>
|
||||
<p v-if="gateway.socket.id">gateway.socket.id: {{ gateway.socket.id }}</p>
|
||||
<p v-if="gateway.debugInfo">gateway.debugInfo: {{ JSON.stringify(gateway.debugInfo) }}</p>
|
||||
<p v-if="loggedInUser._id">userLoggedIn: true</p>
|
||||
<p v-if="!loggedInUser._id">userLoggedIn: false</p>
|
||||
<div id="debug-logged-in-data" v-if="loggedInUser">
|
||||
<p>loggedInUser.username: {{ loggedInUser.username }}</p>
|
||||
<p>loggedInUser._id: {{ loggedInUser._id }}</p>
|
||||
<p>loggedInUser.permissionLevel: {{ loggedInUser.permissionLevel }}</p>
|
||||
<p>loggedInUser.role: {{ loggedInUser.role }}</p>
|
||||
</div>
|
||||
|
||||
<md-dialog-actions>
|
||||
<md-button @click="debugDump()">Dump</md-button>
|
||||
<md-button @click="dialog.show.debug = false">Close</md-button>
|
||||
</md-dialog-actions>
|
||||
</md-dialog-content>
|
||||
</md-dialog>
|
||||
|
||||
<md-dialog id="create-channel-dialog" :md-active.sync="dialog.show.createChannel">
|
||||
<md-dialog-title>Create channel</md-dialog-title>
|
||||
|
||||
<md-dialog-content>
|
||||
<md-field>
|
||||
<label>Title</label>
|
||||
<md-input v-model="dialog.text.createChannel.title"></md-input>
|
||||
</md-field>
|
||||
|
||||
<md-dialog-actions>
|
||||
<md-button @click="dialog.show.createChannel = false">Close</md-button>
|
||||
<md-button class="md-primary" @click="createChannel()">Create</md-button>
|
||||
</md-dialog-actions>
|
||||
</md-dialog-content>
|
||||
</md-dialog>
|
||||
|
||||
<md-dialog id="create-post-dialog" :md-active.sync="dialog.show.createPost">
|
||||
<md-dialog-title>Create post for <strong>{{ selection.channel.title }}</strong></md-dialog-title>
|
||||
|
||||
<md-dialog-content>
|
||||
<md-field>
|
||||
<label>Title</label>
|
||||
<md-input v-model="dialog.text.createPost.title"></md-input>
|
||||
</md-field>
|
||||
|
||||
<md-field>
|
||||
<label>Content</label>
|
||||
<md-textarea v-model="dialog.text.createPost.body" md-counter="1000"></md-textarea>
|
||||
</md-field>
|
||||
|
||||
<md-dialog-actions>
|
||||
<md-button @click="dialog.show.createPost = false">Close</md-button>
|
||||
<md-button class="md-primary" @click="createPost()">Create</md-button>
|
||||
</md-dialog-actions>
|
||||
</md-dialog-content>
|
||||
</md-dialog>
|
||||
|
||||
<md-dialog id="user-info-dialog" :md-active.sync="viewingProfile.show">
|
||||
<md-dialog-title><strong>{{ viewingProfile.username }}</strong></md-dialog-title>
|
||||
|
||||
<md-dialog-content>
|
||||
<p>Role: {{ viewingProfile.role }}</p>
|
||||
|
||||
<md-dialog-actions>
|
||||
<md-button @click="viewingProfile.show = false">Close</md-button>
|
||||
</md-dialog-actions>
|
||||
</md-dialog-content>
|
||||
</md-dialog>
|
||||
|
||||
<md-toolbar class="md-accent" md-elevation="5">
|
||||
<h3 class="md-title" style="flex: 1">Brainlet</h3>
|
||||
<md-menu md-size="small">
|
||||
<md-button md-menu-trigger>{{ loggedInUser.username }}</md-button>
|
||||
<md-menu-content>
|
||||
<md-menu-item @click="navigateToAccountManager()"><span>Manage account</span><md-icon>person</md-icon></md-menu-item>
|
||||
<md-menu-item @click="toggleDebugDialog()"><span>Debug info and shit</span><md-icon>code</md-icon></md-menu-item>
|
||||
</md-menu-content>
|
||||
</md-menu>
|
||||
</md-toolbar>
|
||||
|
||||
<md-toolbar v-show="selection.channel.browsing" class="md-dense" md-elevation="5">
|
||||
<h3 v-if="selection.channel.isChannel && !selection.channel.isChatContext" class="md-title" style="flex: 1">Browsing channel: {{ selection.channel.title }}</h3>
|
||||
<h3 v-if="!selection.channel.isChannel && !selection.channel.isChatContext" class="md-title" style="flex: 1">Browsing {{ selection.channel.title }}</h3>
|
||||
<h3 v-if="!selection.channel.isChannel && selection.channel.isChatContext" class="md-title" style="flex: 1">
|
||||
Browsing {{ selection.channel.title }} with
|
||||
<a v-for="user in userLists[selection.channel._id]" class="md-dense cursor" v-on:click="viewProfile(user.user._id)" v-bind:style="{ 'color': user.user.color }">{{ user.user.username }} </a>
|
||||
</h3>
|
||||
<md-button @click="browseChannels()" v-if="selection.channel.isChannel || selection.channel.isChatContext"><md-icon>arrow_back</md-icon></md-button>
|
||||
<md-button @click="refresh()" v-if="!selection.channel.isChatContext"><md-icon>refresh</md-icon></md-button>
|
||||
</md-toolbar>
|
||||
|
||||
<div id="posts-container" class="posts-container" v-if="selection.channel.browsing">
|
||||
<md-card v-for="post in selection.posts" v-bind:key="post._id" v-if="!selection.channel.isChatContext">
|
||||
<md-card-header>
|
||||
<div class="md-title" v-html="post.title"></div>
|
||||
<span>by <a class="md-dense cursor" v-on:click="viewProfile(post.creator._id)" v-bind:style="{ 'color': post.creator.color}">{{ post.creator.username }}</a></span>
|
||||
</md-card-header>
|
||||
|
||||
<md-card-content style="white-space: break-spaces !important;" v-html="post.body"></md-card-content>
|
||||
|
||||
<md-card-actions>
|
||||
<md-button v-for="button in cardButtons" v-bind:key="button.text" @click="button.click(post)"><md-icon>{{ button.text }}</md-icon></md-button>
|
||||
</md-card-actions>
|
||||
</md-card>
|
||||
<div v-for="post,k in messages[selection.channel._id]" v-if="selection.channel.isChatContext" :key="post._id + post.author._id">
|
||||
<md-card class="message-card">
|
||||
<md-card-header>
|
||||
<a v-if="!post.nickAuthor" class="md-dense cursor md-title" v-on:click="viewProfile(post.author._id)" v-bind:style="{ 'color': post.author.color}"><span>{{ post.author.username }}</span></a>
|
||||
<a v-if="post.nickAuthor" class="md-dense cursor md-title" v-on:click="viewProfile(post.author._id)" v-bind:style="{ 'color': post.author.color}"><span>{{ post.nickAuthor.username }} (from bot "{{ post.author.username }}")</span></a>
|
||||
</md-card-header>
|
||||
|
||||
<md-card-content style="white-space: break-spaces !important;">{{ post.content }}</md-card-content>
|
||||
</md-card>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<md-speed-dial class="md-fixed md-bottom-right" md-direction="top" md-elevation="5" style="z-index: 4000;" v-show="!selection.channel.isChatContext">
|
||||
<md-speed-dial-target>
|
||||
<md-icon class="md-morph-initial">add</md-icon>
|
||||
<md-icon class="md-morph-final">edit</md-icon>
|
||||
</md-speed-dial-target>
|
||||
|
||||
<md-speed-dial-content>
|
||||
<md-button v-show="selection.channel.isChannel" class="md-icon-button" @click="showCreatePostDialog()">
|
||||
<md-icon>add</md-icon>
|
||||
<md-tooltip md-direction="left">Create a new post</md-tooltip>
|
||||
</md-button>
|
||||
|
||||
<md-button class="md-icon-button" @click="dialog.show.createChannel = true">
|
||||
<md-icon>category</md-icon>
|
||||
<md-tooltip md-direction="left">Create a new channel</md-tooltip>
|
||||
</md-button>
|
||||
</md-speed-dial-content>
|
||||
</md-speed-dial>
|
||||
|
||||
<md-field md-inline class="chat-bar" v-show="selection.channel.isChatContext">
|
||||
<label>Write something interesting, go on!</label>
|
||||
<md-input v-model="message.typed" v-on:keyup.enter="sendCurrentMessage()"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
<md-snackbar md-position="center" :md-duration="snackbarNotificationDuration" :md-active.sync="showSnackbarNotification" md-persistent>
|
||||
<span>{{ snackbarNotification }}</span>
|
||||
<md-button class="md-primary" @click="snackbarButtonClick()">{{ snackbarButtonText }}</md-button>
|
||||
</md-snackbar>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,84 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Auth</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic|Material+Icons">
|
||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default-dark.css">
|
||||
|
||||
<script defer src="https://unpkg.com/vue-material"></script>
|
||||
<script defer src="./resources/js/auth.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="topbar-container">
|
||||
<md-toolbar class="md-accent" md-elevation="5">
|
||||
<h3 class="md-title" style="flex: 1">Brainlet</h3>
|
||||
<md-button @click="navigateHome()" v-if="loggedInUser.username">Home</md-button>
|
||||
</md-toolbar>
|
||||
</div>
|
||||
<div class="md-layout md-alignment-top-center" v-if="!successfulLogin">
|
||||
<md-card class="md-layout-item md-size-25 md-medium-size-100">
|
||||
<md-card-header>
|
||||
<div class="md-title">{{ modeName }}</div>
|
||||
</md-card-header>
|
||||
|
||||
<md-card-content>
|
||||
<div v-if="mode === 'LOGIN' || mode === 'SIGNUP'">
|
||||
<div>
|
||||
<md-field>
|
||||
<label>Username</label>
|
||||
<md-input v-model="usernameInput"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
<div v-if="mode === 'SIGNUP'">
|
||||
<md-field>
|
||||
<label>Email</label>
|
||||
<md-input v-model="emailInput"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
<div>
|
||||
<md-field>
|
||||
<label>Password</label>
|
||||
<md-input v-model="passwordInput" type="password"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mode === 'SPECIAL_CODE'">
|
||||
<div>
|
||||
<p>
|
||||
The owner of this Brainlet instance has made it so that signing up requires a special code.
|
||||
</p>
|
||||
<md-field>
|
||||
<label>Special code</label>
|
||||
<md-input v-model="specialCodeInput" type="password"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
</div>
|
||||
</md-card-content>
|
||||
|
||||
<md-card-actions>
|
||||
<md-button v-if="mode === 'SIGNUP'" @click="mode='LOGIN'" class="md-dense">Log in instead</md-button>
|
||||
<md-button v-if="mode === 'LOGIN'" @click="mode='SIGNUP'" class="md-dense">Sign up instead</md-button>
|
||||
|
||||
<md-button v-if="mode === 'SPECIAL_CODE'" class="md-dense" @click="mode='SIGNUP'">Go back</md-button>
|
||||
<md-button v-if="mode === 'SIGNUP' || mode === 'SPECIAL_CODE'" class="md-dense md-raised md-primary" @click="performAccountCreation()">Sign up</md-button>
|
||||
|
||||
<md-button v-if="mode === 'LOGIN'" class="md-dense md-raised md-primary" @click="performTokenCreation()">Log in</md-button>
|
||||
|
||||
<md-button v-if="mode === 'MANAGE'" class="md-dense md-raised" @click="performTokenRemoval()">Log out</md-button>
|
||||
</md-card-actions>
|
||||
</md-card>
|
||||
|
||||
</div>
|
||||
|
||||
<md-snackbar md-position="center" :md-duration="snackbarNotificationDuration" :md-active.sync="showSnackbarNotification" md-persistent>
|
||||
<span>{{ snackbarNotification }}</span>
|
||||
<md-button class="md-primary" @click="snackbarButtonClick()">{{ snackbarButtonText }}</md-button>
|
||||
</md-snackbar>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -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 => `
|
||||
<button class="sidebar-button" id="${channels[k]._id}" onclick="${ev(() => this.navigateToChannel(channels[k]))}">${channels[k].title}</button>
|
||||
`).join("")}
|
||||
`;
|
||||
|
||||
this.ChannelMessages = (messages) => () => `
|
||||
${Object.keys(messages).map(k => `
|
||||
<div class="message">
|
||||
<strong>${messages[k].author.username}</strong>
|
||||
${messages[k].content}
|
||||
</div>
|
||||
`).join("")}
|
||||
`;
|
||||
|
||||
this.ChannelTopBar = (channel) => () => `
|
||||
<strong class="top-bar-channel-name">${channel.title}</strong>
|
||||
`;
|
||||
|
||||
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"));
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<script src="https://git.hippoz.xyz/hippoz/tricarbon/raw/commit/5e15efd94b53db1b289df2b9db12a414cdb1ee7d/lib/indexsmall.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<div class="sidebar" id="channels">
|
||||
<button class="sidebar-button">████</button>
|
||||
<button class="sidebar-button">████</button>
|
||||
<button class="sidebar-button">██████</button>
|
||||
<button class="sidebar-button">███</button>
|
||||
<button class="sidebar-button">██████</button>
|
||||
<button class="sidebar-button">████</button>
|
||||
<button class="sidebar-button">█████</button>
|
||||
</div>
|
||||
<div class="channel-view">
|
||||
<div class="top-bar" id="top-bar">
|
||||
<strong class="top-bar-channel-name">█████</strong>
|
||||
</div>
|
||||
<div class="channel-message-container" id="messages">
|
||||
<div class="message">
|
||||
<strong>█████</strong>
|
||||
██████████
|
||||
</div>
|
||||
<div class="message">
|
||||
<strong>████</strong>
|
||||
██████
|
||||
</div>
|
||||
<div class="message">
|
||||
<strong>█████</strong>
|
||||
████████
|
||||
</div>
|
||||
<div class="message">
|
||||
<strong>███</strong>
|
||||
█████████████
|
||||
</div>
|
||||
</div>
|
||||
<input class="bottom-bar" placeholder="Go on, type something interesting!" id="message-input">
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -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;
|
||||
}
|
||||
}
|
24
brainlet/app/index.html
Normal file
24
brainlet/app/index.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to the API server!</h1>
|
||||
<b>This server is hosting the Brainlet API server. Clients may now use it.</b>
|
||||
<hr>
|
||||
<h3>Setting up the React frontend:</h3>
|
||||
<ol>
|
||||
<li>Clone the repository</li>
|
||||
<li>Enter the frontend folder</li>
|
||||
<li>Install the dependencies using `npm i`</li>
|
||||
<li>Build the static files with `npm run build`</li>
|
||||
<li>Copy the static files from the build folder into the app folder on the server (replacing these files)</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<i>This server is running Brainlet.</i>
|
||||
</body>
|
||||
</html>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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`
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue