0.174-prerelease2

This commit is contained in:
Troplo 2020-10-11 23:08:10 +11:00
parent ea3b76cb0f
commit 1e4836aefc
6 changed files with 474 additions and 113 deletions

View File

@ -1,150 +1,481 @@
<style>
kave-authorized-avatar {
border-radius: 50%;
}
</style>
<template>
<main>
<div class="section">
<div class="column is-3">
<div class="box">
<center>
<avatar-icon
ref='avatar'
:user='user'
></avatar-icon>
<h1>{{$store.state.username}}</h1>
</center>
<div class="columns is-centered">
<div class="column is-3">
<div class="box">
<center>
<avatar-icon
:user='user'
></avatar-icon>
<h1>{{$store.state.username}}</h1>
<tooltips style="padding-left: 5px;">
<b-tooltip v-if='user && user.system' class="is-success" label="This user is a system user operated by administrators that mainly run API operations.">
<b-tag rounded class="is-success">&nbsp;SYSTEM&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.bot' class="is-info" label="This user is a bot account that can run automated API operations.">
<b-tag rounded class="is-info">&nbsp;BOT&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.admin' class="is-danger" label="User is an official Kaverti administrator.">
<b-tag class="is-danger" rounded>&nbsp;ADMIN&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.hidden' class="is-info" label="User is not discoverable in the user list.">
<b-tag rounded>&nbsp;HIDDEN&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.booster' class="is-light" label="User is boosting the Kaverti Discord server.">
<b-tag class="is-light" rounded>&nbsp;BOOSTER&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
</tooltips>
<br>
<br>
<b-button tag="router-link" to="/settings">
<i class="fas fa-cog"></i>
User Settings
</b-button>
</center>
</div>
</div>
<div class='column is-5' :class='{ "user_posts--no_border_bottom": posts && !posts.length }'>
<div class="box">
<h2>Global Wall</h2>
<div
class='editor'
:class='{
"editor--focus": focusInput,
"editor--error": errors.content
}'
>
<div class='editor__input'>
<input-editor-core
v-model='editor'
@mentions='setMentions'
@focus='setFocusInput(true)'
@blur='setFocusInput(false)'
></input-editor-core>
</div>
</div>
<error-tooltip :error='errors.content' class='editor_error'></error-tooltip>
<b-button class='submit' :loading='loading' @click='postThread'>Post on wall</b-button>
<template v-if='!posts'>
<thread-post-placeholder v-if='!posts'>
</thread-post-placeholder>
</template>
<scroll-load
:loading='loadingPosts'
@loadNext='loadNewPosts'
v-else-if='posts.length'
>
<wall-post
v-for='(post, index) in posts'
:key='"thread-post-" + post.id'
:post='post'
:show-thread='true'
:click-for-post='true'
:class='{"post--last": index === posts.length-1}'
></wall-post>
<template v-if='loadingPosts'>
<thread-post-placeholder
v-for='n in nextPostsCount'
:key='"thread-post-placeholder-" + n'
></thread-post-placeholder>
</template>
</scroll-load>
<template v-else><br><br>There are no wall posts yet</template>
</div>
</div>
</div>
</div>
</main>
</template>
<script>
import AvatarIcon from '../AvatarIcon'
<script>
import ScrollLoad from '../ScrollLoad'
import WallPost from '../WallPost'
import ThreadPostPlaceholder from '../ThreadPostPlaceholder'
import InputEditorCore from '../InputEditorCore'
import ErrorTooltip from '../ErrorTooltip'
import AvatarIcon from '../AvatarIcon'
import AjaxErrorHandler from '../../assets/js/errorHandler'
export default {
name: 'user',
name: 'HomeAuthenticated',
props: ['username'],
components: {
WallPost,
ScrollLoad,
ThreadPostPlaceholder,
InputEditorCore,
ErrorTooltip,
AvatarIcon
},
data () {
return {
menuItems: [
/* { name: 'Wall', route: 'wall' }, */
{ name: 'Posts', route: 'posts' },
{ name: 'Threads', route: 'threads' },
/* { name: 'Friends', route: 'friends' } */
],
selected: 0,
username: this.$route.params.username,
selectedCategory: this.$store.state.category.selectedCategory,
editor: '',
mentions: [],
name: '',
loading: false,
focusInput: false,
threads: null,
loadingThreads: false,
nextURL: '',
nextThreadsCount: 0,
posts: null,
user: null,
relationship: false,
relationships: {
type: ''
}
}
},
watch: {
$route (to) {
this.selected = this.getIndexFromRoute(to.path)
errors: {
content: '',
name: '',
pollQuestion: '',
pollAnswer: ''
},
showPoll: false,
pollQuestion: '',
newPollAnswer: '',
pollAnswers: []
}
},
computed: {
userPicture () {
if(this.user && this.user.picture) {
return 'https://cdn.kaverti.com/user/avatars/full/' + this.user.picture + '.png'
} else {
return null
}
categories () {
return this.$store.getters.categoriesWithoutAll
}
},
methods: {
resetFetchData () {
this.offset = 0;
this.users = [];
loadNewPosts () {
if(this.nextURL === null) return
this.fetchData();
this.loadingThreads = true
},
fetchData () {
this.axios
.get(`/api/v1/userinfo`)
.then(res => this.user = res.data)
togglePoll (val) {
if(val !== undefined) {
this.showPoll = val
} else {
this.showPoll = !this.showPoll
}
},
scrubDesc () {
this.axios
.put('/api/v1/admin/user/scrub', {
description: "descscram",
user: this.username
})
.then(() => {
this.resetFetchData()
})
.catch(AjaxErrorHandler(this.$store))
},
scrubUsername () {
this.axios
.put('/api/v1/admin/user/scrub', {
username: "usernamescram",
user: this.username
})
.then(() => {
this.description.loading = false
})
.catch(AjaxErrorHandler(this.$store))
},
doRelationship () {
this.axios
.put('/api/v1/relationships/' + this.user.username, {
type: this.relationships.type
})
.then(() => {
this.description.loading = false
})
.catch(e => {
this.description.loading = false
addPollAnswer () {
if(!this.newPollAnswer.trim().length) return
AjaxErrorHandler(this.$store)(e, error => {
this.description.error = error.message
})
})
this.pollAnswers.push({ answer: this.newPollAnswer })
this.newPollAnswer = ''
},
getIndexFromRoute (path) {
let selectedIndex
let route = path.split('/')[3]
removePollAnswer ($index) {
this.pollAnswers.splice($index, 1)
},
removePoll () {
this.pollQuestion = ''
this.pollAnswers = []
this.newPollAnswer = ''
this.menuItems.forEach((item, index) => {
if(item.route === route) {
selectedIndex = index
}
this.togglePoll()
},
setErrors (errors) {
errors.forEach(error => {
this.errors[error.name] = error.error
})
},
getUserInfo () {
this.axios
.get('/api/v1/userinfo')
},
clearErrors () {
this.errors.content = ''
this.errors.name = ''
this.errors.pollQuestion = ''
this.errors.pollAnswer = ''
},
return selectedIndex
hasDuplicates (array, cb) {
if(cb) array = array.map(cb)
return array.length !== (new Set(array)).size
},
postThread () {
let errors = []
this.clearErrors()
if(!this.editor.trim().length) {
errors.push({name: 'content', error: 'Post content cannot be blank'})
} if(errors.length) {
this.setErrors(errors)
return
}
this.loading = true
this.axios.post(`/api/v1/wall/post`, {
username: "GlobalWall",
content: this.editor,
mentions: this.mentions
}).then(() => {
let ajax = []
return Promise.all(ajax)
}).then(() => {
this.loading = false
this.axios
.get(`/api/v1/user/GlobalWall?wall=true`)
.then(res => {
this.posts = res.data.userWalls
this.nextPostsCount = res.data.meta.postNumber
})
}).catch(e => {
this.loading = false
AjaxErrorHandler(this.$store)(e, (error, errors) => {
let path = error.path
if(this.errors[path] !== undefined) {
this.errors[path] = error.message
} else {
errors.push(error.message)
}
})
})
},
setFocusInput (val) {
this.focusInput = val
},
setMentions (mentions) {
this.mentions = mentions
}
},
created () {
this.resetFetchData()
this.selected = this.getIndexFromRoute(this.$route.path)
watch: {
'$store.state.username' (username) {
if(!username) {
this.$router.push('/')
}
}
},
mounted () {
this.getUserInfo();
this.$store.dispatch('setTitle', this.$route.params.username + '\'s wall')
this.axios
.get(`/api/v1/user/${this.$route.params.username}`)
.then(res => this.user = res.data)
this.axios
.get(`/api/v1/relationships/${this.$route.params.username}`)
.then(res => this.relationship = res.data)
.catch(e => {
let invalidId = e.response.data.errors.find(error => {
return error.name === 'accountDoesNotExist'
})
if(invalidId) {
this.$store.commit('set404Page', true)
} else {
AjaxErrorHandler(this.$store)(e)
}
.get(`/api/v1/user/GlobalWall?wall=true`)
.then(res => {
this.loadingPosts = false
this.posts = res.data.userWalls
this.nextPostsCount = res.data.meta.nextPostsCount
})
this.axios
.get(`/api/v1/userinfo`)
.then(res => {
this.loadingPosts = false
this.user = res.data
this.nextPostsCount = res.data.meta.nextPostsCount
})
},
beforeRouteEnter (to, from, next) {
next(vm => {
if(!vm.$store.state.username) {
vm.$store.commit('setAccountModalState', true);
next('/')
}
})
}
}
</script>
<style>
img {
border-radius: 50%;
<style lang='scss'>
@import '../../assets/scss/variables.scss';
.thread_new {
margin-top: 1rem;
}
.thread_meta_info {
background-color: #fff;
border: thin solid $color__gray--darker;
border-radius: 0.25rem;
padding: 1rem;
margin: 1rem 0;
@at-root #{&}__title {
margin: 0 0.5rem;
margin-top: 0.5rem;
display: inline-block;
}
@at-root #{&}__form {
display: flex;
align-items: baseline;
}
@at-root #{&}__add_poll {
margin-top: 0.5rem;
}
@at-root #{&}__text {
margin-bottom: 0.5rem;
}
@at-root #{&}__poll {
border-top: thin solid $color__gray--primary;
margin-top: 1rem;
padding-top: 0.75rem;
position: relative;
@at-root #{&}__top_bar {
display: flex;
justify-content: space-between;
align-items: baseline;
}
@at-root #{&}__answer {
display: flex;
align-items: baseline;
& > span {
opacity: 0;
pointer-events: none;
transition: all 0.1s;
font-size: 1.5rem;
margin-left: 0.5rem;
cursor: pointer;
@include user-select(none);
}
&:hover > span {
opacity: 1;
pointer-events: all;
}
}
}
}
.submit {
margin-top: 1rem;
}
.editor {
display: flex;
background-color: #fff;
border-radius: 0.25rem;
border: thin solid $color__gray--darker;
transition: all 0.2s;
@at-root #{&}--focus {
border: thin solid $color__gray--darkest;
}
@at-root #{&}--error {
border: thin solid $color__red--primary;
}
@at-root #{&}__format_bar {
height: 2.5rem;
background-color: $color__gray--primary;
display: flex;
padding-right: 1rem;
padding-bottom: 0.25rem;
justify-content: flex-end;
align-items: center;
font-variant: small-caps;
@at-root #{&}--preview {
border-radius: 0 0.25rem 0 0;
}
@at-root #{&}--editor {
border-radius: 0.25rem 0 0 0;
}
}
@at-root #{&}__input {
width: 100%;
position: relative;
.input_editor_core__format_bar {
left: 0rem;
}
.input_editor_core textarea {
height: 5rem;
}
}
@at-root #{&}__preview {
border-left: 1px solid $color__gray--darker;
div.input_editor_preview__markdownHTML {
height: 14.1rem;
}
}
}
.editor_error {
width: 100%;
background: #fff;
margin-top: 0.5rem;
border-radius: 0.2rem;
border: thin solid $color__red--primary;
&.error_tooltip--show {
max-height: 4rem;
padding: 0.5rem;
}
}
@media (max-width: 600px) {
.thread_meta_info {
@at-root #{&}__form {
flex-direction: column;
}
@at-root #{&}__title.fancy_input {
margin: 0;
margin-top: 0.5rem;
}
@at-root #{&}__poll__question {
width: 100%;
> div, input {
width: 100% ;
}
}
}
.editor {
flex-direction: column;
overflow-x: hidden;
@at-root #{&}__input, #{&}__preview {
border: 0;
width: 100%;
}
}
}
.user_posts {
background: #fff;
border-radius: 0.25rem;
padding: 1rem;
border: thin solid $color__gray--darker;
@at-root #{&}__title {
font-size: 1.5rem;
margin-bottom: 1rem;
}
}
@media (max-width: $breakpoint--tablet) {
.user_posts {
margin-top: 1rem;
overflow: hidden;
}
}
</style>

