A lot of user actions, add CSRF tokens to all POSTs
This commit is contained in:
parent
e938ac5ab9
commit
61248119c5
@ -3,7 +3,9 @@ import Models from './models'
|
|||||||
|
|
||||||
const perPage = 6
|
const perPage = 6
|
||||||
|
|
||||||
function cleanUserObject (dbe, admin) {
|
async function cleanUserObject (dbe, admin) {
|
||||||
|
let totp = await Users.User.Login.totpTokenRequired(dbe)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: dbe.id,
|
id: dbe.id,
|
||||||
username: dbe.username,
|
username: dbe.username,
|
||||||
@ -17,7 +19,8 @@ function cleanUserObject (dbe, admin) {
|
|||||||
password: dbe.password !== null,
|
password: dbe.password !== null,
|
||||||
nw_privilege: dbe.nw_privilege,
|
nw_privilege: dbe.nw_privilege,
|
||||||
created_at: dbe.created_at,
|
created_at: dbe.created_at,
|
||||||
bannable: dbe.nw_privilege < admin.nw_privilege && dbe.id !== admin.id
|
totp_enabled: totp,
|
||||||
|
bannable: admin ? (dbe.nw_privilege < admin.nw_privilege && dbe.id !== admin.id) : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +99,7 @@ const API = {
|
|||||||
for (let i in raw) {
|
for (let i in raw) {
|
||||||
let entry = raw[i]
|
let entry = raw[i]
|
||||||
|
|
||||||
users.push(cleanUserObject(entry, admin))
|
users.push(await cleanUserObject(entry, admin))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -104,6 +107,53 @@ const API = {
|
|||||||
users: users
|
users: users
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getUser: async function (id) {
|
||||||
|
let user = await Users.User.get(id)
|
||||||
|
if (!user) throw new Error('No such user')
|
||||||
|
|
||||||
|
return cleanUserObject(user, null)
|
||||||
|
},
|
||||||
|
editUser: async function (id, data) {
|
||||||
|
let user = await Users.User.get(id)
|
||||||
|
if (!user) throw new Error('No such user')
|
||||||
|
|
||||||
|
let fields = [
|
||||||
|
'username', 'display_name', 'email', 'nw_privilege', 'activated'
|
||||||
|
]
|
||||||
|
|
||||||
|
data = dataFilter(data, fields, ['nw_privilege', 'activated'])
|
||||||
|
if (!data) throw new Error('Missing fields')
|
||||||
|
|
||||||
|
await Users.User.update(user, data)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
resendActivationEmail: async function (id) {
|
||||||
|
let user = await Users.User.get(id)
|
||||||
|
if (!user) throw new Error('No such user')
|
||||||
|
|
||||||
|
if (user.activated === 1) return {}
|
||||||
|
|
||||||
|
await Users.User.Register.activationEmail(user)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
revokeTotpToken: async function (id) {
|
||||||
|
let user = await Users.User.get(id)
|
||||||
|
if (!user) throw new Error('No such user')
|
||||||
|
|
||||||
|
await Models.TotpToken.query().delete().where('user_id', user.id)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
sendPasswordEmail: async function (id) {
|
||||||
|
let user = await Users.User.get(id)
|
||||||
|
if (!user) throw new Error('No such user')
|
||||||
|
|
||||||
|
let token = await Users.User.Reset.reset(user.email, false, true)
|
||||||
|
|
||||||
|
return {token}
|
||||||
|
},
|
||||||
// List all clients (paginated)
|
// List all clients (paginated)
|
||||||
getAllClients: async function (page) {
|
getAllClients: async function (page) {
|
||||||
let count = await Models.OAuth2Client.query().count('id as ids')
|
let count = await Models.OAuth2Client.query().count('id as ids')
|
||||||
@ -123,14 +173,13 @@ const API = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page: paginated,
|
page: paginated, clients
|
||||||
clients: clients
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Get information about a client via id
|
// Get information about a client via id
|
||||||
getClient: async function (id) {
|
getClient: async function (id) {
|
||||||
let raw = await Models.OAuth2Client.query().where('id', id)
|
let raw = await Models.OAuth2Client.query().where('id', id)
|
||||||
if (!raw.length) return null
|
if (!raw.length) throw new Error('No such client')
|
||||||
|
|
||||||
return cleanClientObject(raw[0])
|
return cleanClientObject(raw[0])
|
||||||
},
|
},
|
||||||
@ -211,8 +260,7 @@ const API = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page: paginated,
|
page: paginated, bans
|
||||||
bans: bans
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Remove a ban
|
// Remove a ban
|
||||||
|
@ -4,6 +4,10 @@ import nodemailer from 'nodemailer'
|
|||||||
|
|
||||||
import config from '../../scripts/load-config'
|
import config from '../../scripts/load-config'
|
||||||
|
|
||||||
|
// TEMPORARY FIX FOR NODE v9.2.0
|
||||||
|
import tls from 'tls'
|
||||||
|
tls.DEFAULT_ECDH_CURVE = 'auto'
|
||||||
|
|
||||||
const templateDir = path.join(__dirname, '../../', 'templates')
|
const templateDir = path.join(__dirname, '../../', 'templates')
|
||||||
|
|
||||||
let templateCache = {}
|
let templateCache = {}
|
||||||
|
@ -372,8 +372,16 @@ const API = {
|
|||||||
// Create user
|
// Create user
|
||||||
let user = await models.User.query().insert(data)
|
let user = await models.User.query().insert(data)
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
await API.User.Register.activationEmail(user, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
},
|
||||||
|
activationEmail: async function (user, deleteOnFail = false) {
|
||||||
// Activation token
|
// Activation token
|
||||||
let activationToken = API.Hash(16)
|
let activationToken = API.Hash(16)
|
||||||
|
|
||||||
await models.Token.query().insert({
|
await models.Token.query().insert({
|
||||||
expires_at: new Date(Date.now() + 86400000), // 1 day
|
expires_at: new Date(Date.now() + 86400000), // 1 day
|
||||||
token: activationToken,
|
token: activationToken,
|
||||||
@ -381,25 +389,28 @@ const API = {
|
|||||||
type: 1
|
type: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send Activation Email
|
|
||||||
console.debug('Activation token:', activationToken)
|
console.debug('Activation token:', activationToken)
|
||||||
if (email) {
|
|
||||||
try {
|
|
||||||
let em = await emailer.pushMail('activate', user.email, {
|
|
||||||
domain: config.server.domain,
|
|
||||||
display_name: user.display_name,
|
|
||||||
activation_token: activationToken
|
|
||||||
})
|
|
||||||
|
|
||||||
console.debug(em)
|
// Send Activation Email
|
||||||
} catch (e) {
|
try {
|
||||||
console.error(e)
|
let em = await emailer.pushMail('activate', user.email, {
|
||||||
|
domain: config.server.domain,
|
||||||
|
display_name: user.display_name,
|
||||||
|
activation_token: activationToken
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug(em)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
if (deleteOnFail) {
|
||||||
await models.User.query().delete().where('id', user.id)
|
await models.User.query().delete().where('id', user.id)
|
||||||
throw new Error('Invalid email address!')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('Invalid email address!')
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Reset: {
|
Reset: {
|
||||||
|
@ -88,6 +88,19 @@ router.get('/oauth2', wrap(async (req, res) => {
|
|||||||
* =======
|
* =======
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function csrfVerify (req, res, next) {
|
||||||
|
if (req.body.csrf !== req.session.csrf) {
|
||||||
|
return next(new Error('Invalid session'))
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============
|
||||||
|
* User Data
|
||||||
|
* =============
|
||||||
|
*/
|
||||||
|
|
||||||
apiRouter.get('/users', wrap(async (req, res) => {
|
apiRouter.get('/users', wrap(async (req, res) => {
|
||||||
let page = parseInt(req.query.page)
|
let page = parseInt(req.query.page)
|
||||||
if (isNaN(page) || page < 1) {
|
if (isNaN(page) || page < 1) {
|
||||||
@ -98,6 +111,51 @@ apiRouter.get('/users', wrap(async (req, res) => {
|
|||||||
res.jsonp(users)
|
res.jsonp(users)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
apiRouter.get('/user/:id', wrap(async (req, res) => {
|
||||||
|
let id = parseInt(req.params.id)
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new Error('Invalid number')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(await API.getUser(id))
|
||||||
|
}))
|
||||||
|
|
||||||
|
apiRouter.post('/user', csrfVerify, wrap(async (req, res) => {
|
||||||
|
let id = parseInt(req.body.user_id)
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new Error('Invalid or missing user ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(await API.editUser(id, req.body))
|
||||||
|
}))
|
||||||
|
|
||||||
|
apiRouter.post('/user/resend_activation', csrfVerify, wrap(async (req, res) => {
|
||||||
|
let id = parseInt(req.body.user_id)
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new Error('Invalid or missing user ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(await API.resendActivationEmail(id))
|
||||||
|
}))
|
||||||
|
|
||||||
|
apiRouter.post('/user/revoke_totp', csrfVerify, wrap(async (req, res) => {
|
||||||
|
let id = parseInt(req.body.user_id)
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new Error('Invalid or missing user ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(await API.revokeTotpToken(id))
|
||||||
|
}))
|
||||||
|
|
||||||
|
apiRouter.post('/user/reset_password', csrfVerify, wrap(async (req, res) => {
|
||||||
|
let id = parseInt(req.body.user_id)
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new Error('Invalid or missing user ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(await API.sendPasswordEmail(id))
|
||||||
|
}))
|
||||||
|
|
||||||
/* ===============
|
/* ===============
|
||||||
* OAuth2 Data
|
* OAuth2 Data
|
||||||
* ===============
|
* ===============
|
||||||
@ -115,43 +173,34 @@ apiRouter.get('/clients', wrap(async (req, res) => {
|
|||||||
apiRouter.get('/client/:id', wrap(async (req, res) => {
|
apiRouter.get('/client/:id', wrap(async (req, res) => {
|
||||||
let id = parseInt(req.params.id)
|
let id = parseInt(req.params.id)
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return res.status(400).jsonp({error: 'Invalid number'})
|
throw new Error('Invalid number')
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = await API.getClient(id)
|
let client = await API.getClient(id)
|
||||||
if (!client) return res.status(400).jsonp({error: 'Invalid client'})
|
|
||||||
|
|
||||||
res.jsonp(client)
|
res.jsonp(client)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiRouter.post('/client/new', wrap(async (req, res) => {
|
apiRouter.post('/client/new', csrfVerify, wrap(async (req, res) => {
|
||||||
if (req.body.csrf !== req.session.csrf) {
|
|
||||||
return res.status(400).jsonp({error: 'Invalid session'})
|
|
||||||
}
|
|
||||||
|
|
||||||
await API.createClient(req.body, req.session.user)
|
await API.createClient(req.body, req.session.user)
|
||||||
|
|
||||||
res.status(204).end()
|
res.status(204).end()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiRouter.post('/client/update', wrap(async (req, res) => {
|
apiRouter.post('/client/update', csrfVerify, wrap(async (req, res) => {
|
||||||
let id = parseInt(req.body.id)
|
let id = parseInt(req.body.id)
|
||||||
|
|
||||||
if (!id || isNaN(id)) return res.status(400).jsonp({error: 'ID missing'})
|
if (!id || isNaN(id)) throw new Error('ID missing')
|
||||||
|
|
||||||
if (req.body.csrf !== req.session.csrf) {
|
|
||||||
return res.status(400).jsonp({error: 'Invalid session'})
|
|
||||||
}
|
|
||||||
|
|
||||||
await API.updateClient(id, req.body)
|
await API.updateClient(id, req.body)
|
||||||
|
|
||||||
res.status(204).end()
|
res.status(204).end()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => {
|
apiRouter.post('/client/new_secret', csrfVerify, wrap(async (req, res) => {
|
||||||
let id = parseInt(req.params.id)
|
let id = parseInt(req.body.id)
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return res.status(400).jsonp({error: 'Invalid number'})
|
throw new Error('Invalid client ID')
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = await API.newSecret(id)
|
let client = await API.newSecret(id)
|
||||||
@ -159,10 +208,10 @@ apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => {
|
|||||||
res.jsonp(client)
|
res.jsonp(client)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiRouter.post('/client/delete/:id', wrap(async (req, res) => {
|
apiRouter.post('/client/delete', csrfVerify, wrap(async (req, res) => {
|
||||||
let id = parseInt(req.params.id)
|
let id = parseInt(req.body.id)
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return res.status(400).jsonp({error: 'Invalid number'})
|
throw new Error('Invalid client ID')
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = await API.removeClient(id)
|
let client = await API.removeClient(id)
|
||||||
@ -185,10 +234,10 @@ apiRouter.get('/bans', wrap(async (req, res) => {
|
|||||||
res.jsonp(bans)
|
res.jsonp(bans)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => {
|
apiRouter.post('/ban/pardon', csrfVerify, wrap(async (req, res) => {
|
||||||
let id = parseInt(req.params.id)
|
let id = parseInt(req.body.id)
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return res.status(400).jsonp({error: 'Invalid number'})
|
throw new Error('Invalid number')
|
||||||
}
|
}
|
||||||
|
|
||||||
let ban = await API.removeBan(id)
|
let ban = await API.removeBan(id)
|
||||||
@ -196,11 +245,8 @@ apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => {
|
|||||||
res.jsonp(ban)
|
res.jsonp(ban)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiRouter.post('/ban', wrap(async (req, res) => {
|
apiRouter.post('/ban', csrfVerify, wrap(async (req, res) => {
|
||||||
if (!req.body.user_id) return res.status(400).jsonp({error: 'ID missing'})
|
if (!req.body.user_id) throw new Error('ID missing')
|
||||||
if (req.body.csrf !== req.session.csrf) {
|
|
||||||
return res.status(400).jsonp({error: 'Invalid session'})
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = await API.addBan(req.body, req.session.user.id)
|
let result = await API.addBan(req.body, req.session.user.id)
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
import Ban from './Ban.vue'
|
import Ban from './Ban.vue'
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
@ -44,7 +45,10 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
pardon: function (id) {
|
pardon: function (id) {
|
||||||
this.$http.post('/admin/api/ban/pardon/' + id).then(data => {
|
this.$http.post('/admin/api/ban/pardon', {
|
||||||
|
id: id,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
this.getBans(1)
|
this.getBans(1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -53,7 +57,7 @@
|
|||||||
this.getBans(1)
|
this.getBans(1)
|
||||||
|
|
||||||
this.$root.$on('reload_bans', () => {
|
this.$root.$on('reload_bans', () => {
|
||||||
this.getBans(1)
|
this.getBans(this.pagination.page)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$on('pardon', (id) => {
|
this.$on('pardon', (id) => {
|
||||||
|
@ -61,9 +61,9 @@
|
|||||||
this.verified = false
|
this.verified = false
|
||||||
},
|
},
|
||||||
submit: function () {
|
submit: function () {
|
||||||
let url = this.id === -1 ? 'new' : 'update'
|
let uri = this.id === -1 ? 'new' : 'update'
|
||||||
|
|
||||||
this.$http.post('/admin/api/client/' + url, {
|
this.$http.post('/admin/api/client/' + uri, {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
@ -81,7 +81,10 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
newSecret: function () {
|
newSecret: function () {
|
||||||
this.$http.post('/admin/api/client/new_secret/' + this.id).then(data => {
|
this.$http.post('/admin/api/client/new_secret', {
|
||||||
|
id: this.id,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
alert('New secret generated.')
|
alert('New secret generated.')
|
||||||
this.$root.$emit('reload_clients')
|
this.$root.$emit('reload_clients')
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
@ -92,6 +95,7 @@
|
|||||||
watch: {
|
watch: {
|
||||||
id: function () {
|
id: function () {
|
||||||
if (this.id <= 0) return
|
if (this.id <= 0) return
|
||||||
|
|
||||||
this.$http.get('/admin/api/client/' + this.id).then(data => {
|
this.$http.get('/admin/api/client/' + this.id).then(data => {
|
||||||
let dr = data.body
|
let dr = data.body
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
import OAuthClient from './OAuthClient.vue'
|
import OAuthClient from './OAuthClient.vue'
|
||||||
import ClientModal from './ClientModal.vue'
|
import ClientModal from './ClientModal.vue'
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
@ -48,7 +49,10 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteClient: function (id) {
|
deleteClient: function (id) {
|
||||||
this.$http.post('/admin/api/client/delete/' + id).then(data => {
|
this.$http.post('/admin/api/client/delete', {
|
||||||
|
id: id,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
this.getClients(1)
|
this.getClients(1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -57,7 +61,7 @@
|
|||||||
this.getClients(1)
|
this.getClients(1)
|
||||||
|
|
||||||
this.$root.$on('reload_clients', () => {
|
this.$root.$on('reload_clients', () => {
|
||||||
this.getClients(1)
|
this.getClients(this.pagination.page)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$on('edit', function (id) {
|
this.$on('edit', function (id) {
|
||||||
|
@ -8,17 +8,34 @@
|
|||||||
.stamp(title="Used an external login" v-if="!password")
|
.stamp(title="Used an external login" v-if="!password")
|
||||||
i.fa.fa-fw.fa-sign-out
|
i.fa.fa-fw.fa-sign-out
|
||||||
|
|
||||||
.noactive.stamp(v-if='activated == false', title='Not activated.')
|
.noactive.stamp(v-if='activated == false' title='Not activated.')
|
||||||
i.fa.fa-fw.fa-envelope
|
i.fa.fa-fw.fa-envelope
|
||||||
|
|
||||||
|
.totp.stamp(v-if='totp_enabled' title="Two-Factor Authentication Enabled")
|
||||||
|
i.fa.fa-fw.fa-shield
|
||||||
|
|
||||||
.dropdown-wrapper.stamp(@click="dropdown = !dropdown" v-on-clickaway='away')
|
.dropdown-wrapper.stamp(@click="dropdown = !dropdown" v-on-clickaway='away')
|
||||||
i.fa.fa-fw.fa-ellipsis-v
|
i.fa.fa-fw.fa-ellipsis-v
|
||||||
transition(name="pop")
|
transition(name="pop")
|
||||||
.dropdown(v-show="dropdown")
|
.dropdown(v-show="dropdown")
|
||||||
.title Actions
|
.title Actions
|
||||||
|
.action(v-on:click='$parent.$emit("edit", id)')
|
||||||
|
i.fa.fa-fw.fa-pencil
|
||||||
|
| Edit User
|
||||||
.action(v-if='bannable' v-on:click='$parent.$emit("ban", id)')
|
.action(v-if='bannable' v-on:click='$parent.$emit("ban", id)')
|
||||||
i.fa.fa-fw.fa-ban
|
i.fa.fa-fw.fa-ban
|
||||||
| Ban User
|
| Ban User
|
||||||
|
.separator
|
||||||
|
.action(v-if='!activated' v-on:click='$parent.$emit("activation", id)')
|
||||||
|
i.fa.fa-fw.fa-envelope
|
||||||
|
| Activation Email
|
||||||
|
.action(v-if="totp_enabled" v-on:click='$parent.$emit("totp-revoke", id)')
|
||||||
|
i.fa.fa-fw.fa-shield
|
||||||
|
| Revoke 2FA
|
||||||
|
.action(v-on:click='$parent.$emit("reset-password", id)')
|
||||||
|
i.fa.fa-fw.fa-envelope
|
||||||
|
| Password Email
|
||||||
|
|
||||||
|
|
||||||
.display_name {{ display_name }}
|
.display_name {{ display_name }}
|
||||||
.name {{ id }} - {{ username }} ({{ uuid }})
|
.name {{ id }} - {{ username }} ({{ uuid }})
|
||||||
@ -40,7 +57,7 @@
|
|||||||
import { directive as onClickaway } from 'vue-clickaway'
|
import { directive as onClickaway } from 'vue-clickaway'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['avatar_file', 'activated', 'display_name', 'id', 'username', 'uuid', 'email', 'nw_privilege', 'created_at', 'password', 'bannable', 'ip_address'],
|
props: ['avatar_file', 'activated', 'display_name', 'id', 'username', 'uuid', 'email', 'nw_privilege', 'created_at', 'password', 'bannable', 'ip_address', 'totp_enabled'],
|
||||||
directives: {
|
directives: {
|
||||||
onClickaway: onClickaway,
|
onClickaway: onClickaway,
|
||||||
},
|
},
|
||||||
|
@ -5,12 +5,15 @@
|
|||||||
.list.users
|
.list.users
|
||||||
user(v-for='user in users' v-bind="user" :key="user.id")
|
user(v-for='user in users' v-bind="user" :key="user.id")
|
||||||
ban-modal(:show='banning' @close='banning = 0' :id='banning')
|
ban-modal(:show='banning' @close='banning = 0' :id='banning')
|
||||||
|
user-modal(:show='editing' @close='editing = 0' :id='editing')
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
import User from './User.vue'
|
import User from './User.vue'
|
||||||
import BanModal from './BanModal.vue'
|
import BanModal from './BanModal.vue'
|
||||||
|
import UserModal from './UserModal.vue'
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
@ -23,11 +26,12 @@
|
|||||||
total: 0
|
total: 0
|
||||||
},
|
},
|
||||||
users: [],
|
users: [],
|
||||||
banning: 0
|
banning: 0,
|
||||||
|
editing: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Pagination, User, BanModal
|
Pagination, User, BanModal, UserModal
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getUsers: function (page) {
|
getUsers: function (page) {
|
||||||
@ -44,6 +48,51 @@
|
|||||||
this.$on('ban', function (id) {
|
this.$on('ban', function (id) {
|
||||||
this.banning = id
|
this.banning = id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$on('edit', function (id) {
|
||||||
|
this.editing = id
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$on('activation', function (id) {
|
||||||
|
this.$http.post('/admin/api/user/resend_activation', {
|
||||||
|
user_id: id,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
|
alert('Email sent!')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
alert('Failed to send activation email to this user.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$on('totp-revoke', function (id) {
|
||||||
|
this.$http.post('/admin/api/user/revoke_totp', {
|
||||||
|
user_id: id,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
|
alert('Success!')
|
||||||
|
this.getUsers(this.pagination.page)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
alert('An error occured.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$on('reset-password', function (id) {
|
||||||
|
this.$http.post('/admin/api/user/reset_password', {
|
||||||
|
user_id: id,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
|
alert('Email sent!')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
alert('Failed to send activation email to this user.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$root.$on('reload_users', () => {
|
||||||
|
this.getUsers(this.pagination.page)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
93
src/script/component/UserModal.vue
Normal file
93
src/script/component/UserModal.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
modal(:show='show', @close='close')
|
||||||
|
.modal-header
|
||||||
|
h3 Edit User
|
||||||
|
.modal-body.aligned-form
|
||||||
|
.message.error(v-if='error') {{ error }}
|
||||||
|
.cell
|
||||||
|
label(for="username") Username
|
||||||
|
input(type="text" id="username" name="username" v-model="username")
|
||||||
|
.cell
|
||||||
|
label(for="display_name") Display Name
|
||||||
|
input(type="text" id="display_name" name="display_name" v-model="display_name")
|
||||||
|
.cell
|
||||||
|
label(for="email") Email
|
||||||
|
input(type="email" id="email" name="email" v-model="email")
|
||||||
|
.cell
|
||||||
|
label(for="privilege") Privilege
|
||||||
|
input(type="range" min="0" max="5" step="1" id="privilege" name="privilege" v-model="nw_privilege")
|
||||||
|
span {{ nw_privilege }}
|
||||||
|
.cell
|
||||||
|
label(for="activated") Activated
|
||||||
|
input(type="checkbox" id="activated" name="activated" v-model="activated")
|
||||||
|
.modal-footer.text-align
|
||||||
|
button(@click='submit') Done
|
||||||
|
button(@click='close') Cancel
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
import Modal from './Modal.vue'
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['show', 'id'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
username: '',
|
||||||
|
display_name: '',
|
||||||
|
email: '',
|
||||||
|
nw_privilege: 0,
|
||||||
|
activated: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close: function () {
|
||||||
|
this.$emit('close')
|
||||||
|
this.error = ''
|
||||||
|
this.username = ''
|
||||||
|
this.display_name = ''
|
||||||
|
this.email = ''
|
||||||
|
this.nw_privilege = 0
|
||||||
|
this.activated = true
|
||||||
|
},
|
||||||
|
submit: function () {
|
||||||
|
this.$http.post('/admin/api/user', {
|
||||||
|
user_id: this.id,
|
||||||
|
username: this.username,
|
||||||
|
display_name: this.display_name,
|
||||||
|
email: this.email,
|
||||||
|
nw_privilege: this.nw_privilege,
|
||||||
|
activated: this.activated,
|
||||||
|
csrf: csrfToken
|
||||||
|
}).then(data => {
|
||||||
|
this.close()
|
||||||
|
this.$root.$emit('reload_users')
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
if (err.body && err.body.error) this.error = err.body.error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
id: function () {
|
||||||
|
if (this.id <= 0) return
|
||||||
|
this.$http.get('/admin/api/user/' + this.id).then(data => {
|
||||||
|
let dr = data.body
|
||||||
|
|
||||||
|
this.username = dr.username
|
||||||
|
this.display_name = dr.display_name
|
||||||
|
this.email = dr.email
|
||||||
|
this.nw_privilege = dr.nw_privilege
|
||||||
|
this.activated = dr.activated
|
||||||
|
}).catch(err => {
|
||||||
|
alert('Failed to fetch user information')
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -111,8 +111,9 @@ nav
|
|||||||
background-color: #fff
|
background-color: #fff
|
||||||
border: 1px solid #ddd
|
border: 1px solid #ddd
|
||||||
border-radius: 5px
|
border-radius: 5px
|
||||||
min-width: 180px
|
min-width: 210px
|
||||||
font-size: 120%
|
font-size: 100%
|
||||||
|
z-index: 2999
|
||||||
|
|
||||||
.title
|
.title
|
||||||
padding: 5px
|
padding: 5px
|
||||||
|
@ -579,6 +579,10 @@ select
|
|||||||
padding: 8px 0
|
padding: 8px 0
|
||||||
input[type="checkbox"]
|
input[type="checkbox"]
|
||||||
margin-top: 10px
|
margin-top: 10px
|
||||||
|
span
|
||||||
|
padding: 10px
|
||||||
|
display: inline-block
|
||||||
|
vertical-align: top
|
||||||
|
|
||||||
@media all and (max-width: 800px)
|
@media all and (max-width: 800px)
|
||||||
.navigator
|
.navigator
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
h1 Hello, #{display_name}!
|
h1 Hello, #{display_name}!
|
||||||
p You've requested to reset your password on Icy Network.
|
|
||||||
p Click on or copy the following link into your URL bar in order to reset your Icy Network account password:
|
p Click on or copy the following link into your URL bar in order to reset your Icy Network account password:
|
||||||
a.activate(href=domain + "/reset/" + reset_token, target="_blank", rel="nofollow")= domain + "/reset/" + reset_token
|
a.activate(href=domain + "/reset/" + reset_token, target="_blank", rel="nofollow")= domain + "/reset/" + reset_token
|
||||||
p If you did not request a password reset on Icy Network, please ignore this email.
|
p If you did not request a password reset on Icy Network, please ignore this email.
|
||||||
|
@ -1 +1 @@
|
|||||||
|Icy Network - Password reset request
|
|Icy Network - Reset Your Password
|
||||||
|
Reference in New Issue
Block a user