Add option to search for users by email or username in admin panel

This commit is contained in:
Phillip Thelen
2024-07-17 13:32:25 +02:00
parent 04554c5309
commit 489bd851bb
7 changed files with 246 additions and 18 deletions
+15
View File
@@ -0,0 +1,15 @@
// eslint-disable-next-line import/no-commonjs, import/no-unresolved
const autocannon = require('autocannon');
autocannon({
url: 'http://localhost:3000/api/v4/inbox/messages',
method: 'GET',
headers: {
'x-api-user': 'd7ee6e45-7db1-44fd-8c1f-195ef6cd52d6',
'x-api-key': '8a8f9790-a27b-4dff-b403-bddc5cf8fe53',
},
connections: 100, // default
pipelining: 1, // default
duration: 10, // default
workers: 4,
}, console.log);
@@ -2,29 +2,31 @@
<div class="row standard-page">
<div class="well col-12">
<h1>Admin Panel</h1>
<div>
<form
class="form-inline"
@submit.prevent="loadHero(userIdentifier)"
>
<form
class="form-inline"
@submit.prevent="loadUser(userIdentifier)"
>
<div class="input-group">
<input
v-model="userIdentifier"
class="form-control uidField"
type="text"
:placeholder="'User ID or Username; blank for your account'"
:placeholder="'User-ID, Username or E-Mail; blank for your account'"
>
<input
type="submit"
value="Load User"
<div class="input-group-append">
<button
class="btn btn-primary"
type="button"
@click="loadUser(userIdentifier)">Load User</button>
<button
class="btn btn-secondary"
>
</form>
</div>
type="button"
@click="searchUsers(userIdentifier)">Search</button>
</div>
</div>
</form>
<div>
<router-view @changeUserIdentifier="changeUserIdentifier" />
</div>
<router-view @changeUserIdentifier="changeUserIdentifier" class="mt-3" />
</div>
</div>
</template>
@@ -33,6 +35,10 @@
.uidField {
min-width: 45ch;
}
.input-group-append {
width:auto;
}
</style>
<script>
@@ -62,7 +68,20 @@ export default {
// (useful if we want to re-fetch the user after making changes).
this.userIdentifier = newId;
},
async loadHero (userIdentifier) {
async searchUsers (userIdentifier) {
this.$router.push({
name: 'adminPanelSearch',
params: { userIdentifier },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
},
async loadUser (userIdentifier) {
const id = userIdentifier || this.user._id;
this.$router.push({
@@ -0,0 +1,108 @@
<template>
<div>
<div class="alert alert-warning" role="alert"
v-if="noUsersFound"
>
Could not find any matching users.
</div>
<div class="list-group col-4"
v-if="users.length > 0">
<a href="#" class="list-group-item list-group-item-action"
v-for="user in users"
:key="user._id"
@click="loadUser(user._id)"
>
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ user.profile.name }}</h5>
<small>{{ user._id }}</small>
</div>
<p class="mb-1"
:class="{'font-weight-bold': matchValueToIdentifier(user.auth.local.username)}">
@{{ user.auth.local.username }}</p>
<p class="mb-0">
<span
v-for="email in userEmails(user)"
:class="{'font-weigh-bold': matchValueToIdentifier(email)}"
:key="email">
{{ email }}
</span>
</p>
</a>
</div>
</div>
</template>
<script>
import VueRouter from 'vue-router';
import { mapState } from '@/libs/store';
const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default {
data () {
return {
userIdentifier: '',
users: [],
noUsersFound: false,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
watch: {
userIdentifier () {
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
this.users = users;
this.noUsersFound = users.length === 0;
});
this.$emit('changeUserIdentifier', this.userIdentifier); // change user identifier in Admin Panel's form
},
},
mounted () {
this.userIdentifier = this.$route.params.userIdentifier;
},
methods: {
matchValueToIdentifier (value) {
return value.toLowerCase().includes(this.userIdentifier.toLowerCase());
},
userEmails (user) {
const emails = [];
if (user.auth.local.email) emails.push(user.auth.local.email);
if (user.auth.google && user.auth.google.email) {
const email = user.auth.google.email;
if (typeof email === 'string') emails.push(email);
else if (Array.isArray(email)) emails.push(...email);
}
if (user.auth.apple && user.auth.apple.email) {
const email = user.auth.apple.email;
if (typeof email === 'string') emails.push(email);
else if (Array.isArray(email)) emails.push(...email);
}
if (user.auth.facebook && user.auth.facebook.email) {
const email = user.auth.facebook.email;
if (typeof email === 'string') emails.push(email);
else if (Array.isArray(email)) emails.push(...email);
}
return emails;
},
async loadUser (userIdentifier) {
const id = userIdentifier || this.user._id;
this.$router.push({
name: 'adminPanelUser',
params: { userIdentifier: id },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
},
},
};
</script>
+12 -1
View File
@@ -22,6 +22,7 @@ const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall
// Admin Panel
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/search');
// Except for tasks that are always loaded all the other main level
// All the main level
@@ -193,9 +194,19 @@ const router = new VueRouter({
],
},
children: [
{
name: 'adminPanelSearch',
path: 'search/:userIdentifier',
component: AdminPanelSearchPage,
meta: {
privilegeNeeded: [
'userSupport',
],
},
},
{
name: 'adminPanelUser',
path: ':userIdentifier', // User ID or Username
path: ':userIdentifier',
component: AdminPanelUserPage,
meta: {
privilegeNeeded: [
@@ -0,0 +1,7 @@
import axios from 'axios';
export async function searchUsers (store, payload) {
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
const response = await axios.get(url);
return response.data.data;
}
@@ -1,5 +1,6 @@
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
import * as adminPanel from './adminPanel';
import * as common from './common';
import * as user from './user';
import * as tasks from './tasks';
@@ -24,6 +25,7 @@ import * as faq from './faq';
// Example: fetch in user.js -> 'user:fetch'
const actions = flattenAndNamespace({
adminPanel,
common,
user,
tasks,
@@ -0,0 +1,66 @@
import validator from 'validator';
import { authWithHeaders } from '../../middlewares/auth';
import { ensurePermission } from '../../middlewares/ensureAccessRight';
import { model as User } from '../../models/user';
const api = {};
/**
* @api {get} /api/v4/admin/search/:userIdentifier Search for users by username or email
* @apiParam (Path) {String} userIdentifier The username or email of the user to search for
* @apiName SearchUsers
* @apiGroup Admin
* @apiPermission Admin
*
* @apiDescription Returns a list of users that match the search criteria
*
* @apiSuccess {Object} data The User list
*
* @apiUse NoAuthHeaders
* @apiUse NoAccount
* @apiUse NoUser
* @apiUse NotAdmin
*/
api.getHero = {
method: 'GET',
url: '/admin/search/:userIdentifier',
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
async handler (req, res) {
req.checkParams('userIdentifier', res.t('userIdentifierRequired')).notEmpty();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { userIdentifier } = req.params;
const re = new RegExp(String.raw`${userIdentifier}`);
let query;
if (validator.isUUID(userIdentifier)) {
query = { _id: userIdentifier };
} else if (validator.isEmail(userIdentifier)) {
query = {
$or: [
{ 'auth.local.email': { $regex: re, $options: 'i' } },
{ 'auth.google.email': { $regex: re, $options: 'i' } },
{ 'auth.apple.email': { $regex: re, $options: 'i' } },
{ 'auth.facebook.email': { $regex: re, $options: 'i' } },
],
};
} else {
query = { 'auth.local.lowerCaseUsername': { $regex: re, $options: 'i' } };
}
console.log(query);
const users = await User
.find(query)
.select('contributor backer profile auth')
.limit(30)
.lean()
.exec();
res.respond(200, users);
},
};
export default api;