Skip to content

Commit

Permalink
feat(admin): Add users report #4258
Browse files Browse the repository at this point in the history
  • Loading branch information
svenaas committed Oct 5, 2023
1 parent c829f93 commit b64e898
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 0 deletions.
1 change: 1 addition & 0 deletions admin-client/src/Router.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
page('/organizations/:id/edit', queryString, render(Pages.Organization.Edit));
page('/reports/organizations', queryString, render(Pages.Organization.OrgsReport));
page('/reports/published-sites', queryString, render(Pages.Organization.PublishedSitesReport));
page('/reports/users', queryString, render(Pages.User.UsersReport));
page('/reports', queryString, render(Pages.Reports));
page('/tasks', queryString, render(Pages.Tasks));
page('*', render(Pages.NotFound));
Expand Down
10 changes: 10 additions & 0 deletions admin-client/src/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ async function fetchUsers(query = {}) {
return get('/users', query).catch(() => []);
}

async function fetchUsersReport(query = {}) {
return get('/reports/users', query).catch(() => []);
}

async function fetchUsersReportCSV() {
return getAttachedFile('/reports/users.csv').catch(() => []);
}

async function inviteUser(params) {
return post('/users/invite', params);
}
Expand Down Expand Up @@ -333,6 +341,8 @@ export {
fetchUserEnvironmentVariables,
fetchUser,
fetchUsers,
fetchUsersReport,
fetchUsersReportCSV,
inviteUser,
resendInvite,
logout,
Expand Down
1 change: 1 addition & 0 deletions admin-client/src/pages/Reports.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<ul>
<li><a href="/reports/organizations">Organizations</a></li>
<li><a href="/reports/published-sites">Organizations With Published Sites</a></li>
<li><a href="/reports/users">Users</a></li>
</ul>
</GridContainer>
73 changes: 73 additions & 0 deletions admin-client/src/pages/user/UsersReport.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script>
import { formatDistanceStrict } from 'date-fns';
import { fetchUsersReport, fetchUsersReportCSV } from '../../lib/api';
import { PaginatedQueryPage, DataTable } from '../../components';
async function downloadCSV() {
const csv = await fetchUsersReportCSV();
const blob = new Blob([csv], { type: 'application/octet-stream' });
const aElement = document.createElement('a');
aElement.setAttribute('download', 'users.csv');
const href = URL.createObjectURL(blob);
aElement.href = href;
aElement.setAttribute('target', '_blank');
aElement.click();
URL.revokeObjectURL(href);
}
</script>

<PaginatedQueryPage path="reports/organizations" title="Users" query={fetchUsersReport} noSearch let:data>
<div>
<button type="button" class="usa-button margin-left-1" on:click={downloadCSV}>Download CSV of All Users</button>
</div>
<DataTable data={data}>
<tr slot="header">
<th scope="col">ID</th>
<th scope="col">Github Email</th>
<th scope="col">UAA Email</th>
<th scope="col">Organizations</th>
<th scope="col">Details</th>
<th scope="col">Created</th>
<th scope="col">Signed In</th>
</tr>
<tr slot="item" let:item={user}>
<td>
<a href="/organizations/{user.id}">{user.id}</a>
</td>
<td>
{#if user.email }
{user.email}
{/if}
</td>
<td>
{#if user.UAAIdentity }
{user.UAAIdentity.email}
{/if}
</td>
<td>
{#if user.OrganizationRoles.length > 0}
{
user.OrganizationRoles.map((orgRole) => `${orgRole.Organization.name}`).join(', ')
}
{/if}
</td>
<td>
{#if user.OrganizationRoles.length > 0}
{
user.OrganizationRoles.map((orgRole) => `${orgRole.Organization.name}: ${orgRole.Role.name}`).join(', ')
}
{/if}
</td>
<td>
{ formatDistanceStrict(new Date(user.createdAt), new Date(), { addSuffix: true, roundingMethod: 'floor' }) }
</td>
<td>
{#if user.signedInAt }
{ formatDistanceStrict(new Date(user.signedInAt), new Date(), { addSuffix: true, roundingMethod: 'floor' }) }
{:else }
never
{/if }
</td>
</tr>
</DataTable>
</PaginatedQueryPage>
1 change: 1 addition & 0 deletions admin-client/src/pages/user/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Show } from './Show.svelte';
export { default as Index } from './Index.svelte';
export { default as Invite } from './Invite.svelte';
export { default as UsersReport } from './UsersReport.svelte';
54 changes: 54 additions & 0 deletions api/admin/controllers/user.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const json2csv = require('@json2csv/plainjs');
const {
Organization, Site, User, Event,
} = require('../../models');
Expand Down Expand Up @@ -52,6 +53,59 @@ module.exports = wrapHandlers({
return res.json(json);
},

async listForUsersReport(req, res) {
const { limit, page } = req.query;

const serialize = users => userSerializer.serializeMany(users, true);

const scopes = ['withUAAIdentity', 'withOrganizationRoles'];

const pagination = await paginate(User.scope(scopes), serialize, { limit, page });

const json = {
meta: {},
...pagination,
};

return res.json(json);
},

async listForUsersReportCSV(req, res) {
const users = await User.scope(['withUAAIdentity', 'withOrganizationRoles']).findAll();

const fields = [
{
label: 'ID',
value: 'id',
},
{
label: 'Email',
value: 'UAAIdentity.email',
},
{
label: 'Organizations',
value: (user => user.OrganizationRoles.map(orgRole => `${orgRole.Organization.name}`).join('|')),
},
{
label: 'Details',
value: (user => user.OrganizationRoles.map(orgRole => `${orgRole.Organization.name}: ${orgRole.Role.name}`).join(', ')),
},
{
label: 'Created',
value: 'createdAt',
},
{
label: 'Last Signed In',
value: 'signedInAt',
},
];

const parser = new json2csv.Parser({ fields });
const csv = parser.parse(users);
res.attachment('users.csv');
return res.send(csv);
},

async findById(req, res) {
const {
params: { id },
Expand Down
2 changes: 2 additions & 0 deletions api/admin/routers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ apiRouter.get('/reports/organizations', AdminControllers.Organization.listOrdere
apiRouter.get('/reports/organizations.csv', AdminControllers.Organization.listOrderedCSV);
apiRouter.get('/reports/published-sites', AdminControllers.Domain.listPublished);
apiRouter.get('/reports/published-sites.csv', AdminControllers.Domain.listPublishedCSV);
apiRouter.get('/reports/users', AdminControllers.User.listForUsersReport);
apiRouter.get('/reports/users.csv', AdminControllers.User.listForUsersReportCSV);
apiRouter.put('/organization-role', AdminControllers.OrganizationRole.update);
apiRouter.get('/roles', AdminControllers.Role.list);
apiRouter.get('/sites', AdminControllers.Site.list);
Expand Down
112 changes: 112 additions & 0 deletions test/api/admin/requests/user.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const request = require('supertest');
const { expect } = require('chai');
const app = require('../../../../api/admin');
const { authenticatedSession } = require('../../support/session');
const sessionConfig = require('../../../../api/admin/sessionConfig');
const factory = require('../../support/factory');
const config = require('../../../../config');
const {
Organization, OrganizationRole, Role, User, UAAIdentity,
} = require('../../../../api/models');
const { createUAAIdentity } = require('../../support/factory/uaa-identity');

describe('Admin - Organizations API', () => {
let userRole;
let managerRole;

before(async () => {
[userRole, managerRole] = await Promise.all([
Role.findOne({ where: { name: 'user' } }),
Role.findOne({ where: { name: 'manager' } }),
]);
});

afterEach(() =>
Promise.all([
Organization.truncate({ force: true, cascade: true }),
OrganizationRole.truncate({ force: true, cascade: true }),
User.truncate({ force: true, cascade: true }),
])
);

describe('GET /admin/reports/users', () => {
it('should require admin authentication', async () => {
const response = await request(app)
['get']('/reports/users')
.expect(401);
expect(response.body.message).to.equal('Unauthorized');
});

it('returns all users', async () => {
const user1 = await factory.user();
const user2 = await factory.user();

const org1 = await factory.organization.create();
const org2 = await factory.organization.create();

org1.addUser(user1, { through: { roleId: managerRole.id } });
org1.addUser(user2, { through: { roleId: userRole.id } });
org2.addUser(user1, { through: { roleId: userRole.id } });

const cookie = await authenticatedSession(user1, sessionConfig);
const { body } = await request(app)
.get('/reports/users')
.set('Cookie', cookie)
.set('Origin', config.app.adminHostname)
.expect(200);

expect(body.data.length).to.equal(2);
ids = body.data.map(user => user['id']);
expect(ids).to.include(user1.id);
expect(ids).to.include(user2.id);
});
});

describe('GET /admin/reports/users.csv', () => {
it('should require admin authentication', async () => {
const response = await request(app)
['get']('/reports/users.csv')
.expect(401);
expect(response.body.message).to.equal('Unauthorized');
});

it('returns all users', async () => {
const user1 = await factory.user();
createUAAIdentity({uaaId: 'user_id_1', email: '[email protected]', userId: user1.id });

const user2 = await factory.user();
createUAAIdentity({uaaId: 'user_id_2', email: '[email protected]', userId: user2.id });

const org1 = await factory.organization.create();
const org2 = await factory.organization.create();

org1.addUser(user1, { through: { roleId: managerRole.id } });
org1.addUser(user2, { through: { roleId: userRole.id } });
org2.addUser(user1, { through: { roleId: userRole.id } });

const cookie = await authenticatedSession(user1, sessionConfig);
const response = await request(app)
.get('/reports/users.csv')
.set('Cookie', cookie)
.set('Origin', config.app.adminHostname)
.expect(200);
expect(response.headers['content-type']).to.equal(
'text/csv; charset=utf-8'
);
expect(response.headers['content-disposition']).to.equal(
'attachment; filename="users.csv"'
);
[header, ...data] = response.text.split(/\n/);
expect(header).to.equal(
'"ID","Email","Organizations","Details","Created","Last Signed In"'
);
expect(data.length).to.equal(2);
expect(data).to.include(
`${user1.id},"[email protected]","${org1.name}|${org2.name}","${org1.name}: manager, ${org2.name}: user","${user1.createdAt.toISOString()}","${user1.signedInAt.toISOString()}"`
);
expect(data).to.include(
`${user2.id},"[email protected]","${org1.name}","${org1.name}: user","${user2.createdAt.toISOString()}","${user2.signedInAt.toISOString()}"`
);
});
});
});

0 comments on commit b64e898

Please sign in to comment.