diff --git a/server/api/admin.js b/server/api/admin.js index 1ced840..25b06c8 100644 --- a/server/api/admin.js +++ b/server/api/admin.js @@ -154,6 +154,30 @@ const API = { return {token} }, + // Search for users by terms and fields + searchUsers: async function (terms, fields = ['email']) { + let qb = Models.User.query() + + terms = terms.replace(/_/g, '\\_').replace(/%/g, '\\%') + + qb = qb.where(fields[0], 'like', '%' + terms + '%') + if (fields.length >= 1) { + for (let i = 1; i < fields.length; i++) { + qb = qb.orWhere(fields[i], 'like', '%' + terms + '%') + } + } + + let rows = await qb.limit(8) + if (!rows.length) return { error: 'No results' } + + let cleaned = [] + for (let i in rows) { + let userRaw = rows[i] + cleaned.push(await cleanUserObject(userRaw, null)) + } + + return cleaned + }, // List all clients (paginated) getAllClients: async function (page) { let count = await Models.OAuth2Client.query().count('id as ids') diff --git a/server/routes/admin.js b/server/routes/admin.js index f8c6eb5..00d1933 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -156,6 +156,31 @@ apiRouter.post('/user/reset_password', csrfVerify, wrap(async (req, res) => { res.jsonp(await API.sendPasswordEmail(id)) })) +const availableScopes = ['uuid', 'email', 'username', 'display_name'] +apiRouter.get('/search/users', wrap(async (req, res) => { + if (!req.query.terms) throw new Error('Please specify search terms!') + + let scopes = [] + if (req.query.scopes) { + let scq = req.query.scopes.split(',') + + for (let i in scq) { + scq[i] = scq[i].trim() + + if (availableScopes.indexOf(scq[i]) !== -1) { + scopes.push(scq[i]) + } + } + } + + if (!scopes.length) { + scopes.push('email') + } + + let results = await API.searchUsers(req.query.terms, scopes) + res.jsonp(results) +})) + /* =============== * OAuth2 Data * =============== diff --git a/src/script/component/UserList.vue b/src/script/component/UserList.vue index 108d48b..c57c279 100644 --- a/src/script/component/UserList.vue +++ b/src/script/component/UserList.vue @@ -3,7 +3,10 @@ h3 Registered Users ({{ pagination.total }}) pagination(:page="pagination.page" :pages="pagination.pages" v-on:page="getUsers") .list.users - user(v-for='user in users' v-bind="user" :key="user.id") + .searchbox + input(v-model="search" placeholder="Begin typing a name or email address..") + .message.error(v-if="error") {{ error }} + user(v-else v-for='user in users' v-bind="user" :key="user.id") ban-modal(:show='banning' @close='banning = 0' :id='banning') user-modal(:show='editing' @close='editing = 0' :id='editing') @@ -13,6 +16,8 @@ import User from './User.vue' import BanModal from './BanModal.vue' import UserModal from './UserModal.vue' + + import qs from 'querystring' const csrfToken = document.querySelector('meta[name="csrf-token"]').content export default { @@ -27,7 +32,9 @@ }, users: [], banning: 0, - editing: 0 + editing: 0, + search: '', + error: '' } }, components: { @@ -35,11 +42,35 @@ }, methods: { getUsers: function (page) { + this.error = '' this.$http.get('/admin/api/users?page=' + page).then(data => { if (data.body && data.body.error) return this.pagination = data.body.page this.users = data.body.users }) + }, + searchUsers: function () { + this.error = '' + this.$http.get('/admin/api/search/users?' + qs.stringify({ + terms: this.search, + scopes: 'email,username,display_name' + })).then(data => { + if (data.body.error) { + this.error = data.body.error + return + } + + this.users = data.body + }) + } + }, + watch: { + search: function () { + if (this.search === '') { + this.getUsers(1) + } else { + this.searchUsers(this.search) + } } }, mounted: function () { diff --git a/src/style/admin.styl b/src/style/admin.styl index 1675ef2..a2ef71a 100644 --- a/src/style/admin.styl +++ b/src/style/admin.styl @@ -133,6 +133,13 @@ nav border-bottom: 1px solid #ddd margin-bottom: 9px +.searchbox + margin: 10px 0 + + input + font-size: 140% + display: block + width: 100% .modal-mask position: fixed