#20 Add UserManagement page

Connects to backend and gets all registered users by role.
Enabled editors to see all registered users which wasn't possible.
This commit is contained in:
Christoph Lienhard 2021-03-30 18:24:41 +02:00
parent 765443ebc2
commit e26a154518
Signed by: christoph.lienhard
GPG Key ID: 6B98870DDC270884
7 changed files with 377 additions and 1 deletions

View File

@ -19,12 +19,14 @@ create table candymat_data_privat.person_account
email character varying(320) not null unique check (email ~* '^.+@.+\..+$'),
password_hash character varying(256) not null
);
alter table candymat_data.person
enable row level security;
create policy update_person on candymat_data.person for update to candymat_person
with check (row_id = nullif(current_setting('jwt.claims.person_row_id', true), '')::integer);
create policy delete_person on candymat_data.person for delete to candymat_person
using (row_id = nullif(current_setting('jwt.claims.person_row_id', true), '')::integer);
-- The following enables viewing candidates and editors information for every person.
-- This may be changed to only enable registered (and verified) persons.
create policy select_person_public
@ -32,3 +34,10 @@ create policy select_person_public
for select
to candymat_anonymous, candymat_person -- maybe change to candymat_person only in the future
using (role in ('candymat_editor', 'candymat_candidate'));
-- Editors can see all registered persons in order to elevate their privileges
create policy select_person_editor
on candymat_data.person
for select
to candymat_editor
using (true);

View File

@ -0,0 +1,130 @@
import { MockedResponse } from "@apollo/client/testing";
import {
GET_PERSONS_SORTED_BY_ROLE,
GetPersonsSortedByRoleResponse,
} from "./person";
export const getPersonsByRoleAllFilledData: GetPersonsSortedByRoleResponse = {
editors: {
nodes: [
{
id: "WyJwZW9wbGUiLDFd",
rowId: 1,
firstName: "Erika",
lastName: "Mustermann",
role: "candymat_editor",
__typename: "Person",
},
],
__typename: "PeopleConnection",
},
candidates: {
nodes: [
{
id: "WyJwZW9wbGUiLDJd",
rowId: 2,
firstName: "Max",
lastName: "Mustermann",
role: "candymat_candidate",
__typename: "Person",
},
{
id: "WyJwZW9wbGUiLDNd",
rowId: 3,
firstName: "Tricia",
lastName: "McMillan",
role: "candymat_candidate",
__typename: "Person",
},
],
__typename: "PeopleConnection",
},
users: {
nodes: [
{
id: "WyJwZW9wbGUiLDRd",
rowId: 4,
firstName: "Happy",
lastName: "User",
role: "candymat_person",
__typename: "Person",
},
],
__typename: "PeopleConnection",
},
};
export const getPersonsSortedByRoleAllFilledMock: Array<
MockedResponse<GetPersonsSortedByRoleResponse>
> = [
{
request: {
query: GET_PERSONS_SORTED_BY_ROLE,
},
result: {
data: getPersonsByRoleAllFilledData,
},
},
];
export const getPersonsByRoleNoCandidatesData: GetPersonsSortedByRoleResponse = {
editors: {
nodes: [
{
id: "WyJwZW9wbGUiLDFd",
rowId: 1,
firstName: "Erika",
lastName: "Mustermann",
role: "candymat_editor",
__typename: "Person",
},
],
__typename: "PeopleConnection",
},
candidates: {
nodes: [],
__typename: "PeopleConnection",
},
users: {
nodes: [
{
id: "WyJwZW9wbGUiLDJd",
rowId: 2,
firstName: "Max",
lastName: "Mustermann",
role: "candymat_person",
__typename: "Person",
},
{
id: "WyJwZW9wbGUiLDNd",
rowId: 3,
firstName: "Tricia",
lastName: "McMillan",
role: "candymat_person",
__typename: "Person",
},
{
id: "WyJwZW9wbGUiLDRd",
rowId: 4,
firstName: "Happy",
lastName: "User",
role: "candymat_person",
__typename: "Person",
},
],
__typename: "PeopleConnection",
},
};
export const getPersonsSortedByRoleNoCandidatesMock: Array<
MockedResponse<GetPersonsSortedByRoleResponse>
> = [
{
request: {
query: GET_PERSONS_SORTED_BY_ROLE,
},
result: {
data: getPersonsByRoleNoCandidatesData,
},
},
];

