Commit 1a4c79a1 authored by Micha Lutz's avatar Micha Lutz

Initial commit

parents
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
package-lock.json
config.js
\ No newline at end of file
# Subscriber App
Diese Web-Anwendung von fortiss GmbH ist ein Referenz-Client für die Application Subscriber API des FIT-Connect PoC. Sie wurde als Minimal Viable Product (MVP) entwickelt, um die Grundidee des FIT-Connect PoC aus Sicht eines Behördenportals zu demonstrieren.
Die Subscriber App wird durch die Sender App ergänzt, welche den FIT-Connect PoC aus Sicht eines BürgerInnen-Portals demonstriert:
https://git.fortiss.org/fit-connect/sender-app
_*** ENGLISH VERSION ***_
_This web application by fortiss GmbH is a reference client for the Application Subscriber API of the FIT-Connect PoC. It has been developed as a minimal viable product (MVP) to showcase the main idea of the FIT-Connect PoC from the point of view of a government agency portal._
_The Subscriber App is complemented by the Sender App, which demonstrates the FIT-Connect PoC from the point of view of a citizen portal:_
_https://git.fortiss.org/fit-connect/sender-app_
## FIT-Connect PoC
FIT-Connect wird als PoC von [FITKO](https://www.fitko.de/) bereitgestellt. Das Ziel von FIT-Connect ist eine einfache, schnelle Integration von Anwendungen zu OZG-Verfahren in medienbruchfreie Antragsprozesse.
Weitere Informationen sind in der FIT-Connect Dokumentation erhältlich: https://docs.fiep-poc.de/docs
## Funktionsweise der Subscriber App
Die Web-Anwendung basiert auf dem Node JS React Framework. Beim Aufrufen der Web-Anwendung wird die Komponente src/App.js geladen, welche initial folgende Schritte durchführt:
1. Access Token vom Authentication Provider abrufen (Zugangsdaten in src/config.js)
2. Eine Destination (Zustellpunkt einer Behörde) von Subscriber API abrufen. Falls keine existiert, eine neue Destination anlegen
3. Offene Anträge von Subscriber API abrufen
Die Grunddaten der offenen Anträge werden anschließend in der Web-Anwendung angezeigt. Es existiert ein Button, um die Anzeige zu aktualisieren.
Für jedenen offenen Antrag sind folgende Aktionen in der Web-Anwendung möglich:
1. Antrags-Daten von Subscriber API herunterladen
2. Antrags-Dokumente von Subscriber API herunterladen
3. Antrag abschließen
## Installation
Befolgen Sie die folgenden Schritte, um die Web-Anwendung lokal zu installieren
### 1. Node JS und NPM installieren
Falls Node JS und NPM noch nicht auf Ihrem Rechner installiert sind, laden Sie die für Ihr System passende Installationsdatei hier herunter und führen diese aus: https://nodejs.org/en/download/
Im Anschluss sollten folgende Befehle in der Eingabeaufforderung die entsprechenden Versionsnummern fehlerfrei anzeigen:
```
node --version
npm --version
```
Diese Web-Anwendung wurde unter der Node JS Version 12.16.1 entwickelt.
### 2. Quellcode herunterladen
Laden Sie den Quellcode aus diesem Git Repository in ein neues Verzeichnis auf Ihren Rechner. Sie können hierzu in Gitlab den Download-Button nutzen, um den Quellcode als ZIP-Datei herunterzuladen und diese anschließend zu entpacken.
### 3. FIT-Connect Zugangsdaten
Kopieren Sie in dem Verzeichnis, in dem sich nun der Quellcode der Web-Anwendung befindet, die Datei `src/config.js.template` in die Datei `src/config.js` und fügen Sie dort Application Id, Client Id, Client Secret und Scope aus dem FIT-Connect-Portal ein.
### 4. Anwendung installieren
Öffnen Sie die Eingabeaufforderung und wechseln sie in das Verzeichnis, in dem sich nun der Quellcode der Web-Anwendung befindet. Führen Sie anschließend folgenden Befehl aus. Es werden nun alle für diese Anwendung benötigten Node JS Pakete in das Unterverzeichnis node_modules heruntergeladen.
```
npm install
```
### 5. Anwendung im Entwicklermodus starten
Führen Sie abschließend folgenden Befehl aus. Die Web-Anwendung öffnet sich in einem neuen Browser fenster. Sie können die Anwendung beenden, indem sie in der Eingabeaufforderung Strg+C und J eingeben oder die Eingabeaufforderung schließen.
```
npm start
```
## Kontakt
Diese Anwendung wurde von Micha Lutz bei fortiss GmbH entwickelt. Kontakt: Peter Kuhn, pkuhn@fortiss.org
## Lizenz
This project is licensed under the terms of the MIT license.
Copyright (c) 2020 fortiss GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "subscriber-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.10.2",
"@material-ui/icons": "^4.9.1",
"axios": "^0.19.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<title>Subscriber App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
import React, { useState, useEffect } from "react";
import {
AppBar,
Button,
Card,
CardContent,
CardActions,
CircularProgress,
Container,
Dialog,
DialogTitle,
Grid,
IconButton,
Menu,
MenuItem,
Toolbar,
Typography,
} from "@material-ui/core";
import MenuIcon from "@material-ui/icons/Menu";
import {
getAccessToken,
getDestinations,
createNewDestination,
getApplications,
getApplicationData,
acknowledgeApplication,
} from "./requests";
import { useStyles } from "./styles";
const App = () => {
// load styles
const classes = useStyles();
// state variables and hook functions
const [status, setStatus] = useState("init");
const [accessToken, setAccessToken] = useState("");
const [destination, setDestination] = useState({ organizationName: "", destinationId: "" });
const [applications, setApplications] = useState([]);
const [dialogContent, setDialogContent] = useState({ open: false, title: "", content: "" });
const [menuAnchorEl, setMenuAnchorEl] = useState(null);
// execute this callback once page has rendered initially
useEffect(() => {
// get the access token
setStatus("getAccessToken");
getAccessToken().then((newAccessToken) => {
if (newAccessToken === "error") {
setStatus("error");
return;
}
// update access token state var
setAccessToken(newAccessToken);
});
}, []);
// execute this callback once accessToken is set
useEffect(() => {
if (accessToken) {
// get a destination
setStatus("getDestinations");
getDestinations(accessToken).then((newDestination) => {
if (newDestination === "error") {
setStatus("error");
return;
}
if (newDestination) {
// if a destination was found, udpate access token state var with this destination
setDestination(newDestination);
} else {
// else, create new destination and update access token state var with new destination
createNewDestination(accessToken).then((newDestination) => {
setDestination(newDestination);
});
}
setStatus("ready");
});
}
}, [accessToken]);
// execute this callback once destination is set
useEffect(() => {
if (destination.destinationId) {
handleGetApplications();
}
}, [destination]);
// handle function to request all applications from SubscriberAPI and save them in state variable
const handleGetApplications = async () => {
setStatus("getApplications");
const newApplications = await getApplications(destination.destinationId, accessToken);
if (newApplications === "error") {
setStatus("error");
return;
}
setStatus("ready");
setApplications(newApplications);
};
// handle function to request data of an application and display it in a modal
const handleGetApplicationData = async (applicationId) => {
setDialogContent({ open: true, title: "Application Data", content: <CircularProgress /> });
const applicationData = await getApplicationData(applicationId, destination.destinationId, accessToken);
if (applicationData === "error") {
setDialogContent({ open: false, title: "", content: "" });
setStatus("error");
return;
}
setDialogContent({
open: true,
title: "Application Data",
content: <span>{JSON.stringify(applicationData)}</span>,
});
};
// handle function to acknowledge an application
const handleAcknowledgeApplication = async (applicationId) => {
setStatus("acknowledgeApplication");
const ackAppRes = await acknowledgeApplication(applicationId, destination.destinationId, accessToken);
if (ackAppRes === "error") {
setStatus("error");
return;
}
handleGetApplications();
};
// component function to return an application as a card
const ApplicationCard = ({ application }) => {
const applicants = application.applicants
.map((applicant) => applicant.identityInfo.givenName + " " + applicant.identityInfo.familyName)
.join("; ");
// TODO: allow displaying of all docs, not just the first one
return (
<Card className={classes.card}>
<CardContent align="left">
Service: {application.publicServiceType.name}
<br />
LeikaId: {application.publicServiceType.leikaId}
<br />
Applicants: {applicants}
</CardContent>
<CardActions>
<Button size="small" onClick={() => handleGetApplicationData(application.applicationId)}>
Get Data
</Button>
<Button size="small" onClick={() => handleAcknowledgeApplication(application.applicationId)}>
Acknowledge
</Button>
</CardActions>
</Card>
);
};
// finally return our JSX
return (
<div className={classes.root}>
<AppBar position="static" color="primary">
<Toolbar variant="dense">
<IconButton
edge="start"
onClick={(event) => setMenuAnchorEl(event.currentTarget.parentNode)}
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<MenuIcon />
</IconButton>
<Menu
id="simple-menu"
anchorEl={menuAnchorEl}
getContentAnchorEl={null}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
keepMounted
open={Boolean(menuAnchorEl)}
onClose={() => setMenuAnchorEl(null)}
className={classes.menu}
>
<MenuItem onClick={() => (window.location.href = "/sender-app")}>Sender App</MenuItem>
<MenuItem onClick={() => (window.location.href = "/subscriber-app")}>Subscriber App</MenuItem>
</Menu>
<Typography variant="h6" className={classes.title}>
Subscriber App
</Typography>
</Toolbar>
</AppBar>
<Container maxWidth="md">
<Grid container className={classes.formRow}>
{destination.destinationId && (
<header className={classes.formInput}>
<h1>{destination.organizationName}</h1>
<Grid container alignItems="center">
<Grid item xs={3}>
<Typography className={classes.destinationLabel}>My Destination ID:</Typography>
</Grid>
<Grid item xs={9}>
<Typography className={classes.destinationLabel}>{destination.destinationId}</Typography>
</Grid>
</Grid>
</header>
)}
</Grid>
{status !== "ready" && (
<Grid container align="center">
<Grid item xs={12}>
<CircularProgress />
</Grid>
<Grid item xs={12}>
{status === "init" && <span>loading page</span>}
{status === "getAccessToken" && <span>requesting access token</span>}
{status === "getDestinations" && <span>requesting destination</span>}
{status === "createNewDestination" && <span>no destination found, creating new destination</span>}
{status === "getApplications" && <span>requesting applications</span>}
{status === "error" && <span>an error occured, see console for details</span>}
</Grid>
</Grid>
)}
<Grid container align="center" justify="flex-start" alignItems="flex-start">
{status === "ready" && (
<Grid item xs={12}>
<Button onClick={handleGetApplications} className={classes.formRow}>
Update Applications
</Button>
</Grid>
)}
{status === "ready" &&
applications.length > 0 &&
applications.map((application) => (
<ApplicationCard application={application} key={application.applicationId} />
))}
</Grid>
<Dialog open={dialogContent.open} onClose={() => setDialogContent({ open: false, title: "", content: "" })}>
<DialogTitle>{dialogContent.title}</DialogTitle>
<div className={classes.dialogContent}>{dialogContent.content}</div>
</Dialog>
</Container>
</div>
);
};
export default App;
// PLEASE SET YOUR CREDENTIALS FROM FIT-CONNECT PORTAL HERE
export default {
subscriberId: "",
clientId: "",
clientSecret: "",
scope: "",
};
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
import axios from "axios";
import config from "./config";
// function to initially request an access token from SubscriberAPI and save it in state variable
export const getAccessToken = async () => {
try {
const response = await axios.get(
"https://oauth.fiep-poc.de/invoke/pub.apigateway.oauth2/getAccessToken?grant_type=client_credentials&client_id=" +
config.clientId +
"&client_secret=" +
config.clientSecret +
"&scope=" +
config.scope
);
return response.data.access_token;
} catch (error) {
console.log(error);
return "error";
}
};
// function to initially request destinations from SubscriberAPI and save the first one in state variable
export const getDestinations = async (accessToken) => {
try {
const response = await axios.get("https://subscriber.fiep-poc.de/beta5/" + config.subscriberId + "/destinations", {
headers: { Authorization: "Bearer " + accessToken },
});
if (response.data.length > 0) {
return {
organizationName: response.data[0].publicOrganization.organizationName,
destinationId: response.data[0].destinationId,
};
} else {
return null;
}
} catch (error) {
console.log(error);
return "error";
}
};
// function to initially create a new destination at SubscriberAPI and save it in state variable
export const createNewDestination = async (accessToken) => {
try {
const requestBody = {
publicOrganization: {
organizationName: "Beispielbehörde",
},
};
const response = await axios.post(
"https://subscriber.fiep-poc.de/beta5/" + config.subscriberId + "/destinations",
requestBody,
{
headers: { Authorization: "Bearer " + accessToken },
}
);
return [requestBody.publicOrganization.organizationName, response.data.destinationId];
} catch (error) {
console.log(error);
return "error";
}
};
// handle function to request all applications from SubscriberAPI and save them in state variable
export const getApplications = async (destinationId, accessToken) => {
try {
const response = await axios.get(
"https://subscriber.fiep-poc.de/beta5/" +
config.subscriberId +
"/destinations/" +
destinationId +
"/applications",
{
headers: { Authorization: "Bearer " + accessToken },
}
);
return response.data;
} catch (error) {
console.log(error);
return "error";
}
};
// handle function to request data of an application and display it in a modal
export const getApplicationData = async (applicationId, destinationId, accessToken) => {
try {
const response = await axios.get(
"https://subscriber.fiep-poc.de/beta5/" +
config.subscriberId +
"/destinations/" +
destinationId +
"/applications/" +
applicationId +
"/data",
{
headers: { Authorization: "Bearer " + accessToken },
}
);
return response.data;
} catch (error) {
console.log(error);
return "error";
}
};
// handle function to acknowledge an application
export const acknowledgeApplication = async (applicationId, destinationId, accessToken) => {
try {
const requestBody = {
finalDelivery: true,
};
await axios.post(
"https://subscriber.fiep-poc.de/beta5/" +
config.subscriberId +
"/destinations/" +
destinationId +
"/applications/" +
applicationId,
requestBody,
{
headers: { Authorization: "Bearer " + accessToken },
}
);
return "ok";
} catch (error) {
console.log(error);
return "error";
}
};
import { makeStyles } from "@material-ui/core/styles";
// define styles
export const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
},
menu: {
marginLeft: -8,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
formRow: {
width: "100%",
marginBottom: 20,
},
formInput: {
width: "100%",
marginBottom: 10,
},
destinationLabel: {
color: "gray",
fontSize: "0.8em",
},
card: {
margin: 10,
},
dialogContent: {
margin: 10,
},
}));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment