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,
|
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() });
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {
|
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`
|
|
||||||
];
|
|
||||||
|
|
Reference in a new issue