View File

@ -0,0 +1,57 @@
import { gql } from "@apollo/client";
import { UserRole } from "../../jwt/jwt";
const BasicPersonFragment = gql`
fragment BasicPersonFragment on Person {
id
rowId
firstName
lastName
role
}
`;
export interface BasicPersonResponse {
id: string;
rowId: number;
firstName: string | null;
lastName: string | null;
role: UserRole;
__typename: "Person";
}
export const GET_PERSONS_SORTED_BY_ROLE = gql`
query AllPeople {
editors: allPeople(condition: { role: CANDYMAT_EDITOR }) {
nodes {
...BasicPersonFragment
}
}
candidates: allPeople(condition: { role: CANDYMAT_CANDIDATE }) {
nodes {
...BasicPersonFragment
}
}
users: allPeople(condition: { role: CANDYMAT_PERSON }) {
nodes {
...BasicPersonFragment
}
}
}
${BasicPersonFragment}
`;
export interface GetPersonsSortedByRoleResponse {
editors: {
nodes: Array<BasicPersonResponse>;
__typename: "PeopleConnection";
};
candidates: {
nodes: Array<BasicPersonResponse>;
__typename: "PeopleConnection";
};
users: {
nodes: Array<BasicPersonResponse>;
__typename: "PeopleConnection";
};
}

View File

@ -9,6 +9,7 @@ import QuestionAnswerIcon from "@material-ui/icons/QuestionAnswer";
import PeopleIcon from "@material-ui/icons/People";
import { MenuOption } from "./MainMenu";
import { PersonRoutes } from "./Main";
import { UserManagement } from "./UserManagement";
const useStyles = makeStyles((theme) => ({
container: {
@ -47,7 +48,7 @@ export function HomePageEditor(): React.ReactElement {
<CategoryList />
</Route>
<Route path={editorRoutes.userManagement.path}>
<div />
<UserManagement />
</Route>
</Switch>
<Copyright />

View File

@ -0,0 +1,52 @@
import {
Avatar,
Card,
CardActionArea,
CardHeader,
Paper,
Typography,
} from "@material-ui/core";
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
marginBottom: theme.spacing(1),
display: "flex",
flexDirection: "row",
alignItems: "center",
},
avatar: {
margin: theme.spacing(1),
},
title: {
marginLeft: theme.spacing(1),
},
}));
interface PersonCardProps {
firstName: string;
lastName: string;
}
export default function PersonCard(props: PersonCardProps): React.ReactElement {
const classes = useStyles();
const getInitials = (): string => {
const initials =
(props.firstName.charAt(0) || "") + (props.lastName.charAt(0) || "");
return initials ? initials.toUpperCase() : "?";
};
const fullName = `${props.firstName} ${props.lastName}`.trim();
return (
<Paper className={classes.root}>
<Avatar className={classes.avatar} aria-label={fullName}>
{getInitials()}
</Avatar>
<Typography className={classes.title}>{fullName}</Typography>
</Paper>
);
}

View File

@ -0,0 +1,60 @@
import React from "react";
import { Paper, Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import PersonCard from "./PersonCard";
import { useQuery } from "@apollo/client";
import {
BasicPersonResponse,
GET_PERSONS_SORTED_BY_ROLE,
GetPersonsSortedByRoleResponse,
} from "../backend/queries/person";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
padding: theme.spacing(1),
marginBottom: theme.spacing(3),
},
}));
export function UserManagement(): React.ReactElement {
const classes = useStyles();
const persons = useQuery<GetPersonsSortedByRoleResponse>(
GET_PERSONS_SORTED_BY_ROLE
).data;
const convertPersonNodeToPersonCard = (
person: BasicPersonResponse
): JSX.Element => {
return (
<PersonCard
key={person.id}
firstName={person.firstName || ""}
lastName={person.lastName || ""}
/>
);
};
return (
<div>
<Paper className={classes.root}>
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
Redaktion
</Typography>
{persons?.editors.nodes.map(convertPersonNodeToPersonCard)}
</Paper>
<Paper className={classes.root}>
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
Kandidat:innen
</Typography>
{persons?.candidates.nodes.map(convertPersonNodeToPersonCard)}
</Paper>
<Paper className={classes.root}>
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
Andere registrierte Personen
</Typography>
{persons?.users.nodes.map(convertPersonNodeToPersonCard)}
</Paper>
</div>
);
}