View File

@ -4,7 +4,7 @@ const state = {
message: '',
options: [
{ name: "Select a ban type", disabled: true },
{ name: "Block user's known IP addresses", value: "ip" },
{ name: "Block user's known IP addresses", value: "ipBanned" },
{ name: "Read Only Mode", value: "ReadOnlyMode"},
],
selectedOption: 0

View File

@ -25,6 +25,7 @@
"cookie-parser": "^1.4.5",
"cross-env": "^3.1.4",
"crypto-random-string": "^3.3.0",
"csurf": "^1.11.0",
"date-format": "^3.0.0",
"dompurify": "^2.0.12",
"ejs": "^2.5.7",

View File

@ -19,6 +19,8 @@ const crypto = require("crypto")
const cryptoRandomString = require("crypto-random-string")
const rateLimit = require("express-rate-limit");
const moment = require("moment")
const csrf = require('csurf')
const csrfProtection = csrf({ cookie: true })
const emailLimiter = rateLimit({
windowMs: 60000,
max: 1, // limit each IP to 100 requests per windowMs

View File

@ -63,6 +63,8 @@ const swaggerUi = require('swagger-ui-express');
let path = require('path')
const passport = require('passport');
const specs = swaggerJsdoc(options);
const csrf = require('csurf')
const csrfProtection = csrf({ cookie: true })
let session = expressSession({
secret: config.sessionSecret,
@ -83,6 +85,7 @@ app.use(session);
app.use(expAutoSan.all);
app.use(passport.initialize());
app.use(passport.session());
if(process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'production') {
app.use(require('morgan')('dev'))
}

View File

@ -944,6 +944,15 @@ crypto-random-string@^3.3.0:
dependencies:
type-fest "^0.8.1"
csrf@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.1.0.tgz#ec75e9656d004d674b8ef5ba47b41fbfd6cb9c30"
integrity sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==
dependencies:
rndm "1.2.0"
tsscmp "1.0.6"
uid-safe "2.1.5"
css-select@~1.2.0:
version "1.2.0"
resolved "https://npm.open-registry.dev/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
@ -976,6 +985,16 @@ cssstyle@^2.2.0:
dependencies:
cssom "~0.3.6"
csurf@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.11.0.tgz#ab0c3c6634634192bd3d6f4b861be20800eeb61a"
integrity sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==
dependencies:
cookie "0.4.0"
cookie-signature "1.0.6"
csrf "3.1.0"
http-errors "~1.7.3"
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://npm.open-registry.dev/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@ -2084,7 +2103,7 @@ http-errors@1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@1.7.3, http-errors@~1.7.0, http-errors@~1.7.2:
http-errors@1.7.3, http-errors@~1.7.0, http-errors@~1.7.2, http-errors@~1.7.3:
version "1.7.3"
resolved "https://npm.open-registry.dev/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
@ -3915,6 +3934,11 @@ retry-as-promised@^3.1.0:
dependencies:
any-promise "^1.3.0"
rndm@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c"
integrity sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://npm.open-registry.dev/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -4670,7 +4694,7 @@ tslib@^2.0.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
tsscmp@^1.0.6:
tsscmp@1.0.6, tsscmp@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==