Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin search users #960

Merged
merged 34 commits into from May 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9654b80
not working
Dec 12, 2018
fcdb1ff
fix: wire in admin module, cleanup
mrkvon Dec 12, 2018
b880ebf
/admin for users with role admin
Dec 12, 2018
2122a5e
attempt at adding AdminSearchUsers, requiresRole implies requiresAuth
Dec 12, 2018
e9c2fdf
working search at /admin/search-users
Dec 12, 2018
efe5a81
input type search
Dec 13, 2018
60ec359
container-spacer, link back to /admin
Dec 13, 2018
39598bb
better comments
Dec 13, 2018
67ff765
Wanna help us build Trustroots?
Dec 13, 2018
f47c27c
lint;
Dec 13, 2018
331f74a
link to github issue from /admin
Dec 13, 2018
46b984a
creating server side admin routes
Dec 13, 2018
1562728
search by regexp, and by email
Dec 13, 2018
01b8e47
Add files to ES6 whitelist
simison May 10, 2019
d6b3e9a
Work on backend
simison May 10, 2019
204690c
Add policy to limit non-admin users
simison May 10, 2019
e388659
Add API tests
simison May 10, 2019
2d2baae
Work on client
simison May 10, 2019
4ef19a3
Elaborate access error a bit
simison May 10, 2019
cd13ae6
Fix import
simison May 10, 2019
502daf8
Work on client
simison May 11, 2019
2098600
Nicer icons
simison May 11, 2019
06794f2
Escape regexp
simison May 11, 2019
bbdbd6a
Debounce onChange
simison May 11, 2019
605a17a
Improved labels
simison May 11, 2019
f5af2af
Use POST instead of GET as a security measure
simison May 11, 2019
c63c32d
rename some vars
simison May 11, 2019
6b57b9b
Simplify & more secure handling of tokens
simison May 11, 2019
dac719a
Populate tribe info
simison May 11, 2019
952f971
Disable analytics for admin dash
simison May 11, 2019
9bbdfed
Remove debounce, it messes up something with client side testing...
simison May 11, 2019
7cb6adf
Handle temporary emails in the list
simison May 11, 2019
68e2c08
Remove suspended-highlighting with background color
simison May 11, 2019
ea072f1
Workaround onChange API load with onSubmit instead
simison May 11, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.js
Expand Up @@ -172,6 +172,8 @@ module.exports = {
// ES 2018 - specify migrated files and folders here
files: [
'bin/db-maintenance/ensure-indexes.js',
'modules/admin/server/**',
'modules/admin/tests/**',
'modules/references/server/**',
'modules/references/tests/server/**',
'modules/users/tests/server/user-change-locale.server.routes.tests.js',
Expand All @@ -187,6 +189,7 @@ module.exports = {
'config/client/**',
'config/env/**',
'config/webpack/**',
'modules/admin/client/**',
'modules/core/client/app/config.js',
'modules/**/client/components/**',
'modules/**/client/api/**',
Expand Down
1 change: 1 addition & 0 deletions config/lib/express.js
Expand Up @@ -483,6 +483,7 @@ module.exports.initHelmetHeaders = function (app) {
module.exports.initModulesClientRoutes = function (app) {
// Setting the app router and static folder
app.use('/', express.static(path.resolve('./public')));
app.use('/', express.static(path.resolve('./public/assets')));

// Globbing static routing
config.folders.client.forEach(function (staticPath) {
Expand Down
12 changes: 12 additions & 0 deletions config/webpack/webpack.config.js
Expand Up @@ -43,6 +43,18 @@ module.exports = merge(shims, {
]
}
}]
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]-[hash].[ext]',
outputPath: 'images/'
}
}
]
}
]
},
Expand Down
5 changes: 5 additions & 0 deletions modules/admin/client/admin.client.module.js
@@ -0,0 +1,5 @@
'use strict';

