extra routes and stuff

This commit is contained in:
Troplo 2020-08-12 00:09:54 +10:00
parent 197c5e1473
commit 9725247bbd
18 changed files with 103 additions and 331 deletions

View File

@ -5,7 +5,8 @@
"database": "kaverti",
"host": "127.0.0.1",
"dialect": "mysql",
"maintenance": "true"
"maintenance": "true",
"passkey": "true"
},
"test": {
"username": "root",
@ -13,7 +14,8 @@
"database": "kaverti",
"host": "127.0.0.1",
"dialect": "mysql",
"maintenance": "true"
"maintenance": "true",
"passkey": "true"
},
"production": {
"username": "root",
@ -21,6 +23,7 @@
"database": "kaverti",
"host": "127.0.0.1",
"dialect": "mysql",
"maintenance": "false"
"maintenance": "false",
"passkey": "true"
}
}

View File

@ -49,6 +49,9 @@ font-size: 24px !important;
<p style='margin-top: 0;' v-if='$store.state.token'>
<strong>Providing the state token is still valid, this will create an admin account</strong>
</p>
<p style='margin-top: 0;' v-if='$store.state.passkey'>
<strong>Providing a PassKey, this will create a normal user account</strong>
</p>
<p style='margin-top: 0;' v-else>
Sign up to {{name}} today!
<br/>It's fast and easy.
@ -352,7 +355,8 @@ font-size: 24px !important;
this.signup.email = ''
this.$store.commit('setToken', null)
},
this.$store.commit('setPassKey', null)
},
clearSignupErrors () {
this.signup.errors.username = ''
this.signup.errors.hash = ''
@ -386,6 +390,9 @@ font-size: 24px !important;
postParams.admin = true
postParams.token = this.$store.state.token
}
if(this.$store.state.passkey) {
postParams.passkey = this.$store.state.passkey
}
if(this.signup.password !== this.signup.confirmPassword) {
this.signup.errors.confirmPassword = 'Passwords must match'

View File

@ -261,6 +261,11 @@
this.$store.commit('setAccountTabs', 0)
this.$store.commit('setAccountModalState', true)
}
if(this.$route.query.passkey) {
this.$store.commit('setPassKey', this.$route.query.passkey)
this.$store.commit('setAccountTabs', 0)
this.$store.commit('setAccountModalState', true)
}
logger('index')
},

View File

