-
-
-
-
-
+ {getMainPage()}
)
diff --git a/redaktions-app/src/components/MainPageCandidate.tsx b/redaktions-app/src/components/MainPageCandidate.tsx
new file mode 100644
index 0000000..c03f919
--- /dev/null
+++ b/redaktions-app/src/components/MainPageCandidate.tsx
@@ -0,0 +1,21 @@
+import {Container} from "@material-ui/core";
+import React from "react";
+import {makeStyles} from "@material-ui/core/styles";
+
+const useStyles = makeStyles((theme) => ({
+ container: {
+ paddingTop: theme.spacing(4),
+ paddingBottom: theme.spacing(4),
+ flexDirection: 'column',
+ },
+}));
+
+export function MainPageCandidate() {
+ const classes = useStyles();
+
+ return (
+
+ Under construction
+
+ );
+}
diff --git a/redaktions-app/src/components/MainPageEditor.tsx b/redaktions-app/src/components/MainPageEditor.tsx
new file mode 100644
index 0000000..ae0e6db
--- /dev/null
+++ b/redaktions-app/src/components/MainPageEditor.tsx
@@ -0,0 +1,26 @@
+import {Container} from "@material-ui/core";
+import QuestionList from "./QuestionList";
+import CategoryList from "./CategoryList";
+import {Copyright} from "./Copyright";
+import React from "react";
+import {makeStyles} from "@material-ui/core/styles";
+
+const useStyles = makeStyles((theme) => ({
+ container: {
+ paddingTop: theme.spacing(4),
+ paddingBottom: theme.spacing(4),
+ flexDirection: 'column',
+ },
+}));
+
+export function MainPageEditor() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/redaktions-app/src/components/MainPageUser.tsx b/redaktions-app/src/components/MainPageUser.tsx
new file mode 100644
index 0000000..01afac5
--- /dev/null
+++ b/redaktions-app/src/components/MainPageUser.tsx
@@ -0,0 +1,21 @@
+import {Container} from "@material-ui/core";
+import React from "react";
+import {makeStyles} from "@material-ui/core/styles";
+
+const useStyles = makeStyles((theme) => ({
+ container: {
+ paddingTop: theme.spacing(4),
+ paddingBottom: theme.spacing(4),
+ flexDirection: 'column',
+ },
+}));
+
+export function MainPageUser() {
+ const classes = useStyles();
+
+ return (
+
+ Sorry, für dich gibt es hier leider nichts zu sehen...
+
+ );
+}
diff --git a/redaktions-app/src/jwt/jwt.test.ts b/redaktions-app/src/jwt/jwt.test.ts
new file mode 100644
index 0000000..396eed7
--- /dev/null
+++ b/redaktions-app/src/jwt/jwt.test.ts
@@ -0,0 +1,36 @@
+import {parseJwt} from "./jwt";
+
+describe("The parseJwt function", () => {
+ test("parses a valid candymat jwt", () => {
+ const validJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6MSwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
+
+ const jwt = parseJwt(validJwt)
+
+ expect(jwt).not.toBeNull();
+ expect(jwt?.person_row_id).toBe(1);
+ expect(jwt?.role).toBe("candymat_editor");
+ });
+
+ test("returns null if role claim is invalid", () => {
+ const invalidRoleClaimJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHllZGl0b3IiLCJwZXJzb25fcm93X2lkIjoxLCJleHAiOjE2MDk0MTIxNjgsImlhdCI6MTYwOTIzOTM2OCwiYXVkIjoicG9zdGdyYXBoaWxlIiwiaXNzIjoicG9zdGdyYXBoaWxlIn0._AVFTMqMkIuyrfQGTmWE-Qi-C72KCrZ3s_uVyfuEDco";
+
+ const jwt = parseJwt(invalidRoleClaimJwt);
+
+ expect(jwt).toBeNull();
+ })
+
+ test("returns null if person_row_id is not a number", () => {
+ const invalidRowIdClaimJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6IjEiLCJleHAiOjE2MDk0MTIxNjgsImlhdCI6MTYwOTIzOTM2OCwiYXVkIjoicG9zdGdyYXBoaWxlIiwiaXNzIjoicG9zdGdyYXBoaWxlIn0.NfXylzN44qrZA5DX0qxxU71vJ1o9gdunscnK6V193Fc";
+
+ const jwt = parseJwt(invalidRowIdClaimJwt);
+
+ expect(jwt).toBeNull();
+ })
+
+ test("returns null if token is rubish.", () => {
+ const brokenJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eysssssssssssssssssssss.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
+ const jwt = parseJwt(brokenJwt);
+
+ expect(jwt).toBeNull();
+ })
+});
diff --git a/redaktions-app/src/jwt/jwt.ts b/redaktions-app/src/jwt/jwt.ts
new file mode 100644
index 0000000..85f7801
--- /dev/null
+++ b/redaktions-app/src/jwt/jwt.ts
@@ -0,0 +1,48 @@
+export const getRawJsonWebToken = (): string | null => {
+ return localStorage.getItem('token');
+}
+
+export const getJsonWebToken = (): JwtPayload | null => {
+ const rawToken = getRawJsonWebToken();
+ return rawToken ? parseJwt(rawToken) : null
+}
+
+export const parseJwt = (token: string): JwtPayload | null => {
+ try {
+ const base64Url = token.split('.')[1];
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const jsonPayload = decodeURIComponent(
+ atob(base64)
+ .split('')
+ .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
+ .join('')
+ );
+ const jwtPayload = JSON.parse(jsonPayload);
+ return isJwtPayloadValid(jwtPayload) ? jwtPayload : null
+ } catch {
+ return null
+ }
+}
+
+export const isJwtPayloadValid = (jwtPayload: JwtPayload): boolean => {
+ return claims.every(claim => Object.keys(jwtPayload).includes(claim))
+ && userRoles.includes(jwtPayload.role)
+ && typeof (jwtPayload.person_row_id) === 'number'
+ && typeof (jwtPayload.exp) === 'number'
+ && typeof (jwtPayload.iat) === 'number';
+}
+
+const claims = ["role", "person_row_id", "exp", "iat", "aud", "iss"]
+const userRoles = ["candymat_editor", 'candymat_candidate', 'candymat_person']
+
+interface JwtPayload {
+ "role": UserRole,
+ "person_row_id": number,
+ "exp": number,
+ "iat": number,
+ "aud": "postgraphile",
+ "iss": "postgraphile"
+}
+
+type UserRole = "candymat_editor" | 'candymat_candidate' | 'candymat_person'
+