// Use application configuration module to register a new module
// The core module is required for special route handling; see /core/client/config/core.client.routes
AppConfig.registerModule('admin', ['core']);
11 changes: 11 additions & 0 deletions modules/admin/client/api/users.api.js
@@ -0,0 +1,11 @@
import axios from 'axios';

export async function searchUsers(search) {
const { data } = await axios.post('/api/admin/users', { search });
return data;
}

export async function getUser(id) {
const { data } = await axios.post('/api/admin/user', { id });
return data;
}
21 changes: 21 additions & 0 deletions modules/admin/client/components/Admin.component.js
@@ -0,0 +1,21 @@
import React from 'react';
import AdminHeader from './AdminHeader.component.js';
import bmoDancing from '../images/bmo-dancing.gif';

export default function Admin() {
return (
<>
<AdminHeader />
<div className="container container-spacer">
<p><img src={ bmoDancing } alt="" width="200" /></p>
<p>Welcome, friend! 👋</p>
<p>
See our <a href="https://team.trustroots.org/">Team Guide</a> & <a href="https://trustroots.zendesk.com/inbox/">Support queue</a>.
</p>
<p><strong><em>Remember to logout on public computers!</em></strong></p>
</div>
</>
);
}

Admin.propTypes = {};
35 changes: 35 additions & 0 deletions modules/admin/client/components/AdminHeader.component.js
@@ -0,0 +1,35 @@
// External dependencies
import classnames from 'classnames';
import React from 'react';

// Internal dependencies
import adminIcon from '../images/bmo.png';

export default function AdminHeader() {
const path = window.location.pathname;
return (
<>
<br/><br/>
<nav className="navbar navbar-white navbar-admin">
<div className="container">
<div className="navbar-header">
<a className="navbar-brand" href="/admin" aria-label="Admin dash index">
<img src={ adminIcon } height="24" alt="" aria-hidden="true" focusable="false" />
</a>
</div>
<ul className="nav navbar-nav">
<li className={ classnames({ 'active': path === '/admin/user' })}>
<a href="/admin/user">Show user</a>
</li>
<li className={ classnames({ 'active': path === '/admin/search-users' })}>
<a href="/admin/search-users">Search users</a>
</li>
</ul>
<p className="navbar-text pull-right text-muted hidden-xs"><em>Admin dash</em></p>
</div>
</nav>
</>
);
}

AdminHeader.propTypes = {};
150 changes: 150 additions & 0 deletions modules/admin/client/components/AdminSearchUsers.component.js
@@ -0,0 +1,150 @@
// External dependencies
import React, { Component } from 'react';

// Internal dependencies
import { searchUsers } from '../api/users.api';
import AdminHeader from './AdminHeader.component.js';
import UserState from './UserState.component.js';
import ZendeskInboxSearch from './ZendeskInboxSearch.component.js';

// Limitations set in the API
const SEARCH_USERS_LIMIT = 50;
const SEARCH_STRING_LIMIT = 3;

