Skip to content

Commit

Permalink
Super Admin force set password (#1209)
Browse files Browse the repository at this point in the history
  • Loading branch information
codyebberson committed Dec 2, 2022
1 parent 221a1fc commit 6dbdb7f
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 21 deletions.
18 changes: 18 additions & 0 deletions packages/app/src/admin/SuperAdminPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,22 @@ describe('SuperAdminPage', () => {

expect(screen.getByText('Done')).toBeInTheDocument();
});

test('Force set password', async () => {
setup();

await act(async () => {
fireEvent.change(screen.getByLabelText('Email *'), { target: { value: '[email protected]' } });
});

await act(async () => {
fireEvent.change(screen.getByLabelText('Password *'), { target: { value: 'override123' } });
});

await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Force Set Password' }));
});

expect(screen.getByText('Done')).toBeInTheDocument();
});
});
61 changes: 42 additions & 19 deletions packages/app/src/admin/SuperAdminPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, TextInput, Title } from '@mantine/core';
import { Button, Divider, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { normalizeErrorString } from '@medplum/core';
import { Document, Form, FormSection, useMedplum } from '@medplum/react';
Expand Down Expand Up @@ -36,11 +36,18 @@ export function SuperAdminPage(): JSX.Element {
.catch((err) => showNotification({ color: 'red', message: normalizeErrorString(err) }));
}

function forceSetPassword(formData: Record<string, string>): void {
medplum
.post('admin/super/setpassword', formData)
.then(() => showNotification({ color: 'green', message: 'Done' }))
.catch((err) => showNotification({ color: 'red', message: normalizeErrorString(err) }));
}

return (
<Document width={600}>
<Title>Super Admin</Title>
<hr />
<h2>Structure Definitions</h2>
<Title order={1}>Super Admin</Title>
<Divider my="lg" />
<Title order={2}>Structure Definitions</Title>
<p>
StructureDefinition resources contain the metadata about resource types. They are provided with the FHIR
specification. Medplum also includes some custom StructureDefinition resources for internal data types. Press
Expand All @@ -49,8 +56,8 @@ export function SuperAdminPage(): JSX.Element {
<Form>
<Button onClick={rebuildStructureDefinitions}>Rebuild StructureDefinitions</Button>
</Form>
<hr />
<h2>Search Parameters</h2>
<Divider my="lg" />
<Title order={2}>Search Parameters</Title>
<p>
SearchParameter resources contain the metadata about filters and sorting. They are provided with the FHIR
specification. Medplum also includes some custom SearchParameter resources for internal data types. Press this
Expand All @@ -59,31 +66,47 @@ export function SuperAdminPage(): JSX.Element {
<Form>
<Button onClick={rebuildSearchParameters}>Rebuild SearchParameters</Button>
</Form>
<hr />
<h2>Value Sets</h2>
<Divider my="lg" />
<Title order={2}>Value Sets</Title>
<p>
ValueSet resources enum values for a wide variety of use cases. Press this button to update the database
ValueSets from the FHIR specification.
</p>
<Form>
<Button onClick={rebuildValueSets}>Rebuild ValueSets</Button>
</Form>
<hr />
<h2>Reindex Resources</h2>
<Divider my="lg" />
<Title order={2}>Reindex Resources</Title>
<p>
When Medplum changes how resources are indexed, the system may require a reindex for old resources to be indexed
properly.
</p>
<Form>
<FormSection title="Resource Type">
<TextInput
name="resourceType"
placeholder="Resource Type"
defaultValue={resourceType}
onChange={(e) => setResourceType(e.currentTarget.value)}
/>
</FormSection>
<Button onClick={reindexResourceType}>Reindex</Button>
<Stack>
<FormSection title="Resource Type">
<TextInput
name="resourceType"
placeholder="Resource Type"
defaultValue={resourceType}
onChange={(e) => setResourceType(e.currentTarget.value)}
/>
</FormSection>
<Button onClick={reindexResourceType}>Reindex</Button>
</Stack>
</Form>
<Divider my="lg" />
<Title order={2}>Force Set Password</Title>
<p>
Note that this applies to all projects for the user. Therefore, this should only be used in extreme
circumstances. Always prefer to use the "Forgot Password" flow first.
</p>
<Form onSubmit={forceSetPassword}>
<Stack>
<TextInput name="email" label="Email" required />
<PasswordInput name="password" label="Password" required />
<TextInput name="projectId" label="Project ID" />
<Button type="submit">Force Set Password</Button>
</Stack>
</Form>
</Document>
);
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/ResourceInput/ResourceInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const SEARCH_CODES: Record<string, string> = {
Observation: 'code',
RequestGroup: '_id',
ActivityDefinition: 'name',
User: 'email',
};

export interface ResourceInputProps<T extends Resource = Resource> {
Expand Down
65 changes: 65 additions & 0 deletions packages/server/src/admin/super.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { randomUUID } from 'crypto';
import express from 'express';
import request from 'supertest';
import { initApp, shutdownApp } from '../app';
import { registerNew } from '../auth/register';
import { loadTestConfig } from '../config';
import { systemRepo } from '../fhir/repo';
import { generateAccessToken } from '../oauth/keys';
Expand Down Expand Up @@ -199,4 +200,68 @@ describe('Super Admin routes', () => {

expect(res.status).toBe(400);
});

test('Set password access denied', async () => {
const res = await request(app)
.post('/admin/super/setpassword')
.set('Authorization', 'Bearer ' + nonAdminAccessToken)
.type('json')
.send({
email: '[email protected]',
password: 'password123',
});

expect(res.status).toBe(403);
});

test('Set password missing password', async () => {
const res = await request(app)
.post('/admin/super/setpassword')
.set('Authorization', 'Bearer ' + adminAccessToken)
.type('json')
.send({
email: '[email protected]',
password: '',
});

expect(res.status).toBe(400);
expect(res.body.issue[0].details.text).toBe('Invalid password, must be at least 8 characters');
});

test('Set password user not found', async () => {
const res = await request(app)
.post('/admin/super/setpassword')
.set('Authorization', 'Bearer ' + adminAccessToken)
.type('json')
.send({
email: '[email protected]',
password: 'password123',
});

expect(res.status).toBe(400);
expect(res.body.issue[0].details.text).toBe('User not found');
});

test('Set password success', async () => {
const email = `alice${randomUUID()}@example.com`;

await registerNew({
firstName: 'Alice',
lastName: 'Smith',
projectName: 'Alice Project',
email,
password: 'password!@#',
});

const res = await request(app)
.post('/admin/super/setpassword')
.set('Authorization', 'Bearer ' + adminAccessToken)
.type('json')
.send({
email,
password: 'new-password!@#',
});

expect(res.status).toBe(200);
});
});
38 changes: 36 additions & 2 deletions packages/server/src/admin/super.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { allOk, forbidden } from '@medplum/core';
import { allOk, badRequest, forbidden } from '@medplum/core';
import { Request, Response, Router } from 'express';
import { body, validationResult } from 'express-validator';
import { asyncWrap } from '../async';
import { sendOutcome } from '../fhir/outcomes';
import { setPassword } from '../auth/setpassword';
import { invalidRequest, sendOutcome } from '../fhir/outcomes';
import { systemRepo } from '../fhir/repo';
import { validateResourceType } from '../fhir/schema';
import { authenticateToken } from '../oauth/middleware';
import { getUserByEmail } from '../oauth/utils';
import { createSearchParameters } from '../seeds/searchparameters';
import { createStructureDefinitions } from '../seeds/structuredefinitions';
import { createValueSetElements } from '../seeds/valuesets';
Expand Down Expand Up @@ -78,3 +81,34 @@ superAdminRouter.post(
sendOutcome(res, allOk);
})
);

// POST to /admin/super/setpassword
// to force set a User password.
superAdminRouter.post(
'/setpassword',
[
body('email').isEmail().withMessage('Valid email address is required'),
body('password').isLength({ min: 8 }).withMessage('Invalid password, must be at least 8 characters'),
],
asyncWrap(async (req: Request, res: Response) => {
if (!res.locals.login.superAdmin) {
sendOutcome(res, forbidden);
return;
}

const errors = validationResult(req);
if (!errors.isEmpty()) {
sendOutcome(res, invalidRequest(errors));
return;
}

const user = await getUserByEmail(req.body.email, req.body.projectId);
if (!user) {
sendOutcome(res, badRequest('User not found'));
return;
}

await setPassword(user, req.body.password as string);
sendOutcome(res, allOk);
})
);

1 comment on commit 6dbdb7f

@vercel
Copy link

@vercel vercel bot commented on 6dbdb7f Dec 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

medplum-storybook – ./

medplum-storybook-medplum.vercel.app
medplum-storybook-git-main-medplum.vercel.app
storybook.medplum.com

Please sign in to comment.