Zunächst einmal musste eine erste Struktur für die Webseite gebaut werden. Für das Frontend wird React mit Typescript genutzt. Das Backend ist mit Python gebaut. Für diese Rest(-like) API wird Flask in Verbindung mit einer MariaDB Datenbank genutzt. Wie genau das alles aufgebaut ist, erfährst du im Folgenden.
Frontend
Nach dem erstellen einer neuen React-App, musste zunächst ein Desing-Framework ausgewählt werden, welches die ganzen Komponenten bereitstellt. Meine Wahl ist in diesem Fall auf Grommet gefallen. Für Grommet gibt es glücklicherweise einen Online-Designer, wo sich globale Designeinstellungen festlegen lassen. So z.B. die Hauptfarbe oder das Aussehen von Buttons. Diese Theme ließ sich dann letztendlich exportieren und in der Anwendung hinterlegen.
Als nächste galt es die Navigation einzurichten. Dazu wird die „React Router“ Bibliothek genutzt. Diese ermöglicht sogenanntes „client side routing„. Das bedeutet, dass bei einer Navigation nicht die komplette Webseite ausgetauscht wird, sondern nur die Teile, die sich auch ändern. Mit dem Router lassen sich zudem ganz einfach verschieden Routen einrichten. Eine Route legt in diesem Fall fest, für welche URL welche Seite angezeigt werden soll.
const Root = () => {
return (
<Routes>
<Route path="/" element={<Home />}/>
<Route path="/login" element={<Login/>}/>
<Route path="/account" element={<LoggedIn><Account/></LoggedIn>}/>
<Route path="/members" element={<LoggedIn><Members/></LoggedIn>}/>
<Route path="/admin" element={
<ConditionalRendering roles={["admin"]}>
<Admin/>
</ConditionalRendering>}/>
<Route path="*" element={<NotFound/>}/>
</Routes>
);
}
Da das erste Ziel war eine Struktur für die Anwendung aufzubauen, mit der sich diese später leicht erweitern lässt, war der nächste Schritt die Anmeldung von Nutzern. Wie im Codeblock erkennbar ist, soll diese letztendlich dann Komponenten bereitstellen, die es möglich machen, gewisse Elemente nur anzuzeigen, wenn der Nutzer angemeldet ist oder bestimmte Rollen besitzt.
Die Anmeldung geschieht im Normalfall mit Passwort und Nutzername. Gelingt diese initiale Anmeldung, liefert die API einen SessionKey zurück. Dieser kann von nun an zur automatischen Anmeldung genutzt werden. Zudem bekommt das Frontend von der API die Nutzerinformationen. Diese Informationen werden in einem State gespeichert, welcher durch einen Context in der ganzen Anwendung verfügbar ist. Ein Context funktioniert also ähnlich wie eine globale Variable. Solange der Nutzer nicht auf den „Neu-Laden“ Knopf drück, bleibt dessen Inhalt erhalten.
export const UserContext = createContext(userContextDefaultValue);
function App() {
const [user, setUser] = useState(userContextDefaultValue.state);
return (
<>
<BrowserRouter>
<UserContext.Provider value={{state: user, setState: setUser}}>
<Grommet theme={themes.theme_evev}>
<LoginService/>
<SiteHeader/>
<Root />
</Grommet>
</UserContext.Provider>
</BrowserRouter>
</>
)
}
Die zurückgegebenen Nutzerdaten stellen auch den SessionKey bereit. Dieser wird neben der Anmeldung auch benötigt, um beschränkte Daten von der API abzufragen. Heißt, ohne SessionKey keine Daten. Um Abfragen gegen die API zu erleichtern, wird ein selbst geschriebener Hook verwendet, der die wiederverwendbare Abfragelogik kapselt. Dieser Hook kümmert sich auch darum, dass dem Request-Header, der mit an die API geschickt wird, immer der SessionKey zur Authentifizierung angehangen wird.
Entsprechend dem Usermanagement wird auch die Navigationsleiste gerendert. Das bedeutet, Elemente dieser Leiste besitzen zwei optionale Eigenschaften. Zum einen, ob sie nur angezeigt werden sollen, wenn der Nutzer angemeldet ist, und, zum anderen, eine Liste an benötigten Nutzerrollen. Besitzt der Nutzer also keine der erforderlichen Rollen, wird ihm der Menüpunkt auch nicht angezeigt.
const items: navbarItem[] = [
{label: "Profil", link: "/account", loggedIn: true},
{label: "Personen", link: "/members", loggedIn: true},
{label: "Admin", link: "/admin", loggedIn: true, roles: ["admin"]},
];
Hier wird aber nicht festgelegt, ob ein Nutzer diese Seite auch tatsächlich besuchen darf. Dies geschieht, wie im ersten Codeblock ersichtlich, bei der Definition der Routen. Ohne die Überprüfung der Routen könnte ich einfach die URL aufrufen, vorausgesetzt ich kennen sie, und alle Daten werden dargestellt.
Betrachten wird all die soeben erklärten, sowie die nicht erwähnten, kosmetischen Features, steht erstmal ein solides Fundament, auf dem aufgebaut werden kann.
Backend
Ergänzend zum Frontend muss es natürlich auch ein Backend geben. Damit dieses leicht zu handhaben und zu warten ist, ist es in Python geschrieben. Dafür wird die leichtgewichtige Flask-Bibliothek genutzt. Sie stellt den Rahmen für die API bereit. So lassen sich im Handumdrehen alle API Endpunkt ohne großen Aufwand einrichten.
@app.route('/login/password', methods=['POST'])
def post_login():
if request.method == 'POST':
return user_service.post_login_password(
request.headers, request.json)
Hier sichtbar, der Endpoint worüber die Anmeldung abgewickelt wird. Die route
-Methode wird praktischerweise von Flask bereitgestellt und ermöglicht das einfache Management verschiedener API Routen. Einmal angesprochen, wir die Aufforderung der Anmeldung an den user_service
weitergeben.
Der user_service
beinhaltet die komplette Interaktion mit der Datenbank. Zunächst wird dazu ein sogenannter Connection-Pool erstellt und dem user_service
bekannt gemacht. Dieser unterbindet einen ganz schwerwiegenden Fehler. Gibt es nur eine Connection zur Datenbank, kann es sein, wenn diese Verbindung gerade genutzt wird und eine zweite Operation angefordert wird, dass das Backend abstürzt. Der Connection-Pool enthält eine Sammlung von mehreren Verbindungen. Wird nun eine Anfrage gestellt, wird eine Verbindung ausgeliehen, genutzt und dann letztendlich wieder zurückgegeben. So kommt es zu keinen Problemen bei mehreren simultanen Anfragen.
def get_user_info_username(self, username):
conn = self.pool.get_connection()
cur = conn.cursor()
cur.execute(
"""
SELECT U.username, U.name, GROUP_CONCAT(R.role_name)
FROM users U
LEFT JOIN userroles R ON U.id = R.user_id
WHERE U.username = %s
GROUP BY U.id
""", (username,))
r = cur.fetchone()
conn.close()
...
Ein weiterer wichtiger Punkt des Backend ist, dass die Daten geschützt werden. Das Frontend verhindert wohl, dass der Nutzer Seiten aufrufen kann, wo beschränkt Daten angezeigt werden – doch könnte man die Daten direkt über den API Link abfragen.
Um das zu verhindern, kommt der Python-Syntax für sogenannte Wrapper-Methoden zum Einsatz. Geschrieben mit einem @ vor der Methodendeklaration, kann die Logik der Wrapper-Methode der Ausführung der eigentlichen Methode vorgeschaltet werden.
@app.route('/users')
@authorizeAccess(["admin"])
def get_all_users():
return user_service.get_all_users()
An einem Beispiel lässt sich dies einfacher erläutern. Es geht hier um die Abfrage aller Nutzerdaten. Dies soll nur ein Nutzer mit Adminrechten machen können. So wird die Wrapper-Methode authorizeAccess
vorgeschaltet. Diese schaut, ob der Nutzer die Rolle „admin“ besitzt. Ist dies der Fall, wir die Methode get_all_users
regulär ausgeführt. Besitzt der Nutzer die Rolle nicht, wird anstelle des Resultat der Methode get_all_users
der Fehler 403 zurückgegeben – „Forbidden“.
Somit besitzt auch das Backend erstmal die grundlegenden Funktionalitäten, um darauf aufzubauen.
Nun noch ein paar persönliche Worte zum Abschluss: Die Grundlagen zu setzen ging im ersten Schritt doch schneller als erwartet. Das hat aber vor allem den Grund, dass ich zur Zeit, wo ich diese Zeilen geschrieben habe, auf Heimaturlaub war. Dort hatte ich die Zeit, mich auf dieses Projekt zu konzentrieren. Zurück in Stuttgart wartet nämlich erstmal das „Ticketsystem“ auf mich, welches bis zum Sommer auf einem funktionsfähigen Stand sein muss. Von daher wird sich zeigen, wann ich weiter am Projekt „Mitgliederportal“ arbeiten kann.
~ Tino Schaare