Compare commits

..

8 commits

31 changed files with 388 additions and 175 deletions

4
.env
View file

@ -1,7 +1,7 @@
COMPOSE_FILE=docker-compose.dev.yml
COMPOSE_PROJECT_NAME=kandimat
COMPOSE_PROJECT_NAME=candymat
# Backend vars
POSTGRES_PASSWORD=postgres!dev
DATABASE_URL=postgres://kandimat_postgraphile:postgres!dev@postgres:5432/kandimat_db
DATABASE_URL=postgres://candymat_postgraphile:postgres!dev@postgres:5432/candymat_db
JWT_SECRET=asdfasdfasdf

8
.gitmodules vendored
View file

@ -1,4 +1,4 @@
[submodule "kandimat-user-app"]
path = kandimat-user-app
url = https://git.verdigado.com/NB-Public/kandimat-user-app.git
branch = main
[submodule "candymat-user-app"]
path = candymat-user-app
url = git@git.verdigado.com:Netzbegruenung/candymat-user-app.git
branch = develop-candymat

View file

@ -1,8 +1,8 @@
# Kandimat - Wahl-o-Mat fuer Personalwahlen
# Candymat - Wahl-o-Mat fuer Personalwahlen
## Introduction
The kandimat is a Wahl-o-Mat for elections of candidates.
The candymat is a Wahl-o-Mat for elections of candidates.
## Services
@ -27,7 +27,7 @@ and is used to find the perfect candidate for everyone who is allowed to vote.
It is written in vue.js.
See also: [Service Readme](https://git.verdigado.com/Netzbegruenung/kandimat-user-app/src/README.md)
See also: [Service Readme](https://git.verdigado.com/Netzbegruenung/candymat-user-app/src/README.md)
### Postgraphile (Backend)
@ -39,7 +39,7 @@ For more on this (e.g. how to use the graphQl api by yourself) see [backend read
### Check-out repository
* [Install git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* `git clone https://git.verdigado.com/Netzbegruenung/kandimat.git`
* `git clone https://git.verdigado.com/Netzbegruenung/candymat.git`
* To get the (external) user-app source:
```
git submodule update --init
@ -65,8 +65,8 @@ docker-compose up
To start with a clean database, either delete the volume from the postgres configuration in the compose file
or run
```
docker container rm kandimat_postgres_1
docker volume rm kandimat_db-data
docker container rm candymat_postgres_1
docker volume rm candymat_db-data
```
before starting docker-compose.

View file

@ -1,6 +1,6 @@
# Kandimat Backend
# Candymat Backend
The kandimat backend consists of a postgres database and a [postgraphile](https://www.graphile.org/postgraphile/introduction/) instance.
The candymat backend consists of a postgres database and a [postgraphile](https://www.graphile.org/postgraphile/introduction/) instance.
Postgraphile generates a Graphql backend based on the underlying postgres schema.
## Introduction
@ -29,7 +29,7 @@ in setups where there is no public access to the data.
#### Authentication
Authentication is handled via jwt.
The kandimat setup roughly follows the instructions in the [postgraphile docu](https://www.graphile.org/postgraphile/postgresql-schema-design/#authentication-and-authorization).
The candymat setup roughly follows the instructions in the [postgraphile docu](https://www.graphile.org/postgraphile/postgresql-schema-design/#authentication-and-authorization).
## Manually test the backend

View file

@ -1,5 +1,5 @@
# Postgres database setup
POSTGRES_USER=kandimat_postgraphile
POSTGRES_USER=candymat_postgraphile
# Password is handled by docker-compose
POSTGRES_DB=kandimat_db
POSTGRES_SCHEMA=kandimat_data
POSTGRES_DB=candymat_db
POSTGRES_SCHEMA=candymat_data

View file

@ -1,10 +1,10 @@
#!/bin/bash
docker-compose stop postgres
CONTAINER=$(docker image rm kandimat-postgres:11.5 2> >(grep -P '[a-f0-9]{12}' -o) | head -1)
CONTAINER=$(docker image rm candymat-postgres:11.5 2> >(grep -P '[a-f0-9]{12}' -o) | head -1)
echo "Going to remove container: $CONTAINER"
docker container rm $CONTAINER
docker image rm kandimat-postgres:11.5
docker image rm candymat-postgres:11.5
echo "Deleting db-data docker volumes ..."
VOLUMES=$(docker volume ls -q | grep "db-data")
for volume in ${VOLUMES[@]}; do

View file

@ -1,28 +1,28 @@
\connect kandimat_db
\connect candymat_db
-- Create schema for kandimat_data
create SCHEMA kandimat_data;
create SCHEMA kandimat_data_privat;
-- Create schema for candymat_data
create SCHEMA candymat_data;
create SCHEMA candymat_data_privat;
-- create roles
create role kandimat_person;
create role kandimat_anonymous;
create role kandimat_editor;
create role kandimat_candidate;
create role candymat_person;
create role candymat_anonymous;
create role candymat_editor;
create role candymat_candidate;
grant kandimat_editor to kandimat_postgraphile;
grant kandimat_candidate to kandimat_postgraphile;
grant kandimat_person to kandimat_postgraphile, kandimat_candidate, kandimat_editor;
grant kandimat_anonymous to kandimat_postgraphile;
grant candymat_editor to candymat_postgraphile;
grant candymat_candidate to candymat_postgraphile;
grant candymat_person to candymat_postgraphile, candymat_candidate, candymat_editor;
grant candymat_anonymous to candymat_postgraphile;
create type kandimat_data.role as enum (
'kandimat_editor',
'kandimat_candidate',
'kandimat_person'
create type candymat_data.role as enum (
'candymat_editor',
'candymat_candidate',
'candymat_person'
);
-- set table wide permissions
grant usage on schema kandimat_data to kandimat_anonymous, kandimat_person;
grant usage on schema candymat_data to candymat_anonymous, candymat_person;
-- make functions non executeable w/o further checks
alter default privileges revoke execute on functions from public;

View file

@ -1,43 +1,43 @@
-- create table for users
create table kandimat_data.person
create table candymat_data.person
(
row_id serial primary key,
first_name character varying(200) check (first_name <> ''),
last_name character varying(200) check (last_name <> ''),
about character varying(2000),
created_at timestamp default now(),
role kandimat_data.role not null default 'kandimat_person'
role candymat_data.role not null default 'candymat_person'
);
grant select, update, delete on table kandimat_data.person to kandimat_person;
grant select, update, delete on table candymat_data.person to candymat_person;
-- the following is only necessary as long as anonymous should be able to view candidates and editors
grant select on table kandimat_data.person to kandimat_anonymous;
grant select on table candymat_data.person to candymat_anonymous;
-- create table for accounts
create table kandimat_data_privat.person_account
create table candymat_data_privat.person_account
(
person_row_id integer primary key references kandimat_data.person (row_id) on delete cascade,
person_row_id integer primary key references candymat_data.person (row_id) on delete cascade,
email character varying(320) not null unique check (email ~* '^.+@.+\..+$'),
password_hash character varying(256) not null
);
alter table kandimat_data.person
alter table candymat_data.person
enable row level security;
create policy update_person on kandimat_data.person for update to kandimat_person
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 kandimat_data.person for delete to kandimat_person
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
on kandimat_data.person
on candymat_data.person
for select
to kandimat_anonymous, kandimat_person -- maybe change to kandimat_person only in the future
using (role in ('kandimat_editor', 'kandimat_candidate'));
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 kandimat_data.person
on candymat_data.person
for select
to kandimat_editor
to candymat_editor
using (true);

View file

@ -1,51 +1,67 @@
-- create table for categories
create table kandimat_data.category
create table candymat_data.category
(
row_id serial primary key,
title character varying(300) UNIQUE NOT NULL check ( title <> '' ),
description character varying(15000)
);
grant select on table kandimat_data.category to kandimat_person;
-- the following line is only necessary as long as the kandimat should be publicly accessible
grant select on table kandimat_data.category to kandimat_anonymous;
grant insert, update, delete on table kandimat_data.category to kandimat_editor;
grant usage on sequence kandimat_data.category_row_id_seq to kandimat_editor;
grant select on table candymat_data.category to candymat_person;
-- the following line is only necessary as long as the candymat should be publicly accessible
grant select on table candymat_data.category to candymat_anonymous;
grant insert, update, delete on table candymat_data.category to candymat_editor;
grant usage on sequence candymat_data.category_row_id_seq to candymat_editor;
-- create table for questions
create table kandimat_data.question
create table candymat_data.question
(
row_id serial primary key,
category_row_id integer REFERENCES kandimat_data.category (row_id) ON UPDATE CASCADE ON DELETE SET NULL,
category_row_id integer REFERENCES candymat_data.category (row_id) ON UPDATE CASCADE ON DELETE SET NULL,
title character varying(3000) UNIQUE NOT NULL check ( title <> '' ),
description character varying(15000)
);
grant select on table kandimat_data.question to kandimat_person;
-- the following line is only necessary as long as the kandimat should be publicly accessible
grant select on table kandimat_data.question to kandimat_anonymous;
grant insert, update, delete on table kandimat_data.question to kandimat_editor;
grant usage on sequence kandimat_data.question_row_id_seq to kandimat_editor;
grant select on table candymat_data.question to candymat_person;
-- the following line is only necessary as long as the candymat should be publicly accessible
grant select on table candymat_data.question to candymat_anonymous;
grant insert, update, delete on table candymat_data.question to candymat_editor;
grant usage on sequence candymat_data.question_row_id_seq to candymat_editor;
-- create table for answers
create table kandimat_data.answer
create table candymat_data.answer
(
question_row_id integer REFERENCES kandimat_data.question (row_id) ON UPDATE CASCADE ON DELETE CASCADE,
person_row_id integer REFERENCES kandimat_data.person (row_id) ON UPDATE CASCADE ON DELETE CASCADE,
question_row_id integer REFERENCES candymat_data.question (row_id) ON UPDATE CASCADE ON DELETE CASCADE,
person_row_id integer REFERENCES candymat_data.person (row_id) ON UPDATE CASCADE ON DELETE CASCADE,
position integer NOT NULL check (position between 0 and 3),
text character varying(15000),
created_at timestamp default now(),
primary key (question_row_id, person_row_id)
);
grant select on table kandimat_data.answer to kandimat_person;
-- the following line is only necessary as long as the kandimat should be publicly accessible
grant select on table kandimat_data.answer to kandimat_anonymous;
grant insert, update, delete on table kandimat_data.answer to kandimat_candidate;
grant select on table candymat_data.answer to candymat_person;
-- the following line is only necessary as long as the candymat should be publicly accessible
grant select on table candymat_data.answer to candymat_anonymous;
grant insert, update, delete on table candymat_data.answer to candymat_candidate;
alter table kandimat_data.answer
alter table candymat_data.answer
enable row level security;
create policy change_answer on kandimat_data.answer to kandimat_candidate
create policy change_answer on candymat_data.answer to candymat_candidate
using (person_row_id = nullif(current_setting('jwt.claims.person_row_id', true), '')::integer);
create policy select_answer
on kandimat_data.answer
on candymat_data.answer
for select
to kandimat_anonymous, kandimat_person -- maybe change to kandimat_person only in the future
to candymat_anonymous, candymat_person -- maybe change to candymat_person only in the future
using (true);
drop table if exists candymat_data.user_app_info;
create table candymat_data.user_app_info
(
row_id character varying(50) primary key,
title character varying(300) NOT NULL,
content character varying(15000)
);
grant select on table candymat_data.user_app_info to candymat_anonymous, candymat_person;
grant insert, update, delete on table candymat_data.user_app_info to candymat_editor;
delete from candymat_data.user_app_info where row_id = 'about_page';
delete from candymat_data.user_app_info where row_id = 'legal_page';
insert into candymat_data.user_app_info (row_id, title, content) values
('about_page', 'About Candymat', '<h1>Wer steckt eigentlich hinter dem Kandimat?</h1><p>Der Kandimat wurde von den ehrenamtlichen Mitgliedern des Netzbegrünung e.V. entwickelt. Eure Geschäftsstelle und die Kandidat*innen haben die redaktionelle Arbeit für die Inhalte des Kandimats geleistet.</p>');
insert into candymat_data.user_app_info (row_id, title, content) values
('legal_page', 'Legal Candymat', '<h1>Impressum</h1><p>Impressum Infos</p>');

View file

@ -2,8 +2,8 @@ create extension if not exists "pgcrypto";
-- Define JWT claim structure
drop type if exists kandimat_data.jwt_token cascade;
create type kandimat_data.jwt_token as
drop type if exists candymat_data.jwt_token cascade;
create type candymat_data.jwt_token as
(
role text,
person_row_id integer,
@ -12,28 +12,28 @@ create type kandimat_data.jwt_token as
-- Function to get the currently logged-in person
drop function if exists kandimat_data.current_person;
create function kandimat_data.current_person(
) returns kandimat_data.person as
drop function if exists candymat_data.current_person;
create function candymat_data.current_person(
) returns candymat_data.person as
$$
select *
from kandimat_data.person
from candymat_data.person
where row_id = nullif(current_setting('jwt.claims.person_row_id', true), '')::integer
$$ language sql stable;
grant execute on function kandimat_data.current_person() to kandimat_person;
grant execute on function candymat_data.current_person() to candymat_person;
-- Function to register a new user
drop function if exists kandimat_data.register_person;
create function kandimat_data.register_person(
drop function if exists candymat_data.register_person;
create function candymat_data.register_person(
first_name text,
last_name text,
email text,
password text
) returns kandimat_data.person as
) returns candymat_data.person as
$$
declare
person kandimat_data.person;
person candymat_data.person;
begin
if (trim(register_person.first_name) <> '') is not true then
raise 'Invalid first name: ''%''', register_person.first_name;
@ -44,70 +44,70 @@ begin
if (trim(register_person.password) <> '') is not true then
raise 'Invalid password.';
end if;
insert into kandimat_data.person (first_name, last_name)
insert into candymat_data.person (first_name, last_name)
values ($1, $2)
returning * into person;
insert into kandimat_data_privat.person_account (person_row_id, email, password_hash)
insert into candymat_data_privat.person_account (person_row_id, email, password_hash)
values (person.row_id, $3, crypt($4, gen_salt('bf')));
return person;
end ;
$$ language plpgsql strict
security definer;
grant execute on function kandimat_data.register_person(text, text, text, text) to kandimat_anonymous;
grant execute on function candymat_data.register_person(text, text, text, text) to candymat_anonymous;
-- Authenticate: Login for user
drop function if exists kandimat_data.authenticate;
create function kandimat_data.authenticate(
drop function if exists candymat_data.authenticate;
create function candymat_data.authenticate(
email text,
password text
) returns kandimat_data.jwt_token as
) returns candymat_data.jwt_token as
$$
declare
account kandimat_data_privat.person_account;
declare person kandimat_data.person;
account candymat_data_privat.person_account;
declare person candymat_data.person;
begin
select a.*
into account
from kandimat_data_privat.person_account as a
from candymat_data_privat.person_account as a
where a.email = $1;
select p.*
into person
from kandimat_data.person as p
from candymat_data.person as p
where p.row_id = account.person_row_id;
if account.password_hash = crypt(password, account.password_hash) then
return (person.role, account.person_row_id,
extract(epoch from (now() + interval '2 days')))::kandimat_data.jwt_token;
extract(epoch from (now() + interval '2 days')))::candymat_data.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict
security definer;
grant execute on function kandimat_data.authenticate(text, text) to kandimat_anonymous, kandimat_person;
grant execute on function candymat_data.authenticate(text, text) to candymat_anonymous, candymat_person;
-- Change role: Changes role for a given user. Only editors are allowed to use it.
drop function if exists kandimat_data.change_role;
create function kandimat_data.change_role(
drop function if exists candymat_data.change_role;
create function candymat_data.change_role(
person_row_id integer,
new_role kandimat_data.role
new_role candymat_data.role
)
returns kandimat_data.person as
returns candymat_data.person as
$$
declare
person kandimat_data.person;
person candymat_data.person;
begin
update kandimat_data.person
update candymat_data.person
set role = new_role
where kandimat_data.person.row_id = $1
where candymat_data.person.row_id = $1
returning * into person;
return person;
end;
$$ language plpgsql strict security definer;
grant execute on function kandimat_data.change_role(integer, kandimat_data.role) to kandimat_editor;
grant execute on function candymat_data.change_role(integer, candymat_data.role) to candymat_editor;

View file

@ -1,40 +1,40 @@
select kandimat_data.register_person(
select candymat_data.register_person(
'Erika',
'Mustermann',
'erika@mustermann.de',
'password'
);
select kandimat_data.change_role(
select candymat_data.change_role(
1,
'kandimat_editor'
'candymat_editor'
);
select kandimat_data.register_person(
select candymat_data.register_person(
'Max',
'Mustermann',
'max@mustermann.de',
'password'
);
select kandimat_data.change_role(
select candymat_data.change_role(
2,
'kandimat_candidate'
'candymat_candidate'
);
select kandimat_data.register_person(
select candymat_data.register_person(
'Tricia',
'McMillan',
'trillian@universe.com',
'password'
);
select kandimat_data.change_role(
select candymat_data.change_role(
3,
'kandimat_candidate'
'candymat_candidate'
);
select kandimat_data.register_person(
select candymat_data.register_person(
'Happy',
'User',
'happy@user.de',
'password'
);
select kandimat_data.change_role(
select candymat_data.change_role(
4,
'kandimat_person'
'candymat_person'
);

View file

@ -1,9 +1,9 @@
insert into kandimat_data.category (title, description) values
insert into candymat_data.category (title, description) values
('Umwelt', 'Themen rund um Naturschutz usw.');
insert into kandimat_data.category (title, description) values
insert into candymat_data.category (title, description) values
('Sonstiges', '');
insert into kandimat_data.question (category_row_id, title, description) values
insert into candymat_data.question (category_row_id, title, description) values
(1, 'Was sagen Sie zur 10H Regel?', 'In Bayern dürfen Windräder nur ...');
insert into kandimat_data.question (category_row_id, title, description) values
insert into candymat_data.question (category_row_id, title, description) values
(2, 'Umgehungsstraße XY?', 'Zur Entlastung der Hauptstraße ...');

View file

@ -1,9 +1,9 @@
insert into kandimat_data.answer (question_row_id, person_row_id, position, text)
insert into candymat_data.answer (question_row_id, person_row_id, position, text)
values (1, 2, 2, 'bin dagegen');
insert into kandimat_data.answer (question_row_id, person_row_id, position, text)
insert into candymat_data.answer (question_row_id, person_row_id, position, text)
values (2, 2, 0, 'bin dafür');
insert into kandimat_data.answer (question_row_id, person_row_id, position, text)
insert into candymat_data.answer (question_row_id, person_row_id, position, text)
values (1, 3, 1, 'mir egal');
insert into kandimat_data.answer (question_row_id, person_row_id, position, text)
insert into candymat_data.answer (question_row_id, person_row_id, position, text)
values (2, 3, 3, 'keine lust mehr');

1
candymat-user-app Submodule

@ -0,0 +1 @@
Subproject commit 4e290449cb4b5821f5542d1ea6a9a629b99e6aa3

View file

@ -15,7 +15,7 @@ services:
user-app:
build:
context: kandimat-user-app/
context: ./candymat-user-app/
dockerfile: ./dev.Dockerfile
depends_on:
- graphql
@ -25,7 +25,7 @@ services:
- frontend
postgres:
image: kandimat-postgres:11.5
image: candymat-postgres:11.5
build:
dockerfile: ./Dockerfile
context: ./backend/
@ -53,9 +53,9 @@ services:
"--host", "0.0.0.0",
"--port", "5000",
"--cors",
"--schema", "kandimat_data",
"--default-role", "kandimat_anonymous",
"--jwt-token-identifier", "kandimat_data.jwt_token",
"--schema", "candymat_data",
"--default-role", "candymat_anonymous",
"--jwt-token-identifier", "candymat_data.jwt_token",
"--jwt-secret", $JWT_SECRET,
"--watch",
"--retry-on-init-fail",

@ -1 +0,0 @@
Subproject commit dc1602b6e87c80d3c37f92bc87f02c98865895d3

View file

@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="App zum Erstellen von Fragen für den Kandimat"
content="App zum Erstellen von Fragen für den Candymat"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Kandimat Redaktion</title>
<title>Candymat Redaktion</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -1,6 +1,6 @@
{
"short_name": "Kandimat Redaktion",
"name": "Kandimat Redaktions App",
"short_name": "Candymat Redaktion",
"name": "Candymat Redaktions App",
"icons": [
{
"src": "favicon.ico",

View file

@ -0,0 +1,25 @@
import { gql } from "@apollo/client";
import { GetAllPageInfoResponse } from "../queries/page_info";
export const EDIT_INFOS = gql`
mutation UpdateInfos(
$id: ID!
$title: String
$content: String
$rowId: String
) {
updateUserAppInfo(
input: {
id: $id
userAppInfoPatch: { content: $content, rowId: $rowId, title: $title }
clientMutationId: ""
}
) {
clientMutationId
}
}
`;
export interface EditInfosResponse {
updateUserAppInfo: GetAllPageInfoResponse;
}

View file

@ -0,0 +1,28 @@
import { gql } from "@apollo/client";
export const GET_ALL_PAGE_INFO = gql`
query AllInfos {
allUserAppInfos {
nodes {
id
rowId
title
content
}
}
}
`;
export interface PageInfo {
id: string;
rowId: string;
title: string;
content: string;
}
export interface GetAllPageInfoResponse {
allUserAppInfos: {
nodes: Array<PageInfo>;
__typename: "UserAppInfosConnection";
};
}

View file

@ -12,7 +12,7 @@ export const getPersonsByRoleAllFilledData: GetPersonsSortedByRoleResponse = {
rowId: 1,
firstName: "Erika",
lastName: "Mustermann",
role: "KANDIMAT_EDITOR",
role: "CANDYMAT_EDITOR",
__typename: "Person",
},
],
@ -25,7 +25,7 @@ export const getPersonsByRoleAllFilledData: GetPersonsSortedByRoleResponse = {
rowId: 2,
firstName: "Max",
lastName: "Mustermann",
role: "KANDIMAT_CANDIDATE",
role: "CANDYMAT_CANDIDATE",
__typename: "Person",
},
{
@ -33,7 +33,7 @@ export const getPersonsByRoleAllFilledData: GetPersonsSortedByRoleResponse = {
rowId: 3,
firstName: "Tricia",
lastName: "McMillan",
role: "KANDIMAT_CANDIDATE",
role: "CANDYMAT_CANDIDATE",
__typename: "Person",
},
],
@ -46,7 +46,7 @@ export const getPersonsByRoleAllFilledData: GetPersonsSortedByRoleResponse = {
rowId: 4,
firstName: "Happy",
lastName: "User",
role: "KANDIMAT_PERSON",
role: "CANDYMAT_PERSON",
__typename: "Person",
},
],
@ -76,7 +76,7 @@ export const getPersonsByRoleNoCandidatesData: GetPersonsSortedByRoleResponse =
rowId: 1,
firstName: "Erika",
lastName: "Mustermann",
role: "KANDIMAT_EDITOR",
role: "CANDYMAT_EDITOR",
__typename: "Person",
},
],
@ -93,7 +93,7 @@ export const getPersonsByRoleNoCandidatesData: GetPersonsSortedByRoleResponse =
rowId: 2,
firstName: "Max",
lastName: "Mustermann",
role: "KANDIMAT_PERSON",
role: "CANDYMAT_PERSON",
__typename: "Person",
},
{
@ -101,7 +101,7 @@ export const getPersonsByRoleNoCandidatesData: GetPersonsSortedByRoleResponse =
rowId: 3,
firstName: "Tricia",
lastName: "McMillan",
role: "KANDIMAT_PERSON",
role: "CANDYMAT_PERSON",
__typename: "Person",
},
{
@ -109,7 +109,7 @@ export const getPersonsByRoleNoCandidatesData: GetPersonsSortedByRoleResponse =
rowId: 4,
firstName: "Happy",
lastName: "User",
role: "KANDIMAT_PERSON",
role: "CANDYMAT_PERSON",
__typename: "Person",
},
],

View file

@ -22,17 +22,17 @@ export interface BasicPersonResponse {
export const GET_PERSONS_SORTED_BY_ROLE = gql`
query AllPeople {
editors: allPeople(condition: { role: KANDIMAT_EDITOR }) {
editors: allPeople(condition: { role: CANDYMAT_EDITOR }) {
nodes {
...BasicPersonFragment
}
}
candidates: allPeople(condition: { role: KANDIMAT_CANDIDATE }) {
candidates: allPeople(condition: { role: CANDYMAT_CANDIDATE }) {
nodes {
...BasicPersonFragment
}
}
users: allPeople(condition: { role: KANDIMAT_PERSON }) {
users: allPeople(condition: { role: CANDYMAT_PERSON }) {
nodes {
...BasicPersonFragment
}

View file

@ -51,11 +51,11 @@ export default function ChangeRoleMenu(
);
const displayRole = (role: UppercaseUserRole) => {
switch (role) {
case "KANDIMAT_CANDIDATE":
case "CANDYMAT_CANDIDATE":
return "zu Kandidat:in machen";
case "KANDIMAT_EDITOR":
case "CANDYMAT_EDITOR":
return "zu RedakteurIn machen";
case "KANDIMAT_PERSON":
case "CANDYMAT_PERSON":
return "zu Standard User machen";
}
};

View file

@ -63,7 +63,7 @@ function CustomAppBar(): React.ReactElement {
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Kandimat
Candymat
</Typography>
<ProfileMenu />
</Toolbar>

View file

@ -0,0 +1,59 @@
import React from "react";
import { Paper, Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { useQuery } from "@apollo/client";
import {
GET_ALL_PAGE_INFO,
GetAllPageInfoResponse,
PageInfo,
} from "../backend/queries/page_info";
import { EditInformationField } from "./EditInformationField";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
padding: theme.spacing(1),
marginBottom: theme.spacing(3),
},
textArea: {
width: "85%",
height: "150px",
padding: theme.spacing(1),
marginBottom: theme.spacing(3),
marginTop: theme.spacing(1),
resize: "none",
overflow: "auto",
},
}));
interface EditInformationProps {
loggedInPersonRowId: number;
}
export function EditInformation(
props: EditInformationProps
): React.ReactElement {
const infos =
useQuery<GetAllPageInfoResponse, null>(GET_ALL_PAGE_INFO).data
?.allUserAppInfos.nodes || [];
const classes = useStyles();
return (
<React.Fragment>
<Typography component={"h2"} variant="h6" gutterBottom>
Bearbeite hier die Webseiten Info-Texte für deinen Candymat:
</Typography>
<Paper className={classes.root}>
{infos.map((info: PageInfo) => {
return (
<EditInformationField
loggedInPersonRowId={props.loggedInPersonRowId}
key={info.id}
info={info}
/>
);
})}
</Paper>
</React.Fragment>
);
}

View file

@ -0,0 +1,74 @@
import React, { useState } from "react";
import { IconButton, Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { useMutation } from "@apollo/client";
import { Save } from "@material-ui/icons";
import { EDIT_INFOS, EditInfosResponse } from "../backend/mutations/page_info";
import { PageInfo } from "../backend/queries/page_info";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
padding: theme.spacing(1),
marginBottom: theme.spacing(3),
},
textArea: {
width: "85%",
height: "150px",
padding: theme.spacing(1),
marginBottom: theme.spacing(3),
marginTop: theme.spacing(1),
resize: "none",
overflow: "auto",
},
}));
interface EditInformationFieldProps {
info: PageInfo;
loggedInPersonRowId: number;
}
export function EditInformationField(
props: EditInformationFieldProps
): React.ReactElement {
const [info, setInfo] = useState(props.info);
const [edit, { loading, error }] = useMutation<EditInfosResponse>(EDIT_INFOS);
const classes = useStyles();
if (loading) console.log("Loading");
if (error) return <p>An error occurred</p>;
function changeInfo(e: React.ChangeEvent<HTMLTextAreaElement>) {
const changeInfoText = e.target.value;
setInfo({ ...info, content: changeInfoText });
}
return (
<div>
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
{info.title}
</Typography>
<form>
<textarea
className={classes.textArea}
onChange={changeInfo}
value={info.content}
>
{/*Hier kommt der Inhalt aus der Datenbank*/}
</textarea>
<IconButton
onClick={() =>
edit({
variables: {
id: info.id,
content: info.content,
},
})
}
>
<Save />
</IconButton>
</form>
</div>
);
}

View file

@ -10,6 +10,8 @@ import PeopleIcon from "@material-ui/icons/People";
import { MenuOption } from "./MainMenu";
import { PersonRoutes } from "./Main";
import { UserManagement } from "./UserManagement";
import { InfoRounded } from "@material-ui/icons";
import { EditInformation } from "./EditInformation";
const useStyles = makeStyles((theme) => ({
container: {
@ -22,6 +24,7 @@ const useStyles = makeStyles((theme) => ({
interface EditorRoutes extends PersonRoutes {
question: MenuOption;
userManagement: MenuOption;
editInformation: MenuOption;
}
export const editorRoutes: EditorRoutes = {
@ -35,6 +38,11 @@ export const editorRoutes: EditorRoutes = {
path: "/benutzer",
icon: <PeopleIcon />,
},
editInformation: {
title: "Infos bearbeiten",
path: "/edit",
icon: <InfoRounded />,
},
};
interface HomePageEditorProps {
@ -54,6 +62,9 @@ export function HomePageEditor(props: HomePageEditorProps): React.ReactElement {
<Route path={editorRoutes.userManagement.path}>
<UserManagement loggedInPersonRowId={props.loggedInUserRowId} />
</Route>
<Route path={editorRoutes.editInformation.path}>
<EditInformation loggedInPersonRowId={props.loggedInUserRowId} />
</Route>
</Switch>
<Copyright />
</Container>

View file

@ -24,14 +24,14 @@ const baseJwt: JwtPayload = {
exp: 0,
iat: 0,
iss: "postgraphile",
role: "kandimat_person",
role: "candymat_person",
person_row_id: 3,
};
describe("As an editor, the main page", () => {
const jwt: JwtPayload = {
...baseJwt,
role: "kandimat_editor",
role: "candymat_editor",
person_row_id: 1,
};
@ -65,7 +65,7 @@ describe("As a candidate, the main page", () => {
test("displays the candidate's home page ", () => {
const jwt: JwtPayload = {
...baseJwt,
role: "kandimat_candidate",
role: "candymat_candidate",
person_row_id: 2,
};
renderMainPage(jwt);
@ -81,7 +81,7 @@ describe("As a simple user, the main page", () => {
test("displays the user's home page.", () => {
const jwt: JwtPayload = {
...baseJwt,
role: "kandimat_person",
role: "candymat_person",
person_row_id: 3,
};
renderMainPage(jwt);

View file

@ -51,24 +51,24 @@ function Main(props: MainProps): ReactElement {
const getHomePage = (): ReactElement => {
switch (props.userRole) {
case "kandimat_editor":
case "candymat_editor":
return <HomePageEditor loggedInUserRowId={props.loggedInUserRowId} />;
case "kandimat_candidate":
case "candymat_candidate":
return (
<HomePageCandidate loggedInPersonRowId={props.loggedInUserRowId} />
);
case "kandimat_person":
case "candymat_person":
return <HomePageUser />;
}
};
const getMenuOptions = (): Array<MenuOption> => {
switch (props.userRole) {
case "kandimat_editor":
case "candymat_editor":
return Object.values(editorRoutes);
case "kandimat_candidate":
case "candymat_candidate":
return Object.values(candidateRoutes);
case "kandimat_person":
case "candymat_person":
return Object.values(userRoutes);
}
};

View file

@ -1,7 +1,7 @@
import { parseJwt } from "./jwt";
describe("The parseJwt function", () => {
test("parses a valid kandimat jwt", () => {
test("parses a valid candymat jwt", () => {
const validJwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6MSwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
@ -9,7 +9,7 @@ describe("The parseJwt function", () => {
expect(jwt).not.toBeNull();
expect(jwt?.person_row_id).toBe(1);
expect(jwt?.role).toBe("kandimat_editor");
expect(jwt?.role).toBe("candymat_editor");
});
test("returns null if role claim is invalid", () => {

View file

@ -3,14 +3,14 @@ import { client } from "../backend/helper";
type Claim = "role" | "person_row_id" | "exp" | "iat" | "aud" | "iss";
export type UppercaseUserRole =
| "KANDIMAT_PERSON"
| "KANDIMAT_EDITOR"
| "KANDIMAT_CANDIDATE";
| "CANDYMAT_PERSON"
| "CANDYMAT_EDITOR"
| "CANDYMAT_CANDIDATE";
export type UserRole =
| "kandimat_editor"
| "kandimat_candidate"
| "kandimat_person";
| "candymat_editor"
| "candymat_candidate"
| "candymat_person";
export interface JwtPayload {
role: UserRole;
@ -24,15 +24,15 @@ export interface JwtPayload {
const CLAIMS: Claim[] = ["role", "person_row_id", "exp", "iat", "aud", "iss"];
export const UPPERCASE_USER_ROLES: UppercaseUserRole[] = [
"KANDIMAT_PERSON",
"KANDIMAT_EDITOR",
"KANDIMAT_CANDIDATE",
"CANDYMAT_PERSON",
"CANDYMAT_EDITOR",
"CANDYMAT_CANDIDATE",
];
export const USER_ROLES: UserRole[] = [
"kandimat_editor",
"kandimat_candidate",
"kandimat_person",
"candymat_editor",
"candymat_candidate",
"candymat_person",
];
export const getRawJsonWebToken = (): string | null => {