-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
254 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}"` | ||
); | ||
}); | ||
}); | ||
}); |