export default class AdminSearchUsers extends Component {
constructor(props) {
super(props);
this.onSearchChange = this.onSearchChange.bind(this);
this.doSearch = this.doSearch.bind(this);
this.state = {
search: '',
userResults: []
};
}

componentDidMount() {
const urlParams = new URLSearchParams(window.location.search);
const search = urlParams.get('search');
if (search) {
this.setState({ search }, this.doSearch);
}
}

onSearchChange(event) {
const search = event.target.value;
this.setState({ search });

// Update URL
const url = new URL(document.location);
url.searchParams.set('search', search);
window.history.pushState(
{ search },
window.document.title,
url.toString()
);
}

async doSearch(event) {
if (event) {
event.preventDefault();
}
const { search } = this.state;
if (search.length >= SEARCH_STRING_LIMIT) {
const userResults = await searchUsers(search);
this.setState({ userResults });
}
}

render() {
const { userResults } = this.state;

return (
<>
<AdminHeader />
<div className="container">
<h2>Search users</h2>

<form onSubmit={ this.doSearch } className="form-inline">
<label>
Name, username or email<br/>
<input
className="form-control input-lg"
type="search"
value={ this.state.search }
onChange={ this.onSearchChange }
/>
</label>
<button
className="btn btn-lg btn-default"
disabled={ this.state.search.length < SEARCH_STRING_LIMIT }
type="submit"
>
Search
</button>
</form>

{ userResults.length ? (
<div className="panel panel-default">
<div className="panel-body">
<table className="table table-striped table-responsive">
<thead>
<tr>
<th>Username</th>
<th>Name</th>
<th>Email</th>
<th>ID</th>
</tr>
</thead>
<tbody>
{
userResults.map((user) => {
const { _id, displayName, email, emailTemporary, username } = user;
return (
<tr key={_id}>
<td className="admin-search-users__actions">
<a href={'/profile/' + username} title="Profile on Trustroots">{ username }</a>
<UserState user={ user } />
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ username } />
<a
className="admin-action admin-hidden-until-hover"
href={ `/admin/user?id=${ _id }` }
>
Show more
</a>
</td>
<td>
{ displayName }
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ displayName } />
</td>
<td>
{ email }
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ email } />
{ (emailTemporary && emailTemporary !== email) && (
<>
<br/>
{ emailTemporary } (temporary email)
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ emailTemporary } />
</>
) }
</td>
<td><small><code style={ { 'userSelect': 'all' } }>{ _id }</code></small></td>
</tr>
);
})
}
</tbody>
</table>
</div>
<div className="panel-footer">
{ userResults.length } user(s).
{ userResults.length === SEARCH_USERS_LIMIT && <p className="text-warning">There might be more results but { SEARCH_USERS_LIMIT } is maximum.</p>}
</div>
</div>
) : <p><br/><em className="text-muted">Search something...</em></p> }
</div>
</>
);
};
}

AdminSearchUsers.propTypes = {};
103 changes: 103 additions & 0 deletions modules/admin/client/components/AdminUser.component.js
@@ -0,0 +1,103 @@
// External dependencies
import React, { Component } from 'react';

// Internal dependencies
import { getUser } from '../api/users.api';
import AdminHeader from './AdminHeader.component.js';
import UserState from './UserState.component.js';

// Mongo ObjectId is always 24 chars long
const MONGO_OBJECT_ID_LENGTH = 24;

export default class AdminUser extends Component {
constructor(props) {
super(props);
this.onIdChange = this.onIdChange.bind(this);
this.queryUser = this.queryUser.bind(this);
this.state = { id: '', user: false };
}

componentDidMount() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');

if (id && id.length === MONGO_OBJECT_ID_LENGTH) {
this.setState({ id }, this.queryUser);
}
}

onIdChange(event) {
const id = event.target.value;
this.setState({ id });

// Update URL
const url = new URL(document.location);
url.searchParams.set('id', id);
window.history.pushState(
{ id },
window.document.title,
url.toString()
);
}

queryUser(event) {
if (event) {
event.preventDefault();
}

const { id } = this.state;

this.setState({ user: false }, async () => {
if (id.length === MONGO_OBJECT_ID_LENGTH) {
const user = await getUser(id);
this.setState({ user });
}
});
}

render() {
const { user } = this.state;

return (
<>
<AdminHeader />
<div className="container">

<h2>Show user</h2>

<form onSubmit={ this.queryUser } className="form-inline">
<label>
User ID<br/>
<input
className="form-control input-lg"
onChange={ this.onIdChange }
size={ MONGO_OBJECT_ID_LENGTH + 2 }
maxLength={ MONGO_OBJECT_ID_LENGTH }
type="text"
value={ this.state.id }
/>
</label>
<button
className="btn btn-lg btn-default"
disabled={ this.state.id.length !== MONGO_OBJECT_ID_LENGTH }
type="submit"
>
Show
</button>
</form>

{ user && (
<div className="panel panel-default">
<div className="panel-body">
<UserState user={ user } />
<pre>{ JSON.stringify(user, null, 2) }</pre>
</div>
</div>
) }
</div>
</>
);
};
}

AdminUser.propTypes = {};