@ -55,7 +55,6 @@ const Index = () => import('./components/routes/Index')
const HomeUnauthenticated = () => import('./components/routes/HomeUnauthenticated')
const P = () => import('./components/routes/P')
const Start = () => import('./components/routes/Start')
const Thread = () => import('./components/routes/Thread')
const ThreadNew = () => import('./components/routes/ThreadNew')
const Games = () => import('./components/routes/Games')
@ -133,8 +132,9 @@ const router = new VueRouter({
{ path: '/', component: HomeUnauthenticated },
{ path: '/category/:category', component: Index },
{ path: '/p/:id', component: P },
{ path: '/start', component: Start },
{ path: '/thread/:slug/:id', component: Thread },
{ path: '/start', component: Reserved },
{ path: '/experiments', component: Reserved },
{ path: '/thread/:slug/:id', component: Thread },
{ path: '/thread/:slug/:id/:post_number', name: 'thread-post', component: Thread },
{ path: '/thread/new', component: ThreadNew },
{ path: '/marketplace', component: Reserved },

View File

@ -22,8 +22,9 @@ export default new Vuex.Store({
admin: false,
token: null,
passkey: null,
show404Page: false,
show404Page: false,
ajaxErrors: [],
ajaxErrorsModal: false,
@ -75,6 +76,9 @@ export default new Vuex.Store({
setToken (state, token) {
state.token = token
},
setPassKey (state, passkey) {
state.passkey = passkey
},
set404Page (state, value) {
state.show404Page = value
},

View File

@ -48,7 +48,11 @@ let Errors = {
401
],
invalidToken: [
'Your session token is invalid, If this issue persists, please contact Kaverti support.',
'Your QACT (Quick Admin Creation Token) is invalid.',
401
],
invalidPassKey: [
'Your PassKey is invalid.',
401
],
noSettings: [

0
lib/ratelimit.js Normal file
View File

View File

@ -1,24 +0,0 @@
module.exports = {
up(queryInterface, Sequelize) {
return Promise.all([
queryInterface.renameColumn(
'settings',
'siteName',
'siteName',
),
queryInterface.renameColumn(
'settings',
'forumDescription',
'siteDesc',
),
queryInterface.addColumn(
'settings',
'createdAt',
),
queryInterface.addColumn(
'settings',
'updatedAt',
),
]);
},
};

View File

@ -1,22 +0,0 @@
module.exports = {
up(queryInterface, Sequelize) {
return Promise.all([
queryInterface.addColumn(
'settings',
'created',
{
type: Sequelize.DATE,
allowNull: false
},
),
queryInterface.addColumn(
'settings',
'updated',
{
type: Sequelize.DATE,
allowNull: false
},
),
]);
},
};

View File

@ -1,16 +0,0 @@
module.exports = {
up(queryInterface, Sequelize) {
return Promise.all([
queryInterface.renameColumn(
'settings',
'created',
'createdAt',
),
queryInterface.renameColumn(
'settings',
'updated',
'updatedAt',
),
]);
},
};

View File

@ -0,0 +1,30 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('passkey', {
code: {
type: Sequelize.STRING(191),
primaryKey: true,
required: true
},
createdAt: Sequelize.DATE,
uses: {
type: Sequelize.BOOLEAN,
primaryKey: true,
required: true
},
maxUses: {
type: Sequelize.BOOLEAN,
primaryKey: true,
required: true
}
}, {
charset: 'utf8mb4'
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('passkey');
}
};

View File

@ -1,120 +0,0 @@
let urlSlug = require('url-slug')
module.exports = (sequelize, DataTypes) => {
let Thread = sequelize.define('BlogPost', {
name: {
type: DataTypes.TEXT,
set (val) {
this.setDataValue('name', val)
if(val) {
this.setDataValue(
'slug',
//if you don't covert to lowercase it doesn't
//correctly slugify diacritics, e.g. thrËad
//becomes 'thr-ead' not 'thread'
urlSlug(val.toString().toLowerCase() || '') || '_'
)
}
},
allowNull: false,
validate: {
notEmpty: {
msg: 'Did you forget something? (Fill this in!)'
},
len: {
args: [4, 256],
msg: 'The title must be between 4 and 256 characters'
},
isString (val) {
if(typeof val !== 'string') {
throw new sequelize.ValidationError('The title must be a string')
}
}
}
},
slug: DataTypes.TEXT,
}, {
instanceMethods: {
getMeta (limit) {
let meta = {}
let blogposts = this.Blogposts
let firstPost = posts[0]
let lastPost = posts.slice(-1)[0]
//next url
if(!lastPost || lastPost.postNumber+1 === this.postsCount) {
meta.nextURL = null
} else {
meta.nextURL =
`/api/v1/blog/post/${this.id}?limit=${limit}&from=${lastPost.postNumber + 1}`
}
//previous url
if(!firstPost || firstPost.postNumber === 0) {
meta.previousURL = null
} else if(firstPost.postNumber - limit < 0) {
meta.previousURL =
`/api/v1/blog/post/${this.id}?limit=${firstPost.postNumber}&from=0`
} else {
meta.previousURL =
`/api/v1/blog/post/${this.id}?limit=${limit}&from=${firstPost.postNumber - limit}`
}
//remaining posts
if(lastPost === undefined) {
meta.nextPostsCount = 0
meta.previousPostsCount = 0
meta.postsRemaining = 0
} else {
let postsRemaining =
this.postsCount - lastPost.postNumber - 1
meta.postsRemaining = postsRemaining
if(postsRemaining < limit) {
meta.nextPostsCount = postsRemaining
} else {
meta.nextPostsCount = limit
}
if(firstPost.postNumber === 0) {
meta.previousPostsCount = 0
} else if(firstPost.postNumber - limit < 0) {
meta.previousPostsCount = firstPost.postNumber
} else {
meta.previousPostsCount = limit
}
}
return meta
}
},
classMethods: {
associate (models) {
Thread.belongsTo(models.User)
Thread.hasMany(models.Post, { foreignKeyConstraint: true, onDelete: 'CASCADE' })
},
includeOptions (from, limit) {
let models = sequelize.models
return [
{ model: models.User, attributes: ['username', 'createdAt', 'color', 'picture', 'updatedAt', 'id'] },
{
model: models.Post,
where: { postNumber: { $gte: from } },
order: [['id', 'ASC']],
limit,
include: [
{ model: models.Thread, attributes: ['slug'] },
{ model: models.User, as: 'Likes', attributes: ['username', 'createdAt', 'id', 'color', 'picture'] },
{ model: models.User, attributes: ['username', 'createdAt', 'id', 'color', 'picture', 'admin'] },
]
}
]
}
}
})
return Thread
}

View File

@ -24,6 +24,7 @@
"dompurify": "^2.0.12",
"ejs": "^2.5.7",
"express": "^4.16.4",
"express-rate-limit": "^5.1.3",
"express-recaptcha": "^5.0.2",
"express-session": "^1.15.6",
"helmet": "^3.23.3",

View File

@ -1,137 +0,0 @@
let express = require('express')
let router = express.Router()
const Errors = require('../lib/errors.js')
let { User, BlogPost, Sequelize } = require('../models')
let pagination = require('../lib/pagination.js')
router.get('/:blogpost_id', async (req, res, next) => {
try {
let { from, limit } = pagination.getPaginationProps(req.query)
let blogpost = await BlogPost.findById(req.params.blogpost_id, {
include: BlogPost.includeOptions(from, limit)
})
if(!blogpost) throw Errors.invalidParameter('id', 'blogpost does not exist')
let meta = blogpost.getMeta(limit)
res.json(Object.assign( blogpost.toJSON(), { meta } ))
} catch (e) { next(e) }
})
//Only logged in routes
router.all('*', (req, res, next) => {
if(req.session.loggedIn) {
next()
} else {
res.status(401)
res.json({
errors: [Errors.requestNotAuthorized]
})
}
})
router.post('/', async (req, res, next) => {
let validationErrors = []
try {
if(!req.session.admin) {
res.status(401)
res.json({
errors: [Errors.requestNotAuthorized]
})}
let user = await User.findOne({ where: {
username: req.session.username
}})
let blogpost = await BlogPost.create({
name: req.body.name
})
await blogpost.setCategory("Blog")
await blogpost.setUser(user)
res.json(await blogpost.reload({
include: [
{ model: User, attributes: ['username', 'createdAt', 'updatedAt', 'id'] },
Category
]
}))
req.app.get('io').to('index').emit('new blogpost', {
name: "Blog",
value: category.value
})
} catch (e) { next(e) }
})
//Only admin routes
router.all('*', (req, res, next) => {
if(req.session.admin) {
next()
} else {
res.status(401)
res.json({
errors: [Errors.requestNotAuthorized]
})
}
})
router.delete('/:blogpost_id', async (req, res, next) => {
try {
let blogpost = await BlogPost.findById(req.params.blogpost_id)
if(!blogpost) {
throw Errors.sequelizeValidation(Sequelize, {
error: 'invalid blogpost id',
value: req.params.blogpost_id
})
} else {
//Find all posts with reports and get reports
//Then delete those reports
//Temporary fix because cascade is not working
let posts = await Post.findAll({
where: {
BlogPostId: blogpost.id
},
})
let reports = posts
.map(post => post.Reports)
.reduce((a, b) => a.concat(b), [])
let destroyPromises = reports.map(report => report.destroy())
await Promise.all(destroyPromises)
await Post.destroy({ where: { BlogPostId: blogpost.id } })
await blogpost.destroy()
res.json({ success: true })
}
} catch (e) { next(e) }
})
router.put('/:blogpost_id', async (req, res, next) => {
try {
let blogpost = await BlogPost.findById(req.params.blogpost_id)
if(!blogpost) {
res.status(400)
res.json({ errors:
[Errors.invalidParameter('blogpostId', 'blogpost does not exist')]
})
} else {
if(req.body.locked) {
await blogpost.update({ locked: true })
} else {
await blogpost.update({ locked: false })
}
res.json({ success: true })
}
} catch (e) { next(e) }
})
module.exports = router

View File

@ -8,7 +8,7 @@ var reCAPTCHASecret = "6LdlbrwZAAAAAKvtcVQhVl_QaNOqmQ4PgyW3SKHy";
const Errors = require('../lib/errors.js')
var format = require('date-format');
let {
User, Post, ProfilePicture, AdminToken, Thread, Category, Sequelize, Ip, Ban, sequelize
User, Post, ProfilePicture, AdminToken, PassKey, Thread, Category, Sequelize, Ip, Ban, sequelize
} = require('../models')
let pagination = require('../lib/pagination.js')

32
routes/user_passkey.js Normal file
View File

@ -0,0 +1,32 @@
let crypto = require('crypto')
module.exports = (sequelize, DataTypes) => {
let PassKey = sequelize.define('PassKey', {
passkey: {
type: DataTypes.STRING,
defaultValue () {
return crypto.randomBytes(64).toString('hex')
},
validate: {
isString (val) {
if(typeof val !== 'string') {
throw new sequelize.ValidationError('passkey must be a string')
}
}
}
}
}, {
instanceMethods: {
isValid () {
let ms = Date.now() - this.createdAt
let dayMs = 1000*60*60*24
//Has less than 1 day passed
//since generating token?
return ms / dayMs < 1
}
}
})
return PassKey
}

View File

@ -16,6 +16,7 @@ let helmet = require('helmet')
var Recaptcha = require('express-recaptcha').RecaptchaV3;
var recaptcha = new Recaptcha('6LdlbrwZAAAAAKvtcVQhVl_QaNOqmQ4PgyW3SKHy', '6LdlbrwZAAAAAMAWPVDrL8eNPxrws6AMDtLf1bgd');
let path = require('path')
@ -34,7 +35,6 @@ app.use(compression())
app.use(bodyParser.json({ limit: '5mb' }))
app.use(bodyParser.urlencoded({ extended: true }))
app.use(session)
var maintoptions = {
current: config.maintenance, // current state, default **false**
httpEndpoint: true, // expose http endpoint for hot-switch, default **false**,
@ -53,9 +53,9 @@ if(process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'production') {
app.use('/api/v1/user/', require('./routes/user'))
app.use('/api/v1/admin/admin_token', require('./routes/admin_token'))
app.use('/api/v1/admin/passkey', require('./routes/user_passkey'))
app.use('/api/v1/forums/category', require('./routes/category'))
app.use('/api/v1/forums/thread', require('./routes/thread'))
app.use('/api/v1/blog/', require('./routes/blog'))
app.use('/api/v1/users/notification', require('./routes/notification'))
app.use('/api/v1/forums/post', require('./routes/post'))
app.use('/api/v1/kaverti/state', require('./routes/settings'))

View File

@ -1094,6 +1094,11 @@ expand-template@^2.0.3:
resolved "https://npm.open-registry.dev/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
express-rate-limit@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.1.3.tgz#656bacce3f093034976346958a0f0199902c9174"
integrity sha512-TINcxve5510pXj4n9/1AMupkj3iWxl3JuZaWhCdYDlZeoCPqweGZrxbrlqTCFb1CT5wli7s8e2SH/Qz2c9GorA==
express-recaptcha@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/express-recaptcha/-/express-recaptcha-5.0.2.tgz#17de212c165489462ab85e1e07c4ea81661d5e5f"