diff --git a/autocannon.js b/autocannon.js
new file mode 100644
index 0000000000..383047b4f7
--- /dev/null
+++ b/autocannon.js
@@ -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);
diff --git a/website/client/src/components/admin-panel/index.vue b/website/client/src/components/admin-panel/index.vue
index acd58c31d2..5cd6e89826 100644
--- a/website/client/src/components/admin-panel/index.vue
+++ b/website/client/src/components/admin-panel/index.vue
@@ -2,29 +2,31 @@
@@ -33,6 +35,10 @@
.uidField {
min-width: 45ch;
}
+
+ .input-group-append {
+ width:auto;
+ }
diff --git a/website/client/src/router/index.js b/website/client/src/router/index.js
index 5e51cff9fc..55bc30b2d8 100644
--- a/website/client/src/router/index.js
+++ b/website/client/src/router/index.js
@@ -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: [
diff --git a/website/client/src/store/actions/adminPanel.js b/website/client/src/store/actions/adminPanel.js
new file mode 100644
index 0000000000..43e1805429
--- /dev/null
+++ b/website/client/src/store/actions/adminPanel.js
@@ -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;
+}
diff --git a/website/client/src/store/actions/index.js b/website/client/src/store/actions/index.js
index f514eec99a..d9be79396d 100644
--- a/website/client/src/store/actions/index.js
+++ b/website/client/src/store/actions/index.js
@@ -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,
diff --git a/website/server/controllers/api-v4/admin.js b/website/server/controllers/api-v4/admin.js
new file mode 100644
index 0000000000..acb6fe4ea2
--- /dev/null
+++ b/website/server/controllers/api-v4/admin.js
@@ -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;