Initial commit
This commit is contained in:
commit
c87c7f98d3
19 changed files with 2498 additions and 0 deletions
64
api/v1/authfunctions.js
Executable file
64
api/v1/authfunctions.js
Executable file
|
@ -0,0 +1,64 @@
|
|||
const User = require('../../models/User');
|
||||
const secret = require('../../secret');
|
||||
const config = require('../../config');
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const redirect = (res, status=401, url=undefined) => {
|
||||
if (!url) {
|
||||
res.status(status).json({
|
||||
error: true,
|
||||
message: 'ERROR_ACCESS_DENIED'
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.redirect(url);
|
||||
}
|
||||
|
||||
function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config.roleMap.RESTRICTED) {
|
||||
return (req, res) => {
|
||||
const token = req.cookies.token;
|
||||
if (!token) {
|
||||
redirect(res, 403, url);
|
||||
return;
|
||||
}
|
||||
|
||||
jwt.verify(token, secret.jwtPrivateKey, {}, async (err, data) => {
|
||||
if (err) {
|
||||
redirect(res, 401, url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
redirect(res, 401, url);
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.username) {
|
||||
redirect(res, 401, url);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await User.findByUsername(data.username);
|
||||
|
||||
if (!user) {
|
||||
redirect(res, 401, url);
|
||||
return;
|
||||
}
|
||||
|
||||
let permissionLevel = config.roleMap[user.role];
|
||||
if (!permissionLevel) {
|
||||
permissionLevel = 0;
|
||||
}
|
||||
|
||||
if (permissionLevel < minPermissionLevel) {
|
||||
redirect(res, 401, url);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(req, res, user);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { authenticateEndpoint };
|
140
api/v1/content.js
Executable file
140
api/v1/content.js
Executable file
|
@ -0,0 +1,140 @@
|
|||
const User = require('../../models/User');
|
||||
const Category = require('../../models/Category');
|
||||
const Post = require('../../models/Post');
|
||||
const config = require('../../config');
|
||||
const secret = require('../../secret');
|
||||
|
||||
const { authenticateEndpoint } = require('./authfunctions');
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const { body, query, param, validationResult } = require('express-validator');
|
||||
const express = require('express');
|
||||
|
||||
const app = express.Router();
|
||||
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const createLimiter = rateLimit({
|
||||
windowMs: 2 * 60 * 1000,
|
||||
max: 3,
|
||||
});
|
||||
|
||||
app.post('/category/create', [
|
||||
createLimiter,
|
||||
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape()
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const title = req.body.title;
|
||||
const category = await Category.create({
|
||||
title: title,
|
||||
creator: user._id,
|
||||
posts: []
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_CATEGORY_CREATED',
|
||||
category: {
|
||||
title: category.title,
|
||||
creator: category.creator,
|
||||
posts: category.posts
|
||||
}
|
||||
});
|
||||
}, undefined, config.roleMap.USER));
|
||||
|
||||
app.post('/post/create', [
|
||||
createLimiter,
|
||||
body('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
|
||||
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
|
||||
body('body').not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const category = req.body.category;
|
||||
const title = req.body.title;
|
||||
const content = req.body.body;
|
||||
|
||||
const post = new Post();
|
||||
post.title = title;
|
||||
post.body = content;
|
||||
post.creator = user._id;
|
||||
post.category = category;
|
||||
|
||||
const r = await Category.updateOne({
|
||||
_id: category
|
||||
}, {
|
||||
$push: { posts: post }
|
||||
});
|
||||
|
||||
if (r.n < 1) {
|
||||
res.status(404).json({
|
||||
error: true,
|
||||
message: 'ERROR_CATEGORY_NOT_FOUND'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_POST_CREATED'
|
||||
});
|
||||
}, undefined, config.roleMap.USER));
|
||||
|
||||
app.get('/category/:category/info', [
|
||||
param('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryId = req.params.category;
|
||||
const category = await Category.findById(categoryId).populate('posts.creator');
|
||||
|
||||
if (!category) {
|
||||
res.status(404).json({
|
||||
error: true,
|
||||
message: 'ERROR_CATEGORY_NOT_FOUND'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_CATEGORY_DATA_FETCHED',
|
||||
category: {
|
||||
title: category.title,
|
||||
creator: category.creator,
|
||||
posts: category.posts
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
app.get('/category/list', authenticateEndpoint(async (req, res, user) => {
|
||||
let count = parseInt(req.query.count);
|
||||
if (!Number.isInteger(count)) {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
// TODO: This is probably not efficient
|
||||
const categories = await Category.find().sort({ _id: -1 }).limit(count).select('-posts -__v').populate('creator', 'username _id');
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_CATEGORY_LIST_FETCHED',
|
||||
categories
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = app;
|
16
api/v1/index.js
Executable file
16
api/v1/index.js
Executable file
|
@ -0,0 +1,16 @@
|
|||
const usersAPI = require('./users');
|
||||
const contentAPI = require('./content');
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const app = express.Router();
|
||||
|
||||
app.use('/users', usersAPI);
|
||||
app.use('/content', contentAPI);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
// TODO: Add more checks for this, or maybe remove
|
||||
res.json({ apiStatus: 'OK', apis: [ 'users', 'content' ] });
|
||||
});
|
||||
|
||||
module.exports = app;
|
191
api/v1/users.js
Executable file
191
api/v1/users.js
Executable file
|
@ -0,0 +1,191 @@
|
|||
const User = require('../../models/User');
|
||||
const config = require('../../config');
|
||||
const secret = require('../../secret');
|
||||
|
||||
const { authenticateEndpoint } = require('./authfunctions');
|
||||
|
||||
// TODO: Might want to use something else (https://blog.benpri.me/blog/2019/01/13/why-you-shouldnt-be-using-bcrypt-and-scrypt/)
|
||||
const bcrypt = require('bcrypt');
|
||||
const mongoose = require('mongoose');
|
||||
const { body, query, param, validationResult } = require('express-validator');
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const app = express.Router();
|
||||
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const createAccountLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour window
|
||||
max: 5, // start blocking after 5 requests
|
||||
message: "You are being rate limited"
|
||||
});
|
||||
|
||||
app.post('/account/create', [
|
||||
createAccountLimiter,
|
||||
body('username').not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
|
||||
body('email').not().isEmpty().isEmail().normalizeEmail(),
|
||||
body('password').not().isEmpty().isLength({ min: 8, max: 128 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const username = req.body.username;
|
||||
|
||||
const existingUser = await User.findByUsername(username);
|
||||
if (existingUser) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_USERNAME_EXISTS', errors: [{ value: username, msg: 'Username exists', param: 'username', location: 'body' }] });
|
||||
return;
|
||||
}
|
||||
|
||||
const unhashedPassword = req.body.password;
|
||||
const email = req.body.email;
|
||||
const startingRole = 'USER';
|
||||
|
||||
|
||||
const hashedPassword = await bcrypt.hash(unhashedPassword, config.bcryptRounds);
|
||||
|
||||
const user = await User.create({
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role: startingRole
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_USER_CREATED',
|
||||
user: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
permissionLevel: config.roleMap[user.role]
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Internal server error', e);
|
||||
res.status(500).json({ error: true, message: 'INTERNAL_SERVER_ERROR' });
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.post('/token/create', [
|
||||
createAccountLimiter,
|
||||
body('username').not().isEmpty().trim().isAlphanumeric(),
|
||||
body('password').not().isEmpty()
|
||||
], async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
|
||||
return;
|
||||
}
|
||||
|
||||
const username = req.body.username;
|
||||
|
||||
const existingUser = await User.findByUsername(username);
|
||||
if (!existingUser) {
|
||||
res.status(403).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
|
||||
return;
|
||||
}
|
||||
|
||||
const password = req.body.password;
|
||||
|
||||
let passwordCheck;
|
||||
try {
|
||||
passwordCheck = await bcrypt.compare(password, existingUser.password);
|
||||
} catch(e) {
|
||||
passwordCheck = false;
|
||||
}
|
||||
if (!passwordCheck) {
|
||||
res.status(403).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
|
||||
return;
|
||||
}
|
||||
|
||||
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: '3h' }, (err, token) => {
|
||||
if (err) {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
message: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_TOKEN_CREATED',
|
||||
user: {
|
||||
_id: existingUser._id,
|
||||
username: existingUser.username,
|
||||
email: existingUser.email,
|
||||
role: existingUser.role,
|
||||
permissionLevel: config.roleMap[existingUser.role]
|
||||
},
|
||||
token
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/current/info', authenticateEndpoint((req, res, user) => {
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_USER_DATA_FETCHED',
|
||||
user: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
permissionLevel: config.roleMap[user.role]
|
||||
},
|
||||
});
|
||||
}, undefined, 0));
|
||||
|
||||
app.get('/user/:userid/info', [
|
||||
param('userid').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
|
||||
], authenticateEndpoint(async (req, res, user) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const userid = req.params.userid;
|
||||
if (!userid) {
|
||||
res.sendStatus(400).json({
|
||||
error: true,
|
||||
message: 'ERROR_REQUEST_INVALID_DATA'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const otherUser = await User.findById(userid);
|
||||
|
||||
res.status(200).json({
|
||||
error: false,
|
||||
message: 'SUCCESS_USER_DATA_FETCHED',
|
||||
user: {
|
||||
_id: otherUser._id,
|
||||
username: otherUser.username,
|
||||
role: otherUser.role
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
app.post('/browser/token/clear', authenticateEndpoint((req, res, user) => {
|
||||
res.clearCookie('token');
|
||||
res.sendStatus(200);
|
||||
}));
|
||||
|
||||
module.exports = app;
|
57
apitest.rest
Executable file
57
apitest.rest
Executable file
|
@ -0,0 +1,57 @@
|
|||
POST http://localhost:3000/api/v1/users/account/create
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "test",
|
||||
"password": "testtesttest",
|
||||
"email": "test@test.test"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:3000/api/v1/users/token/create
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "test",
|
||||
"password": "testtesttest"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET http://localhost:3000/api/v1/users/current/info
|
||||
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2MDA1MTA2MTEsImV4cCI6MTYwMDUyMTQxMX0.q85p94FLPR4fxZ4O5pmalEEjU9Hyr9js63u6LgoCQCw
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:3000/api/v1/content/category/create
|
||||
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2MDA1MTA2MTEsImV4cCI6MTYwMDUyMTQxMX0.q85p94FLPR4fxZ4O5pmalEEjU9Hyr9js63u6LgoCQCw
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "testing1"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:3000/api/v1/content/post/create
|
||||
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2MDA1MTA2MTEsImV4cCI6MTYwMDUyMTQxMX0.q85p94FLPR4fxZ4O5pmalEEjU9Hyr9js63u6LgoCQCw
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"category": "5f65e1f05c3cdd86400f43ec",
|
||||
"title": "Test title",
|
||||
"content": "Test content!!!"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET http://localhost:3000/api/v1/content/category/5f65e1f05c3cdd86400f43ec/info
|
||||
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2MDA1MTA2MTEsImV4cCI6MTYwMDUyMTQxMX0.q85p94FLPR4fxZ4O5pmalEEjU9Hyr9js63u6LgoCQCw
|
||||
Content-Type: application/json
|
||||
|
||||
###
|
||||
|
||||
GET http://localhost:3000/api/v1/content/category/list
|
||||
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2MDA1MTA2MTEsImV4cCI6MTYwMDUyMTQxMX0.q85p94FLPR4fxZ4O5pmalEEjU9Hyr9js63u6LgoCQCw
|
||||
Content-Type: application/json
|
138
app/app.html
Executable file
138
app/app.html
Executable file
|
@ -0,0 +1,138 @@
|
|||
<!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="./resources/js/app.js"></script>
|
||||
|
||||
<style>
|
||||
.md-card {
|
||||
width: 312px;
|
||||
margin: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div v-if="showApp">
|
||||
<md-dialog id="create-category-dialog" :md-active.sync="dialog.show.createCategory">
|
||||
<md-dialog-title>Create category</md-dialog-title>
|
||||
|
||||
<md-dialog-content>
|
||||
<md-field>
|
||||
<label>Title</label>
|
||||
<md-input v-model="dialog.text.createCategory.title"></md-input>
|
||||
</md-field>
|
||||
|
||||
<md-dialog-actions>
|
||||
<md-button @click="dialog.show.createCategory = false">Close</md-button>
|
||||
<md-button class="md-primary" @click="createCategory()">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.category.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()">Manage account</md-menu-item>
|
||||
</md-menu-content>
|
||||
</md-menu>
|
||||
</md-toolbar>
|
||||
|
||||
<md-toolbar v-show="selection.category.browsing" class="md-dense" md-elevation="5">
|
||||
<h3 v-if="selection.category.isCategory" class="md-title" style="flex: 1">Browsing category: {{ selection.category.title }}</h3>
|
||||
<h3 v-if="!selection.category.isCategory" class="md-title" style="flex: 1">Browsing {{ selection.category.title }}</h3>
|
||||
<md-button @click="browseCategories()" v-if="selection.category.isCategory">Back</md-button>
|
||||
<md-button @click="refresh()">Refresh</md-button>
|
||||
</md-toolbar>
|
||||
|
||||
<div id="posts-container" v-if="selection.category.browsing">
|
||||
<md-card v-for="post in selection.posts">
|
||||
<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)">{{ post.creator.username }}</a></span>
|
||||
</md-card-header>
|
||||
|
||||
<md-card-content v-html="post.body"></md-card-content>
|
||||
|
||||
<md-card-actions>
|
||||
<md-button v-for="button in cardButtons" @click="button.click(post)">{{ button.text }}</md-button>
|
||||
</md-card-actions>
|
||||
</md-card>
|
||||
</div>
|
||||
|
||||
<md-speed-dial class="md-fixed md-bottom-right" md-direction="top" md-elevation="5" style="z-index: 4000;">
|
||||
<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.category.isCategory" 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.createCategory = true">
|
||||
<md-icon>category</md-icon>
|
||||
<md-tooltip md-direction="left">Create a new category</md-tooltip>
|
||||
</md-button>
|
||||
</md-speed-dial-content>
|
||||
</md-speed-dial>
|
||||
</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>
|
71
app/auth.html
Executable file
71
app/auth.html
Executable file
|
@ -0,0 +1,71 @@
|
|||
<!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>
|
||||
</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 === 'SIGNUP'" 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>
|
0
app/resources/css/base.css
Executable file
0
app/resources/css/base.css
Executable file
357
app/resources/js/app.js
Executable file
357
app/resources/js/app.js
Executable file
|
@ -0,0 +1,357 @@
|
|||
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 'category': {
|
||||
return 'Invalid category. Something went wrong.';
|
||||
}
|
||||
default: {
|
||||
return 'Invalid value sent to server. Something went wrong.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'ERROR_CATEGORY_NOT_FOUND': {
|
||||
return 'The category 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 getCreateCategoryError = (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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
showSnackbarNotification: false,
|
||||
snackbarNotification: '',
|
||||
snackbarNotificationDuration: 999999,
|
||||
snackbarButtonText: 'Ok',
|
||||
loggedInUser: {},
|
||||
showApp: false,
|
||||
menuVisible: false,
|
||||
selection: {
|
||||
category: {
|
||||
title: '',
|
||||
browsing: false,
|
||||
_id: undefined,
|
||||
isCategory: false
|
||||
},
|
||||
posts: []
|
||||
},
|
||||
cardButtons: [],
|
||||
dialog: {
|
||||
show: {
|
||||
createPost: false,
|
||||
createCategory: false
|
||||
},
|
||||
text: {
|
||||
createPost: {
|
||||
title: '',
|
||||
body: ''
|
||||
},
|
||||
createCategory: {
|
||||
title: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
viewingProfile: {
|
||||
show: false,
|
||||
_id: '',
|
||||
username: '',
|
||||
role: ''
|
||||
}
|
||||
},
|
||||
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.showApp = true;
|
||||
this.browseCategories();
|
||||
} 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: {
|
||||
navigateToAccountManager() {
|
||||
window.location.href = `${window.location.origin}/auth.html`;
|
||||
},
|
||||
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;
|
||||
},
|
||||
button: function(text, click) {
|
||||
this.cardButtons.push({ text, click });
|
||||
},
|
||||
refresh: function() {
|
||||
if (this.selection.category.title === 'categories' && this.selection.category.isCategory === false) {
|
||||
this.browseCategories();
|
||||
} else {
|
||||
this.browse(this.selection.category);
|
||||
}
|
||||
},
|
||||
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.status === 200) {
|
||||
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.resetSnackbarButton();
|
||||
this.notification('Failed to fetch user data');
|
||||
}
|
||||
},
|
||||
stopBrowsing: function() {
|
||||
this.selection.category = {
|
||||
title: '',
|
||||
browsing: false,
|
||||
_id: undefined,
|
||||
isCategory: false
|
||||
};
|
||||
this.selection.posts = [];
|
||||
this.cardButtons = [];
|
||||
},
|
||||
showCreatePostDialog: function() {
|
||||
if (!this.selection.category.isCategory) {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('You are not in a category');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog.show.createPost = true;
|
||||
},
|
||||
createPost: async function() {
|
||||
if (!this.selection.category.isCategory) {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('You are not in a category');
|
||||
return;
|
||||
}
|
||||
|
||||
const category = this.selection.category;
|
||||
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({
|
||||
category: category._id,
|
||||
title: input.title,
|
||||
body: input.body
|
||||
})
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('You are not allowed to do that');
|
||||
return;
|
||||
}
|
||||
if (res.status === 429) {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('Chill! You are posting too much!');
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
this.resetSnackbarButton();
|
||||
this.notification(getCreatePostError(json));
|
||||
return;
|
||||
} else {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('Successfully created post');
|
||||
this.dialog.show.createPost = false;
|
||||
this.browse(this.selection.category);
|
||||
return;
|
||||
}
|
||||
},
|
||||
createCategory: async function() {
|
||||
const input = this.dialog.text.createCategory;
|
||||
|
||||
const res = await fetch(`${window.location.origin}/api/v1/content/category/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
title: input.title
|
||||
})
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('You are not allowed to do that');
|
||||
return;
|
||||
}
|
||||
if (res.status === 429) {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('Chill! You are posting too much!');
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
this.resetSnackbarButton();
|
||||
this.notification(getCreateCategoryError(json));
|
||||
return;
|
||||
} else {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('Successfully created category');
|
||||
this.dialog.show.createCategory = false;
|
||||
this.browseCategories();
|
||||
return;
|
||||
}
|
||||
},
|
||||
browse: async function(category) {
|
||||
const { _id, title } = category;
|
||||
|
||||
const res = await fetch(`${window.location.origin}/api/v1/content/category/${_id}/info`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
const json = await res.json();
|
||||
|
||||
this.selection.category.title = title;
|
||||
this.selection.category._id = _id;
|
||||
this.selection.category.browsing = true;
|
||||
this.selection.category.isCategory = true;
|
||||
this.selection.posts = [];
|
||||
|
||||
this.cardButtons = [];
|
||||
|
||||
for (let i = 0; i < json.category.posts.length; i++) {
|
||||
const v = json.category.posts[i];
|
||||
this.selection.posts.push({ title: v.title, body: v.body, _id: v._id, creator: v.creator });
|
||||
}
|
||||
} else {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('Failed to fetch category');
|
||||
}
|
||||
},
|
||||
browseCategories: async function() {
|
||||
const res = await fetch(`${window.location.origin}/api/v1/content/category/list?count=50`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
const json = await res.json();
|
||||
|
||||
this.selection.category.title = 'categories';
|
||||
this.selection.category.browsing = true;
|
||||
this.selection.category.isCategory = false;
|
||||
this.selection.posts = [];
|
||||
|
||||
this.cardButtons = [];
|
||||
|
||||
this.button('View', (post) => {
|
||||
this.browse(post);
|
||||
});
|
||||
|
||||
for (let i = 0; i < json.categories.length; i++) {
|
||||
const v = json.categories[i];
|
||||
this.selection.posts.push({ title: v.title, body: '', _id: v._id, creator: v.creator });
|
||||
}
|
||||
} else {
|
||||
this.resetSnackbarButton();
|
||||
this.notification('Failed to fetch category list');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
227
app/resources/js/auth.js
Executable file
227
app/resources/js/auth.js
Executable file
|
@ -0,0 +1,227 @@
|
|||
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.';
|
||||
}
|
||||
|
||||
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: '',
|
||||
mode: 'SIGNUP',
|
||||
showSnackbarNotification: false,
|
||||
snackbarNotification: '',
|
||||
snackbarNotificationDuration: 999999,
|
||||
snackbarButtonText: 'Ok',
|
||||
successfulLogin: false,
|
||||
loggedInUser: {}
|
||||
},
|
||||
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 {
|
||||
this.mode = 'SIGNUP';
|
||||
}
|
||||
},
|
||||
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 '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() {
|
||||
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({
|
||||
username: this.usernameInput,
|
||||
email: this.emailInput,
|
||||
password: this.passwordInput
|
||||
})
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
14
config.js
Executable file
14
config.js
Executable file
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
ports: {
|
||||
mainServerPort: 25,
|
||||
},
|
||||
address: '188.25.251.46',
|
||||
mongoUrl: 'mongodb://localhost:27017/app',
|
||||
bcryptRounds: 10,
|
||||
roleMap: {
|
||||
'BANNED': 0,
|
||||
'RESTRICTED': 1,
|
||||
'USER': 2,
|
||||
'ADMIN': 3
|
||||
}
|
||||
};
|
33
index.js
Executable file
33
index.js
Executable file
|
@ -0,0 +1,33 @@
|
|||
const config = require('./config');
|
||||
const apiRoute = require('./api/v1');
|
||||
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const cors = require('cors')
|
||||
|
||||
const { authenticateEndpoint } = require('./api/v1/authfunctions');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(cors({
|
||||
origin: `http://${config.address}`,
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200
|
||||
}));
|
||||
app.use('/api/v1', apiRoute);
|
||||
app.use(express.static('app'));
|
||||
|
||||
app.get('/', authenticateEndpoint((req, res, user) => {
|
||||
res.redirect('/app.html');
|
||||
}, `/auth.html`));
|
||||
|
||||
app.get('/admin', (req, res) => {
|
||||
res.send('Keanu chungus wholesome 100 reddit moment 😀i beat up a kid that said minecraft bad 😂and my doggo bit him so i gave him snaccos😉 and we watched pewdiepie together while in elon musk’s cyber truck 😳talking about how superior reddit memers are : “haha emojis bad” 😲i said and keanu reeves came outta nowhere and said “this is wholesome 100, updoot this wholesome boy” 😗so i got alot of updoots and edit: thanks for the gold kind stranger😣. but the kind stranger revealed himself to be baby yoda eating chiccy nuggies😨 and drinking choccy milk😎 so we went to the cinema to see our (communism funny) favorite movies avengers endgame😆 but then thor played fortnite and fortnite bad😡, so then i said “reality is often dissappointing” and then baby yoda replied r/unexpectedthanos and i replied by r/expectedthanos😖 for balance and then danny devito came to pick us up from the cinema😩 and all the insta normies and gay mods stood watching😵 ,as we,superior redditors went home with danny devito to suck on his magnum dong😫 but i said no homo and started sucking,not like those gay mods😮,then the next morning we woke up to MrBeast telling us to plant 69420 million trees😌, me, baby yoda and danny said nice, and then on our way to plant 69420 million trees😊 (nice) we saw a kid doing a tiktok so keanu reeves appeared and said “we have a kid to burn” and i replied “you’re breathtaking”😄 so i said “i need a weapon” and baby yoda gave me an RPG so i blew the kid (DESTRUCTION 100)😎 and posted it on r/memes and r/dankmemes and r/pewdiepiesubmissions and got 1000000000 updoots😘,i’m sure pewds will give me a big pp, then we shat on emoji users😂😂 and started dreaming about girls that will never like me😢 and posted a lie on r/teenagers about how i got a GF after my doggo died by the hands of fortnite players😳 so i exploited his death for updoots😜, but i watched the sunset with the wholesome gang😁 (keanu,danny,Mrbeast, pewds, spongebob,stefan karl , bob ross, steve irwin, baby yoda and other artists that reddit exploits them) [Everyone liked that] WHOLESOME 100 REDDIT 100🤡');
|
||||
});
|
||||
|
||||
app.listen(config.ports.mainServerPort, () => {
|
||||
console.log(`Main server is listening on port ${config.ports.mainServerPort}`);
|
||||
});
|
10
models/Category.js
Executable file
10
models/Category.js
Executable file
|
@ -0,0 +1,10 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Post = require('./Post');
|
||||
|
||||
const Category = mongoose.model('Category', {
|
||||
title: String,
|
||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
||||
posts: [Post.schema]
|
||||
});
|
||||
|
||||
module.exports = Category;
|
10
models/Post.js
Executable file
10
models/Post.js
Executable file
|
@ -0,0 +1,10 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const Post = mongoose.model('Post', {
|
||||
title: String,
|
||||
body: String,
|
||||
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
|
||||
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: 'Category'}
|
||||
});
|
||||
|
||||
module.exports = Post;
|
14
models/User.js
Executable file
14
models/User.js
Executable file
|
@ -0,0 +1,14 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const User = mongoose.model('User', {
|
||||
username: String,
|
||||
password: String,
|
||||
email: String,
|
||||
role: String
|
||||
});
|
||||
|
||||
User.findByUsername = async function(username) {
|
||||
return await User.findOne({ username }).exec();
|
||||
};
|
||||
|
||||
module.exports = User;
|
3
notes.txt
Executable file
3
notes.txt
Executable file
|
@ -0,0 +1,3 @@
|
|||
vulns:
|
||||
- you can send a malformed request and express will send the full error
|
||||
- 2 users can have the same name if one of the letters is uppercase
|
1128
package-lock.json
generated
Executable file
1128
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load diff
22
package.json
Executable file
22
package.json
Executable file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "dictionar",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"express-validator": "^6.6.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^5.10.0"
|
||||
}
|
||||
}
|
3
secret.js.template
Executable file
3
secret.js.template
Executable file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
jwtPrivateKey: 'KEY'
|
||||
};
|
Reference in a new issue