View File

@ -0,0 +1,67 @@
import React from "react";
import { findByText, render, screen } from "@testing-library/react";
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
import { MemoryRouter } from "react-router-dom";
import { SnackbarProvider } from "notistack";
import { UserManagement } from "../components/UserManagement";
import {
getPersonsSortedByRoleAllFilledMock,
getPersonsSortedByRoleNoCandidatesMock,
} from "../backend/queries/person.mock";
function renderUserManagementPage(mocks: Array<MockedResponse>) {
return render(
<MockedProvider mocks={mocks}>
<MemoryRouter>
<SnackbarProvider>
<UserManagement />
</SnackbarProvider>
</MemoryRouter>
</MockedProvider>
);
}
describe("The UserManagement page", () => {
test(" displays all registered persons sorted by role.", async () => {
renderUserManagementPage(getPersonsSortedByRoleAllFilledMock);
const editorsSectionTitle = await screen.findByText(/Redaktion/);
const editorsSection = editorsSectionTitle.parentElement as HTMLElement;
const editor = await findByText(editorsSection, /Erika Mustermann/);
expect(editor).not.toBeNull();
const candidatesSectionTitle = await screen.findByText(/Kandidat/);
const candidatesSection = candidatesSectionTitle.parentElement as HTMLElement;
const candidate1 = await findByText(candidatesSection, /Max Mustermann/);
const candidate2 = await findByText(candidatesSection, /Tricia McMillan/);
expect(candidate1).not.toBeNull();
expect(candidate2).not.toBeNull();
const otherUsersSectionTitle = await screen.findByText(/Andere/);
const otherUsersSection = otherUsersSectionTitle.parentElement as HTMLElement;
const otherUser = await findByText(otherUsersSection, /Happy User/);
expect(otherUser).not.toBeNull();
});
test(" displays all registered persons sorted by role even though there are no candidates.", async () => {
renderUserManagementPage(getPersonsSortedByRoleNoCandidatesMock);
const editorsSectionTitle = await screen.findByText(/Redaktion/);
const editorsSection = editorsSectionTitle.parentElement as HTMLElement;
const editor = await findByText(editorsSection, /Erika Mustermann/);
expect(editor).not.toBeNull();
const candidatesSectionTitle = await screen.findByText(/Kandidat/);
const candidatesSection = candidatesSectionTitle.parentElement as HTMLElement;
expect(candidatesSection.childElementCount).toBe(1);
const otherUsersSectionTitle = await screen.findByText(/Andere/);
const otherUsersSection = otherUsersSectionTitle.parentElement as HTMLElement;
const otherUser1 = await findByText(otherUsersSection, /Max Mustermann/);
const otherUser2 = await findByText(otherUsersSection, /Tricia McMillan/);
const otherUser3 = await findByText(otherUsersSection, /Happy User/);
expect(otherUser1).not.toBeNull();
expect(otherUser2).not.toBeNull();
expect(otherUser3).not.toBeNull();
});
});