diff --git a/server/api/admin.js b/server/api/admin.js
index c2aa95f..1ced840 100644
--- a/server/api/admin.js
+++ b/server/api/admin.js
@@ -3,7 +3,9 @@ import Models from './models'
const perPage = 6
-function cleanUserObject (dbe, admin) {
+async function cleanUserObject (dbe, admin) {
+ let totp = await Users.User.Login.totpTokenRequired(dbe)
+
return {
id: dbe.id,
username: dbe.username,
@@ -17,7 +19,8 @@ function cleanUserObject (dbe, admin) {
password: dbe.password !== null,
nw_privilege: dbe.nw_privilege,
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) {
let entry = raw[i]
- users.push(cleanUserObject(entry, admin))
+ users.push(await cleanUserObject(entry, admin))
}
return {
@@ -104,6 +107,53 @@ const API = {
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)
getAllClients: async function (page) {
let count = await Models.OAuth2Client.query().count('id as ids')
@@ -123,14 +173,13 @@ const API = {
}
return {
- page: paginated,
- clients: clients
+ page: paginated, clients
}
},
// Get information about a client via id
getClient: async function (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])
},
@@ -211,8 +260,7 @@ const API = {
}
return {
- page: paginated,
- bans: bans
+ page: paginated, bans
}
},
// Remove a ban
diff --git a/server/api/emailer.js b/server/api/emailer.js
index 0135742..89b9109 100644
--- a/server/api/emailer.js
+++ b/server/api/emailer.js
@@ -4,6 +4,10 @@ import nodemailer from 'nodemailer'
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')
let templateCache = {}
diff --git a/server/api/index.js b/server/api/index.js
index d7df691..3dbd4da 100644
--- a/server/api/index.js
+++ b/server/api/index.js
@@ -372,8 +372,16 @@ const API = {
// Create user
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
let activationToken = API.Hash(16)
+
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: activationToken,
@@ -381,25 +389,28 @@ const API = {
type: 1
})
- // Send Activation Email
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)
- } catch (e) {
- console.error(e)
+ // Send Activation 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)
+ } catch (e) {
+ console.error(e)
+
+ if (deleteOnFail) {
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: {
diff --git a/server/routes/admin.js b/server/routes/admin.js
index 4ab3a46..f8c6eb5 100644
--- a/server/routes/admin.js
+++ b/server/routes/admin.js
@@ -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) => {
let page = parseInt(req.query.page)
if (isNaN(page) || page < 1) {
@@ -98,6 +111,51 @@ apiRouter.get('/users', wrap(async (req, res) => {
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
* ===============
@@ -115,43 +173,34 @@ apiRouter.get('/clients', wrap(async (req, res) => {
apiRouter.get('/client/:id', wrap(async (req, res) => {
let id = parseInt(req.params.id)
if (isNaN(id)) {
- return res.status(400).jsonp({error: 'Invalid number'})
+ throw new Error('Invalid number')
}
let client = await API.getClient(id)
- if (!client) return res.status(400).jsonp({error: 'Invalid client'})
res.jsonp(client)
}))
-apiRouter.post('/client/new', wrap(async (req, res) => {
- if (req.body.csrf !== req.session.csrf) {
- return res.status(400).jsonp({error: 'Invalid session'})
- }
-
+apiRouter.post('/client/new', csrfVerify, wrap(async (req, res) => {
await API.createClient(req.body, req.session.user)
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)
- if (!id || isNaN(id)) return res.status(400).jsonp({error: 'ID missing'})
-
- if (req.body.csrf !== req.session.csrf) {
- return res.status(400).jsonp({error: 'Invalid session'})
- }
+ if (!id || isNaN(id)) throw new Error('ID missing')
await API.updateClient(id, req.body)
res.status(204).end()
}))
-apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => {
- let id = parseInt(req.params.id)
+apiRouter.post('/client/new_secret', csrfVerify, wrap(async (req, res) => {
+ let id = parseInt(req.body.id)
if (isNaN(id)) {
- return res.status(400).jsonp({error: 'Invalid number'})
+ throw new Error('Invalid client ID')
}
let client = await API.newSecret(id)
@@ -159,10 +208,10 @@ apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => {
res.jsonp(client)
}))
-apiRouter.post('/client/delete/:id', wrap(async (req, res) => {
- let id = parseInt(req.params.id)
+apiRouter.post('/client/delete', csrfVerify, wrap(async (req, res) => {
+ let id = parseInt(req.body.id)
if (isNaN(id)) {
- return res.status(400).jsonp({error: 'Invalid number'})
+ throw new Error('Invalid client ID')
}
let client = await API.removeClient(id)
@@ -185,10 +234,10 @@ apiRouter.get('/bans', wrap(async (req, res) => {
res.jsonp(bans)
}))
-apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => {
- let id = parseInt(req.params.id)
+apiRouter.post('/ban/pardon', csrfVerify, wrap(async (req, res) => {
+ let id = parseInt(req.body.id)
if (isNaN(id)) {
- return res.status(400).jsonp({error: 'Invalid number'})
+ throw new Error('Invalid number')
}
let ban = await API.removeBan(id)
@@ -196,11 +245,8 @@ apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => {
res.jsonp(ban)
}))
-apiRouter.post('/ban', wrap(async (req, res) => {
- if (!req.body.user_id) return res.status(400).jsonp({error: 'ID missing'})
- if (req.body.csrf !== req.session.csrf) {
- return res.status(400).jsonp({error: 'Invalid session'})
- }
+apiRouter.post('/ban', csrfVerify, wrap(async (req, res) => {
+ if (!req.body.user_id) throw new Error('ID missing')
let result = await API.addBan(req.body, req.session.user.id)
diff --git a/src/script/component/BanList.vue b/src/script/component/BanList.vue
index da00099..370615a 100644
--- a/src/script/component/BanList.vue
+++ b/src/script/component/BanList.vue
@@ -11,6 +11,7 @@
diff --git a/src/script/component/UserModal.vue b/src/script/component/UserModal.vue
new file mode 100644
index 0000000..1f0903e
--- /dev/null
+++ b/src/script/component/UserModal.vue
@@ -0,0 +1,93 @@
+
+ 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
+
+
+
diff --git a/src/style/admin.styl b/src/style/admin.styl
index b63f58b..1675ef2 100644
--- a/src/style/admin.styl
+++ b/src/style/admin.styl
@@ -111,8 +111,9 @@ nav
background-color: #fff
border: 1px solid #ddd
border-radius: 5px
- min-width: 180px
- font-size: 120%
+ min-width: 210px
+ font-size: 100%
+ z-index: 2999
.title
padding: 5px
diff --git a/src/style/main.styl b/src/style/main.styl
index 75a0b97..92959b7 100644
--- a/src/style/main.styl
+++ b/src/style/main.styl
@@ -579,6 +579,10 @@ select
padding: 8px 0
input[type="checkbox"]
margin-top: 10px
+ span
+ padding: 10px
+ display: inline-block
+ vertical-align: top
@media all and (max-width: 800px)
.navigator
diff --git a/templates/reset_password/html.pug b/templates/reset_password/html.pug
index 8a8f29c..509231d 100644
--- a/templates/reset_password/html.pug
+++ b/templates/reset_password/html.pug
@@ -1,5 +1,4 @@
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:
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.
diff --git a/templates/reset_password/subject.pug b/templates/reset_password/subject.pug
index 85b8d0b..f266d28 100644
--- a/templates/reset_password/subject.pug
+++ b/templates/reset_password/subject.pug
@@ -1 +1 @@
-|Icy Network - Password reset request
+|Icy Network - Reset Your Password