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:
hippoz 2021-08-21 21:30:02 +03:00
parent 9a4787b1a1
commit 7dda7fbcb2
No known key found for this signature in database
GPG key ID: 7C52899193467641
14 changed files with 69 additions and 1547 deletions

View file

@ -21,6 +21,8 @@ app.post("/channel/create", [
createLimiter, createLimiter,
body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape() body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape()
], authenticateEndpoint(async (req, res, user) => { ], 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); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); 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("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
body("body").not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(), body("body").not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
], authenticateEndpoint(async (req, res, user) => { ], 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); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });

View file

@ -9,8 +9,7 @@ app.use("/users", usersAPI);
app.use("/content", contentAPI); app.use("/content", contentAPI);
app.get("/", (req, res) => { app.get("/", (req, res) => {
// TODO: Add more checks for this, or maybe remove res.json({ error: false });
res.json({ apiStatus: "OK", apis: [ "users", "content" ] });
}); });
module.exports = app; module.exports = app;

View file

@ -46,6 +46,8 @@ app.post("/account/create", [
body("password").not().isEmpty().isLength({ min: 8, max: 128 }), body("password").not().isEmpty().isLength({ min: 8, max: 128 }),
body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric() body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
], async (req, res) => { ], async (req, res) => {
if (!config.policies.allowAccountCreation) return res.status(403).json({ error: true, message: "ERROR_FORBIDDEN_BY_POLICY" });
try { try {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@ -114,6 +116,8 @@ app.post("/token/create", [
body("username").not().isEmpty().trim().isAlphanumeric(), body("username").not().isEmpty().trim().isAlphanumeric(),
body("password").not().isEmpty() body("password").not().isEmpty()
], async (req, res) => { ], async (req, res) => {
if (!config.policies.allowLogin) return res.status(403).json({ error: true, message: "ERROR_FORBIDDEN_BY_POLICY" });
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" }); res.status(400).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
@ -141,7 +145,7 @@ app.post("/token/create", [
return; return;
} }
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: "3h" }, async (err, token) => { jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: config.tokenExpiresIn }, async (err, token) => {
if (err) { if (err) {
res.status(500).json({ res.status(500).json({
error: true, error: true,
@ -150,13 +154,6 @@ app.post("/token/create", [
return; 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(); const userObject = await existingUser.getPublicObject();
console.log("[*] [logger] [users] [token create] Token created", userObject); 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({ res.status(200).json({
error: false, error: false,
message: "SUCCESS_USER_DATA_FETCHED", message: "SUCCESS_USER_DATA_FETCHED",
user: { user: userObject
token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure
...userObject
},
}); });
}, undefined, 0)); }, undefined, 0));
@ -209,9 +203,4 @@ app.get("/user/:userid/info", [
}); });
}, undefined, config.roleMap.USER)); }, undefined, config.roleMap.USER));
app.post("/browser/token/clear", authenticateEndpoint((req, res) => {
res.clearCookie("token");
res.sendStatus(200);
}));
module.exports = app; module.exports = app;

View file

@ -3,6 +3,7 @@ const EventEmitter = require("events");
const uuid = require("uuid"); const uuid = require("uuid");
const werift = require("werift"); const werift = require("werift");
const { policies } = require("../../../config");
const { experiments } = require("../../../experiments"); const { experiments } = require("../../../experiments");
const User = require("../../../models/User"); const User = require("../../../models/User");
const Channel = require("../../../models/Channel"); const Channel = require("../../../models/Channel");
@ -61,6 +62,7 @@ class GatewayServer extends EventEmitter {
}); });
this.wss.on("connection", (ws) => { this.wss.on("connection", (ws) => {
if (!policies.allowGatewayConnection) return ws.close(4007, "Disallowed by policy.");
// Send HELLO message as soon as the client connects // Send HELLO message as soon as the client connects
ws.send(packet("HELLO", {})); ws.send(packet("HELLO", {}));
ws.session = { ws.session = {

View file

@ -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>

View file

@ -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>

View file

@ -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"));
});

View file

@ -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>

View file

@ -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
View 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>

View file

@ -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);
}
}
});

View file

@ -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;
}
}
}
});

View file

@ -1,14 +1,37 @@
module.exports = { module.exports = {
ports: {
mainServerPort: 3005,
},
address: "localhost",
//restrictions: {
// signup: {
// specialCode: ''
// }
//},
mongoUrl: "mongodb://127.0.0.1:27017/app", 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, bcryptRounds: 10,
roleMap: { roleMap: {
"BANNED": 0, "BANNED": 0,
@ -17,14 +40,4 @@ module.exports = {
"BOT": 3, "BOT": 3,
"ADMIN": 4 "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`
];