33 Commits

Author SHA1 Message Date
93a788d292 Update Diplomarbeitsbuch-individueller-teil-Konstantin-Hintermayer.md
All checks were successful
Gemini Writing Review / gemini-review (pull_request) Successful in 4m24s
Word Count / count-words (pull_request) Successful in 32s
2026-03-07 14:15:43 +00:00
23c27600ef Diplomarbeitsbuch-individueller-teil-Adam-Gaiswinkler.md aktualisiert
Some checks failed
Word Count / count-words (push) Failing after 31s
2026-03-05 20:55:14 +00:00
e86b1c6b76 Diplomarbeitsbuch-individueller-teil-Adam-Gaiswinkler.md aktualisiert
Some checks failed
Word Count / count-words (push) Failing after 30s
2026-03-05 20:50:35 +00:00
b6886ce52a Diplomarbeitsbuch-individueller-teil-Adam-Gaiswinkler.md aktualisiert
Some checks failed
Word Count / count-words (push) Failing after 32s
2026-03-05 20:49:53 +00:00
bbf3626dcb Diplomarbeitsbuch-individueller-teil-Adam-Gaiswinkler.md aktualisiert
Some checks failed
Word Count / count-words (push) Failing after 33s
2026-03-05 16:22:52 +00:00
a1ecaaadb4 Punkt 6 geschrieben noch nicht fertig muss noch erweitert werden
Some checks failed
Word Count / count-words (push) Failing after 32s
2026-03-05 16:48:58 +01:00
63d3436f0e Merge branch 'main' of https://git.kocoder.xyz/Diplomarbeit-Absolventenverein/pm
Some checks failed
Word Count / count-words (push) Failing after 32s
2026-03-05 13:47:54 +01:00
756980c5ae Punkt 5 mit allen unter Punkten fertig geschrieben 2026-03-05 13:47:46 +01:00
416babf6ac Merge pull request 'CI/CD: grundlegende Einführung in den verwendungszweck von Gitea Pipelines' (#63) from Einleitung-CI/C into main
Some checks failed
Word Count / count-words (push) Has been cancelled
Reviewed-on: #63
2026-03-05 12:42:04 +00:00
104cfca699 Merge pull request 'Einleitung: Individueller Teil: Konstantin Hintermayer' (#64) from Einleitung-individueller-Teil-Konstantin-Hintermayer into main
Some checks failed
Word Count / count-words (push) Has been cancelled
Reviewed-on: #64
2026-03-05 12:41:47 +00:00
fea6d6bdda Apply Gemini-Dokumentation-Review-Suggestions
All checks were successful
Word Count / count-words (pull_request) Successful in 32s
2026-03-04 10:51:20 +00:00
395000aa47 Apply Gemini-Dokumentation-Review-Suggestions
All checks were successful
Word Count / count-words (pull_request) Successful in 33s
2026-03-04 10:42:19 +00:00
adb32e1234 CI/CD: grundlegende Einführung in den verwendungszweck von Gitea Pipelines
All checks were successful
Gemini Writing Review / gemini-review (pull_request) Successful in 1m20s
Word Count / count-words (pull_request) Successful in 33s
2026-03-04 11:33:28 +01:00
c51a6f09b4 Einleitung: Individueller Teil: Konstantin Hintermayer
All checks were successful
Gemini Writing Review / gemini-review (pull_request) Successful in 1m22s
Word Count / count-words (pull_request) Successful in 32s
2026-03-04 11:14:00 +01:00
a6aad1478f Fix: Typos
Some checks failed
Word Count / count-words (push) Failing after 31s
2026-03-02 11:33:10 +00:00
2d9b380935 Apply proposed Changes 2026-03-02 11:33:10 +00:00
33cce846ac CI: Issue Change Requests 3 2026-03-02 11:33:10 +00:00
2a886bcd8d CI: Issue Change Requests 2 2026-03-02 11:33:10 +00:00
df3e4ef53c CI: Issue Change Requests 2026-03-02 11:33:10 +00:00
bd5bb446b3 Fix: WC Output 2026-03-02 11:33:10 +00:00
1f38008509 Fix: WC 2026-03-02 11:33:10 +00:00
d6357adeed CI: Setup golang 2026-03-02 11:33:10 +00:00
71854cd101 New: save wordcounts in PR Comment 2026-03-02 11:33:10 +00:00
152ddf5bcc CI: Alter Prompt 2026-03-02 11:33:10 +00:00
fc002cdcfc CI: Use gemini 3 flash preview 2026-03-02 11:33:10 +00:00
a24cb4cd76 CI: Use gemini 3 flash 2026-03-02 11:33:10 +00:00
975b7bb319 Fix 2026-03-02 11:33:10 +00:00
42e26b896d Fix: pipeline and go building 2026-03-02 11:33:10 +00:00
f06c193b71 New: fix paths 2026-03-02 11:33:10 +00:00
723031782b NEW: Oqtane Allgemein 2026-03-02 11:33:10 +00:00
85a670eb04 New: CI 2026-03-02 11:33:10 +00:00
c9d9221a19 Merge branch 'main' of https://git.kocoder.xyz/Diplomarbeit-Absolventenverein/pm 2026-02-28 09:51:36 +01:00
011542a32d 5.1 und 5.2 geschrieben 2026-02-28 09:51:27 +01:00
10 changed files with 718 additions and 75 deletions

View File

@@ -26,4 +26,7 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
run: | run: |
go run scripts/gemini_review/main.go cd scripts/gemini_review
go build -o gemini_review_bin main.go
cd ../..
./scripts/gemini_review/gemini_review_bin

View File

@@ -13,7 +13,19 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Count words in Markdown files - name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run Gemini Review
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: | run: |
echo "Counting words in all .md files..." cd scripts/wordcount
find . -name "*.md" -not -path "*/.*" -exec wc -w {} + | sort -rn go build -o wordcount_bin main.go
cd ../..
./scripts/wordcount/wordcount_bin

View File

@@ -1,62 +1,158 @@
# Diplomarbeitsbuch # Diplomarbeitsbuch
--- ---
## 1. Einleitung ## 1. Einleitung
### Ausgangssituation ### Ausgangssituation
### Motivation ### Motivation
### Kurzbeschreibung des Projekts ### Kurzbeschreibung des Projekts
### Persönlicher Aufgabenbereich ### Persönlicher Aufgabenbereich
### Abgrenzung der Arbeit ### Abgrenzung der Arbeit
Die Datenbankinfrastruktur war zum Projektstart bereits vorhanden und wurde nicht im Rahmen dieser Arbeit konzipiert. Die Diplomarbeit beschränkt sich auf die Entwicklung der Anwendungsschicht, bestehend aus CMS-Konfiguration, Modul- und Theme-Entwicklung. Die Datenbankinfrastruktur war zum Projektstart bereits vorhanden und wurde nicht im Rahmen dieser Arbeit konzipiert. Die Diplomarbeit beschränkt sich auf die Entwicklung der Anwendungsschicht, bestehend aus CMS-Konfiguration, Modul- und Theme-Entwicklung.
### Individuelle Themenstellung ### Individuelle Themenstellung
--- ---
## 3. Projektumfeld & Rahmenbedingungen ## 3. Projektumfeld & Rahmenbedingungen
### Einsatz eines bestehenden CMS (Oqtane) ### Einsatz eines bestehenden CMS (Oqtane)
### Teamarbeit ### Teamarbeit
Das Projektteam wurde im Verlauf des Projekts von sechs auf drei Personen reduziert. Das Projektteam wurde im Verlauf des Projekts von sechs auf drei Personen reduziert.
### Schulisches Umfeld (HTL) ### Schulisches Umfeld (HTL)
### Zeitliche Rahmenbedingungen ### Zeitliche Rahmenbedingungen
--- ---
## 4. Technologischer Überblick ## 4. Technologischer Überblick
### 4.1 Web-Entwicklung mit ASP.NET & C# ### 4.1 Web-Entwicklung mit ASP.NET & C#
#### Backend-Logik #### Backend-Logik
#### API-basierte Kommunikation #### API-basierte Kommunikation
#### Zusammenspiel mit Oqtane #### Zusammenspiel mit Oqtane
### 4.2 Oqtane Überblick ### 4.2 Oqtane Überblick
#### Grundidee & Architektur #### Grundidee & Architektur
#### Modul- & Theme-Konzept #### Modul- & Theme-Konzept
#### Vorteile für das Projekt #### Vorteile für das Projekt
### 4.3 CMS-Grundkonfiguration ### 4.3 CMS-Grundkonfiguration
#### Initiale Einrichtung #### Initiale Einrichtung
#### Modul-Integration #### Modul-Integration
#### Rollen & Berechtigungen #### Rollen & Berechtigungen
#### Grundlegende Systemeinstellungen #### Grundlegende Systemeinstellungen
--- ---
## 5. System- und Lösungsarchitektur ## 5. System- und Lösungsarchitektur
### Gesamtarchitektur des Systems ### Gesamtarchitektur des Systems
### Einordnung von CMS, Modulen und Theme ### Einordnung von CMS, Modulen und Theme
### Datenfluss ### Datenfluss
### Architekturentscheidungen ### Architekturentscheidungen
--- ---
## 6. Entwicklung des Oqtane Themes ## 6. Entwicklung des Oqtane Themes
### Ziel des Themes
### Technische Umsetzung ### 6.1 Ziel des Themes
### Herausforderungen
Im Rahmen des Projekts AlumniHub wurde ein eigenes Theme für das Content-Management-System Oqtane entwickelt. Ziel dieser Entwicklung war es, das Standarddesign von Oqtane vollständig durch eine projektspezifische Benutzeroberfläche zu ersetzen, die den Anforderungen des Absolventenvereins entspricht. Dabei standen Übersichtlichkeit, Benutzerfreundlichkeit sowie eine moderne und konsistente Gestaltung im Vordergrund. Das Theme sollte zudem auf unterschiedlichen Endgeräten sowohl Desktop als auch mobil optimal dargestellt werden.
### 6.2 Technische Umsetzung
Als technische Grundlage diente die Theme-Architektur von Oqtane. Das Layout wurde in einer Razor-Datei (`Theme.razor`) definiert, welche von der Basisklasse `ThemeBase` erbt. Dadurch stehen zentrale Funktionen wie Seitenzustand (`PageState`), Navigation und Systemeinstellungen automatisch zur Verfügung.
#### Navigationsleiste
Im oberen Bereich wurde eine fixierte Navigationsleiste implementiert, die drei Hauptbereiche umfasst: links das Logo der Schule, in der Mitte die dynamisch generierten Navigationspunkte und rechts die Benutzerfunktionen (Login, Registrierung, Profil). Da die Standardkomponente `<Menu>` von Oqtane auch interne Systemseiten wie Login, Register oder NotFound ausgibt, wurde auf diese verzichtet. Stattdessen wurden eigene Komponenten implementiert, die von `MenuBase` bzw. `MenuItemsBase` erben. Die Seitenliste wird dabei über `PageState.Pages` abgerufen und mittels LINQ gefiltert es werden nur Root-Seiten (`ParentId == null`) angezeigt, die als Navigationsseite markiert sind und nicht in einer definierten Ausschlussliste (`hiddenNames`) stehen.
```csharp
var hiddenNames = new[]
{
"Login", "Register", "Reset", "Profile",
"Search", "Privacy", "Terms", "Not Found", "Admin"
};
foreach (var item in PageState.Pages
.Where(p => p.ParentId == null
&& !hiddenNames.Contains(p.Name)))
{
<a class="nav-link text-white" href="@item.Path">
@item.Name
</a>
}
```
#### Responsive Design / Burger-Menü
Für mobile Endgeräte wurde ein Burger-Menü implementiert. Das Öffnen und Schließen der Sidebar erfolgt über eine CSS-Checkbox-Lösung ohne zusätzliche Frameworks. Die Sidebar zeigt alle Navigationspunkte vertikal untereinander an, wobei Login- und Registrierungsfunktionen am unteren Rand fixiert sind und unabhängig von der Anzahl der Menüpunkte stets sichtbar bleiben.
#### Pane-Struktur
Für den Inhaltsbereich wurden mehrere Panes definiert (z.B. `Top 100%`, `Left 50%`, `Right Sidebar 33%`, `Bottom 100%`), die als flexible Platzhalter für Module dienen. Dadurch können Seiteninhalte ohne Änderungen am Theme-Code strukturiert werden.
#### Weitere Integrationen
Zusätzlich wurden die Cookie-Consent-Komponente sowie das ControlPanel für Administratoren integriert. Login- und Registrierungsoptionen werden über den `SettingService` aus den Site-Einstellungen geladen, sodass diese ohne Code-Anpassung gesteuert werden können.
### 6.3 Herausforderungen
Eine der zentralen Herausforderungen bei der Entwicklung des Themes war die korrekte Filterung der Navigationsseiten. Die von Oqtane bereitgestellten Blazor-Standardkomponenten für die Navigation verhielten sich dabei nicht wie erwartet, da sie auch interne Systemseiten wie Login, Register oder NotFound in der Hauptnavigation ausgaben und keine ausreichende Möglichkeit boten, diese gezielt auszublenden. Aus diesem Grund wurde auf die Standardkomponenten verzichtet und stattdessen eine eigene Implementierung entwickelt. Durch den direkten Zugriff auf `PageState.Pages` sowie eine LINQ-basierte Filterlogik konnten die anzuzeigenden Seiten vollständig kontrolliert und Systemseiten zuverlässig ausgeblendet werden.
--- ---
## 7. Umsetzung der Module ## 7. Umsetzung der Module
### 7.1 Anmeldetool ### 7.1 Anmeldetool
#### Ziel des Moduls #### Ziel des Moduls
#### Frontend (Eingabemaske) #### Frontend (Eingabemaske)
#### Backend-Logik #### Backend-Logik
#### API-Schnittstelle #### API-Schnittstelle
#### Datenauswertung #### Datenauswertung
#### UX-Überlegungen #### UX-Überlegungen
---
### 7.2 Hall of Fame ### 7.2 Hall of Fame
Das Hall-of-Fame-Modul ist ein Oqtane-Modul, das ehemalige Absolventinnen und Absolventen der Schule auf der Vereinswebseite präsentiert. Es wurde als eigenständiges, wiederverwendbares Modul innerhalb des Oqtane-Frameworks entwickelt und ermöglicht registrierten Benutzerinnen und Benutzern, sich selbst mit einem persönlichen Profil einzutragen. Die Einträge werden erst nach bewusster Veröffentlichung durch die jeweilige Person auf der öffentlichen Seite angezeigt, können als PDF exportiert werden und unterliegen einem Meldesystem zur Qualitätssicherung. Das Hall-of-Fame-Modul ist ein Oqtane-Modul, das ehemalige Absolventinnen und Absolventen der Schule auf der Vereinswebseite präsentiert. Es wurde als eigenständiges, wiederverwendbares Modul innerhalb des Oqtane-Frameworks entwickelt und ermöglicht registrierten Benutzerinnen und Benutzern, sich selbst mit einem persönlichen Profil einzutragen. Die Einträge werden erst nach bewusster Veröffentlichung durch die jeweilige Person auf der öffentlichen Seite angezeigt, können als PDF exportiert werden und unterliegen einem Meldesystem zur Qualitätssicherung.
#### Datenmodell #### Datenmodell
Das Modul verwendet zwei Entitäten, die in der Datenbank als Tabellen abgebildet werden. Das Modul verwendet zwei Entitäten, die in der Datenbank als Tabellen abgebildet werden.
**Entität HallOfFame** **Entität HallOfFame**
Die zentrale Entität repräsentiert einen einzelnen Absolventeneintrag und wird in der Datenbanktabelle `SZUAbsolventenvereinHallOfFame` gespeichert. Die zentrale Entität repräsentiert einen einzelnen Absolventeneintrag und wird in der Datenbanktabelle `SZUAbsolventenvereinHallOfFame` gespeichert.
| Spalte | Datentyp | Beschreibung | | Spalte | Datentyp | Beschreibung |
|--------|----------|--------------| |--------|----------|--------------|
| `HallOfFameId` | `int` (PK, Auto-Inkrement) | Primärschlüssel | | `HallOfFameId` | `int` (PK, Auto-Inkrement) | Primärschlüssel |
@@ -74,9 +170,13 @@ Die zentrale Entität repräsentiert einen einzelnen Absolventeneintrag und wird
| `CreatedOn` | `DateTime` | Erstellzeitpunkt (Audit) | | `CreatedOn` | `DateTime` | Erstellzeitpunkt (Audit) |
| `ModifiedBy` | `string` | Zuletzt geändert von (Audit) | | `ModifiedBy` | `string` | Zuletzt geändert von (Audit) |
| `ModifiedOn` | `DateTime` | Zeitpunkt der letzten Änderung (Audit) | | `ModifiedOn` | `DateTime` | Zeitpunkt der letzten Änderung (Audit) |
Die Entität implementiert das Oqtane-Interface `IAuditable`, wodurch die Audit-Felder automatisch vom Framework befüllt werden. Der Fremdschlüssel `ModuleId` verknüpft jeden Eintrag mit einer bestimmten Modulinstanz und ermöglicht so den Multi-Tenant-Betrieb: Mehrere Hall-of-Fame-Module auf verschiedenen Seiten der Website verwalten jeweils unabhängige Datensätze. Die Entität implementiert das Oqtane-Interface `IAuditable`, wodurch die Audit-Felder automatisch vom Framework befüllt werden. Der Fremdschlüssel `ModuleId` verknüpft jeden Eintrag mit einer bestimmten Modulinstanz und ermöglicht so den Multi-Tenant-Betrieb: Mehrere Hall-of-Fame-Module auf verschiedenen Seiten der Website verwalten jeweils unabhängige Datensätze.
**Entität HallOfFameReport** **Entität HallOfFameReport**
Die zweite Entität bildet einzelne Meldungen zu einem Eintrag ab und wird in der Tabelle `SZUAbsolventenvereinHallOfFameReport` gespeichert. Die zweite Entität bildet einzelne Meldungen zu einem Eintrag ab und wird in der Tabelle `SZUAbsolventenvereinHallOfFameReport` gespeichert.
| Spalte | Datentyp | Beschreibung | | Spalte | Datentyp | Beschreibung |
|--------|----------|--------------| |--------|----------|--------------|
| `HallOfFameReportId` | `int` (PK, Auto-Inkrement) | Primärschlüssel | | `HallOfFameReportId` | `int` (PK, Auto-Inkrement) | Primärschlüssel |
@@ -86,70 +186,149 @@ Die zweite Entität bildet einzelne Meldungen zu einem Eintrag ab und wird in de
| `CreatedOn` | `DateTime` | Erstellzeitpunkt (Audit) | | `CreatedOn` | `DateTime` | Erstellzeitpunkt (Audit) |
| `ModifiedBy` | `string` | Zuletzt geändert von (Audit) | | `ModifiedBy` | `string` | Zuletzt geändert von (Audit) |
| `ModifiedOn` | `DateTime` | Zeitpunkt der letzten Änderung (Audit) | | `ModifiedOn` | `DateTime` | Zeitpunkt der letzten Änderung (Audit) |
Der Fremdschlüssel zu `SZUAbsolventenvereinHallOfFame` ist mit kaskadierendem Löschen konfiguriert, sodass beim Löschen eines Eintrags automatisch alle zugehörigen Meldungen entfernt werden. Zwischen den beiden Entitäten besteht somit eine 1:n-Beziehung: Ein Eintrag kann beliebig viele Meldungen besitzen. Der Fremdschlüssel zu `SZUAbsolventenvereinHallOfFame` ist mit kaskadierendem Löschen konfiguriert, sodass beim Löschen eines Eintrags automatisch alle zugehörigen Meldungen entfernt werden. Zwischen den beiden Entitäten besteht somit eine 1:n-Beziehung: Ein Eintrag kann beliebig viele Meldungen besitzen.
#### Datenbankmigrationen #### Datenbankmigrationen
Die Datenbankstruktur wird über Entity Framework Core Migrationen versioniert verwaltet. Oqtane verwendet ein eigenes Migrationssystem, das auf der Klasse `MultiDatabaseMigration` basiert und die Kompatibilität mit verschiedenen Datenbankanbietern sicherstellt. Die Datenbankstruktur wird über Entity Framework Core Migrationen versioniert verwaltet. Oqtane verwendet ein eigenes Migrationssystem, das auf der Klasse `MultiDatabaseMigration` basiert und die Kompatibilität mit verschiedenen Datenbankanbietern sicherstellt.
| Migration | Versionsnummer | Inhalt | | Migration | Versionsnummer | Inhalt |
|-----------|---------------|--------| |-----------|----------------|--------|
| `InitializeModule` | `01.00.00.00` | Erstellt die Haupttabelle mit allen Grundspalten (Name, Year, Description, Image, Link, Status, UserId) sowie den Audit-Spalten | | `InitializeModule` | `01.00.00.00` | Erstellt die Haupttabelle mit allen Grundspalten (Name, Year, Description, Image, Link, Status, UserId) sowie den Audit-Spalten |
| `AddReportingColumns` | `01.00.00.02` | Erweitert die Haupttabelle um die Spalten `IsReported` und `ReportReason` | | `AddReportingColumns` | `01.00.00.02` | Erweitert die Haupttabelle um die Spalten `IsReported` und `ReportReason` |
| `AddReportTable` | `01.00.00.03` | Erstellt die eigenständige Report-Tabelle mit Fremdschlüssel zur Haupttabelle | | `AddReportTable` | `01.00.00.03` | Erstellt die eigenständige Report-Tabelle mit Fremdschlüssel zur Haupttabelle |
Jede Migration definiert sowohl eine Aufwärts- als auch eine Abwärtsmethode, sodass ein Rollback möglich ist. Jede Migration definiert sowohl eine Aufwärts- als auch eine Abwärtsmethode, sodass ein Rollback möglich ist.
#### Benutzeroberfläche (Razor-Komponenten) #### Benutzeroberfläche (Razor-Komponenten)
Das Modul umfasst vier Blazor-Razor-Komponenten. Das Modul umfasst vier Blazor-Razor-Komponenten.
**Index.razor Übersichtsseite** **Index.razor Übersichtsseite**
Die Übersichtsseite zeigt alle veröffentlichten Einträge in einem responsiven Kartenlayout mit drei Spalten auf Desktop-Breite. Die Funktionalität umfasst eine Echtzeit-Textsuche über Name und Beschreibung, eine umschaltbare Sortierung nach Datum, Name oder Jahrgang sowie eine Kartenanzeige mit Bild, Name, Jahrgang und gekürzter Beschreibung. Administratorinnen und Administratoren sehen zusätzlich Warnmeldungen bei gemeldeten Einträgen und einen Lösch-Button. Angemeldete Benutzerinnen und Benutzer können über einen eigenen Button ihren Eintrag erstellen oder bearbeiten. Die Übersichtsseite zeigt alle veröffentlichten Einträge in einem responsiven Kartenlayout mit drei Spalten auf Desktop-Breite. Die Funktionalität umfasst eine Echtzeit-Textsuche über Name und Beschreibung, eine umschaltbare Sortierung nach Datum, Name oder Jahrgang sowie eine Kartenanzeige mit Bild, Name, Jahrgang und gekürzter Beschreibung. Administratorinnen und Administratoren sehen zusätzlich Warnmeldungen bei gemeldeten Einträgen und einen Lösch-Button. Angemeldete Benutzerinnen und Benutzer können über einen eigenen Button ihren Eintrag erstellen oder bearbeiten.
**Edit.razor Erstellungs- und Bearbeitungsseite** **Edit.razor Erstellungs- und Bearbeitungsseite**
Die Edit-Komponente dient sowohl zum Erstellen als auch zum Bearbeiten eines Eintrags. Sie bietet Formularfelder für Name, Jahrgang, Beschreibung (mit Live-Zeichenzähler), Foto-Upload und Link. Über zwei Speicheroptionen kann zwischen Entwurf und Veröffentlichung gewählt werden. Eine Eigentümerprüfung verhindert das Bearbeiten fremder Einträge, und eine Duplikatprüfung verhindert das Erstellen mehrerer Einträge pro Person. Die Edit-Komponente dient sowohl zum Erstellen als auch zum Bearbeiten eines Eintrags. Sie bietet Formularfelder für Name, Jahrgang, Beschreibung (mit Live-Zeichenzähler), Foto-Upload und Link. Über zwei Speicheroptionen kann zwischen Entwurf und Veröffentlichung gewählt werden. Eine Eigentümerprüfung verhindert das Bearbeiten fremder Einträge, und eine Duplikatprüfung verhindert das Erstellen mehrerer Einträge pro Person.
**Details.razor Detailseite** **Details.razor Detailseite**
Die Details-Komponente zeigt einen einzelnen Eintrag in einem zweispaltigen Layout mit unscharfem Bildhintergrund. Administratorinnen und Administratoren sehen bei gemeldeten Einträgen eine Liste aller Meldungen mit Lösch-Button. Ein modaler Dialog ermöglicht die PDF-Vorschau sowie den Download. Die Meldefunktion wird über die zentrale `IReportUI`-Komponente eingebunden. Die Details-Komponente zeigt einen einzelnen Eintrag in einem zweispaltigen Layout mit unscharfem Bildhintergrund. Administratorinnen und Administratoren sehen bei gemeldeten Einträgen eine Liste aller Meldungen mit Lösch-Button. Ein modaler Dialog ermöglicht die PDF-Vorschau sowie den Download. Die Meldefunktion wird über die zentrale `IReportUI`-Komponente eingebunden.
**Settings.razor Moduleinstellungen** **Settings.razor Moduleinstellungen**
Die Settings-Komponente bietet eine einfache Oberfläche für Moduleinstellungen über Oqtanes Setting-Service. Die Settings-Komponente bietet eine einfache Oberfläche für Moduleinstellungen über Oqtanes Setting-Service.
#### Gemeinsame Melde-Komponente (IReportUI) #### Gemeinsame Melde-Komponente (IReportUI)
Die Meldefunktion in der Detailseite ist nicht direkt im Hall-of-Fame-Modul implementiert, sondern wird über eine zentrale Schnittstelle aus dem Interfaces-Paket eingebunden. Das Hall-of-Fame-Modell implementiert das Interface `IReportable`, das eine Entität als meldbar kennzeichnet. In der Detailseite wird per Dependency Injection eine `IReportUI`-Implementierung injiziert die konkrete `ReportComponent` stammt dabei aus dem Admin-Modul und stellt den Melden-Button samt modalem Dialog bereit. Die Komponente wird über Blazors `DynamicComponent` dynamisch gerendert. Ist keine Implementierung im Container registriert, wird die Meldefunktion schlicht nicht angezeigt. Dieses Konzept ermöglicht es, die Melde-Oberfläche zentral zu pflegen und in beliebig vielen weiteren Modulen wiederzuverwenden, ohne dass die einzelnen Module die Melde-Logik selbst implementieren müssen. Die Meldefunktion in der Detailseite ist nicht direkt im Hall-of-Fame-Modul implementiert, sondern wird über eine zentrale Schnittstelle aus dem Interfaces-Paket eingebunden. Das Hall-of-Fame-Modell implementiert das Interface `IReportable`, das eine Entität als meldbar kennzeichnet. In der Detailseite wird per Dependency Injection eine `IReportUI`-Implementierung injiziert die konkrete `ReportComponent` stammt dabei aus dem Admin-Modul und stellt den Melden-Button samt modalem Dialog bereit. Die Komponente wird über Blazors `DynamicComponent` dynamisch gerendert. Ist keine Implementierung im Container registriert, wird die Meldefunktion schlicht nicht angezeigt. Dieses Konzept ermöglicht es, die Melde-Oberfläche zentral zu pflegen und in beliebig vielen weiteren Modulen wiederzuverwenden, ohne dass die einzelnen Module die Melde-Logik selbst implementieren müssen.
#### PDF-Export mit QuestPDF #### PDF-Export mit QuestPDF
Für die Generierung der PDF-Dokumente wird die Open-Source-Bibliothek QuestPDF in der Community-Edition eingesetzt. Im Server-Projekt wurde ein benutzerdefiniertes MSBuild-Target erstellt, das nach jedem Build automatisch die QuestPDF-DLL sowie die plattformspezifischen nativen Bibliotheken in das bin-Verzeichnis des Oqtane-Servers kopiert. Dies ist notwendig, weil Oqtane Module zur Laufzeit dynamisch lädt und QuestPDF native Abhängigkeiten (unter anderem die SkiaSharp-Rendering-Engine) benötigt. Für die Generierung der PDF-Dokumente wird die Open-Source-Bibliothek QuestPDF in der Community-Edition eingesetzt. Im Server-Projekt wurde ein benutzerdefiniertes MSBuild-Target erstellt, das nach jedem Build automatisch die QuestPDF-DLL sowie die plattformspezifischen nativen Bibliotheken in das bin-Verzeichnis des Oqtane-Servers kopiert. Dies ist notwendig, weil Oqtane Module zur Laufzeit dynamisch lädt und QuestPDF native Abhängigkeiten (unter anderem die SkiaSharp-Rendering-Engine) benötigt.
Das PDF-Design folgt einem Glasmorphismus-Ansatz. Jede Seite hat das Format DIN A4 ohne Ränder. Über die Layers-API von QuestPDF wird das Hintergrundbild von der Inhaltsebene getrennt. Der Name wird in 36 Punkt ExtraBold mit Großbuchstaben und Zeichenabstand dargestellt, der Jahrgang in 15 Punkt mit erhöhtem Zeichenabstand und die Beschreibung in 11 Punkt mit 1,5-fachem Zeilenabstand. Der Glaseffekt wird durch halbtransparente dunkle Hintergründe mit mehrschichtigen Rahmen unterschiedlicher Transparenz erzeugt. Das PDF-Design folgt einem Glasmorphismus-Ansatz. Jede Seite hat das Format DIN A4 ohne Ränder. Über die Layers-API von QuestPDF wird das Hintergrundbild von der Inhaltsebene getrennt. Der Name wird in 36 Punkt ExtraBold mit Großbuchstaben und Zeichenabstand dargestellt, der Jahrgang in 15 Punkt mit erhöhtem Zeichenabstand und die Beschreibung in 11 Punkt mit 1,5-fachem Zeilenabstand. Der Glaseffekt wird durch halbtransparente dunkle Hintergründe mit mehrschichtigen Rahmen unterschiedlicher Transparenz erzeugt.
#### Implementierungsdetails und Problemlösungen #### Implementierungsdetails und Problemlösungen
Während der Entwicklung traten mehrere technische Herausforderungen auf, die im Folgenden zusammen mit ihren Lösungen beschrieben werden. Während der Entwicklung traten mehrere technische Herausforderungen auf, die im Folgenden zusammen mit ihren Lösungen beschrieben werden.
**Bild-Upload-System** **Bild-Upload-System**
In der ursprünglichen Version des Moduls mussten Benutzerinnen und Benutzer eine Bild-URL manuell in ein Textfeld eingeben. Das implementierte Bild-Upload-System ersetzt dies durch eine Blazor-InputFile-Komponente mit Live-Vorschau, Fortschrittsanzeige und Lösch-Button. Die Upload-Methode prüft die Dateigröße (maximal 5 MB), öffnet die Datei als Stream und übermittelt sie als MultipartFormDataContent an den Server, der den Dateityp serverseitig validiert, einen UUID-Dateinamen generiert und die Datei speichert. Das System unterstützt beide Blazor-Rendering-Modi.
In der ursprünglichen Version des Moduls mussten Benutzerinnen und Benutzer eine Bild-URL manuell in ein Textfeld eingeben. Das implementierte Bild-Upload-System ersetzt dies durch eine Blazor-`InputFile`-Komponente mit Live-Vorschau, Fortschrittsanzeige und Lösch-Button. Die Upload-Methode prüft die Dateigröße (maximal 5 MB), öffnet die Datei als Stream und übermittelt sie als `MultipartFormDataContent` an den Server, der den Dateityp serverseitig validiert, einen UUID-Dateinamen generiert und die Datei speichert. Das System unterstützt beide Blazor-Rendering-Modi.
**Concurrency Exception beim Löschen** **Concurrency Exception beim Löschen**
Beim Löschen von Einträgen mit vorhandenen Meldungen trat eine `DbUpdateConcurrencyException` auf, verursacht durch Konflikte im Entity Framework Change Tracker. Die Lösung bestand in der Aufteilung der Löschoperation in zwei separate Transaktionen mit jeweils eigenem DbContext: zuerst werden alle zugehörigen Meldungen gelöscht, danach der Haupteintrag. Beim Löschen von Einträgen mit vorhandenen Meldungen trat eine `DbUpdateConcurrencyException` auf, verursacht durch Konflikte im Entity Framework Change Tracker. Die Lösung bestand in der Aufteilung der Löschoperation in zwei separate Transaktionen mit jeweils eigenem DbContext: zuerst werden alle zugehörigen Meldungen gelöscht, danach der Haupteintrag.
**Kartendesign-Optimierung** **Kartendesign-Optimierung**
Karten hatten ursprünglich unterschiedliche Höhen durch variierende Beschreibungslängen. Die Lösung kombiniert CSS-Flexbox auf dem Kartenelement mit einer Maximalhöhe von 150 Pixeln und `overflow: hidden` auf dem Beschreibungscontainer, sodass alle Karten einer Zeile dieselbe Höhe aufweisen. Karten hatten ursprünglich unterschiedliche Höhen durch variierende Beschreibungslängen. Die Lösung kombiniert CSS-Flexbox auf dem Kartenelement mit einer Maximalhöhe von 150 Pixeln und `overflow: hidden` auf dem Beschreibungscontainer, sodass alle Karten einer Zeile dieselbe Höhe aufweisen.
**Sortier-Toggle** **Sortier-Toggle**
Die ursprünglich fest codierten Sortierrichtungen wurden durch einen Toggle-Button neben dem Sortier-Dropdown ersetzt, der mit einem dynamischen Pfeil-Icon zwischen aufsteigender und absteigender Sortierung umschaltet. Die Sortierlogik ist in einer berechneten Eigenschaft gekapselt, die Suche und Sortierung kombiniert. Die ursprünglich fest codierten Sortierrichtungen wurden durch einen Toggle-Button neben dem Sortier-Dropdown ersetzt, der mit einem dynamischen Pfeil-Icon zwischen aufsteigender und absteigender Sortierung umschaltet. Die Sortierlogik ist in einer berechneten Eigenschaft gekapselt, die Suche und Sortierung kombiniert.
--- ---
## 8. Projektorganisation & Teamarbeit ## 8. Projektorganisation & Teamarbeit
### 8.1 Planung & Meilensteine ### 8.1 Planung & Meilensteine
#### Meilensteine #### Meilensteine
#### Soll-/Ist-Vergleich #### Soll-/Ist-Vergleich
#### Zeitverzug #### Zeitverzug
### 8.2 Teamverkleinerung ### 8.2 Teamverkleinerung
#### Downsizing von 6 auf 3 Personen #### Downsizing von 6 auf 3 Personen
#### Neue Aufgabenverteilung #### Neue Aufgabenverteilung
#### Auswirkungen auf Theme-Entwicklung, Module und Zeitplanung #### Auswirkungen auf Theme-Entwicklung, Module und Zeitplanung
--- ---
## 9. Übergangslösung, Probleme & Learnings ## 9. Übergangslösung, Probleme & Learnings
### 9.1 Übergangslösung (Sommer 2025) ### 9.1 Übergangslösung (Sommer 2025)
#### Gründe & Technische Umsetzung #### Gründe & Technische Umsetzung
Da AlumniHub zum Zeitpunkt des ersten Absolventenstreffens im Sommer 2025 noch nicht fertig war und das Hosting von Oqtane auf dem Hetzner-Server zu Problemen geführt hat, wurde im Team eine einfache Übergangslösung entwickelt. Ziel war es, pünktlich zur Veranstaltung eine funktionierende Lösung bereitzustellen, über die Absolventinnen und Absolventen ihre Teilnahme bestätigen oder absagen sowie Feedback zum Projekt abgeben konnten.
Das Frontend wurde mit HTML, CSS und JavaScript umgesetzt. Die Hauptseite (`index.html`) zeigt einen Button, über den ein modales Overlay-Formular geöffnet wird. Darüber konnten Nutzer ihre E-Mail-Adresse und ihr Feedback eingeben und absenden. Für Zu- und Absagen wurden separate Bestätigungsseiten erstellt (`zusage.html` und `absage.html`). Das Overlay schließt sich automatisch beim Klick außerhalb des Formulars. Das Design orientiert sich am Erscheinungsbild der HTL Ungargasse.
#### Differenzen zur finalen Lösung #### Differenzen zur finalen Lösung
Im Vergleich zu AlumniHub ist die Übergangslösung deutlich schlichter gehalten. Es gibt keine Benutzerkonten, keine Datenbank und keine Verwaltungsfunktionen. Nach dem Absolvententreffen wurde die Seite nicht abgeschaltet, sondern als Feedback-Seite für das Diplomprojekt weitergenutzt. Lehrkräfte, Mitschülerinnen und Mitschüler sowie externe Besucher konnten darüber Feedback abgeben, bis AlumniHub diese Funktion selbst übernahm.
### 9.2 Probleme ### 9.2 Probleme
#### Technische Probleme #### Technische Probleme
Ein großes Problem war der Zeitdruck vor dem ersten Absolvententreffen. Da AlumniHub zu dem Zeitpunkt noch nicht einsatzbereit war, musste die Übergangslösung schnell entwickelt und in Betrieb genommen werden. Außerdem gab es beim Hosting von Oqtane auf dem Hetzner-Server technische Schwierigkeiten, die den Entwicklungsfortschritt gebremst haben.
#### Organisatorische Probleme #### Organisatorische Probleme
Im Sommer 2025 war die Mitarbeit im Team sehr ungleich verteilt. Viele Teammitglieder waren nicht erreichbar oder haben nicht aktiv am Projekt weitergearbeitet. Nur ein kleiner Teil des Teams hat in dieser Zeit wirklich weitergemacht. Das hat zu einer ungleichen Arbeitsbelastung geführt und den Fortschritt in dieser Phase deutlich verlangsamt.
### 9.3 Learnings ### 9.3 Learnings
#### Technisch #### Technisch
Durch das Projekt wurden praktische Kenntnisse in Git zur Versionskontrolle gewonnen, was besonders für die Zusammenarbeit im Team wichtig war. Außerdem wurde Responsive Design vertieft also wie man Layouts baut, die auf verschiedenen Bildschirmgrößen gut aussehen. Auch der Umgang mit JavaScript für UI-Elemente wie Overlays und Menüs wurde durch die praktische Arbeit besser verstanden.
#### Methodisch #### Methodisch
Es hat sich gezeigt, dass eine klare Aufgabenverteilung von Anfang an wichtig ist. Wenn nicht klar ist, wer was macht, entstehen Verzögerungen. Regelmäßige Team-Meetings wären hilfreich gewesen, um den Stand abzugleichen und Probleme früh zu erkennen.
#### Persönlich #### Persönlich
Eine wichtige persönliche Erkenntnis war, dass Eigeninitiative in einem Teamprojekt entscheidend ist. Besonders in Phasen, wo nicht alle aktiv waren, hat sich gezeigt, dass man Aufgaben selbst in die Hand nehmen muss, damit das Projekt vorankommt.
--- ---
## 10. Testen & Qualitätssicherung ## 10. Testen & Qualitätssicherung
### Funktionstests der Module ### Funktionstests der Module
### Theme-Tests ### Theme-Tests
### Usability-Tests ### Usability-Tests
### Bekannte Einschränkungen ### Bekannte Einschränkungen
--- ---
## 11. Fazit & Ausblick ## 11. Fazit & Ausblick
### Zielerreichung & Zusammenfassung ### Zielerreichung & Zusammenfassung
### Persönliche Reflexion ### Persönliche Reflexion
### Erweiterungsmöglichkeiten ### Erweiterungsmöglichkeiten

View File

@@ -109,37 +109,268 @@ Darüber hinaus besteht das Recht, sich bei der österreichischen Datenschutzbeh
Durch die Kombination aus klar definierten Rechtsgrundlagen, transparenten Informationspflichten und technischen Schutzmaßnahmen wird ein hohes Datenschutzniveau gewährleistet und der verantwortungsvolle Umgang mit personenbezogenen Daten sichergestellt. Durch die Kombination aus klar definierten Rechtsgrundlagen, transparenten Informationspflichten und technischen Schutzmaßnahmen wird ein hohes Datenschutzniveau gewährleistet und der verantwortungsvolle Umgang mit personenbezogenen Daten sichergestellt.
# 5. Automatisierung von Serverprozessen # 5. Automatisierung von Serverprozessen
## 5.1 Notwendigkeit von Datensicherung ## 5.1 Notwendigkeit von Datensicherung [^4][^5]
Die Datensicherung ist ein wesentlicher Bestandteil der Website des Absolventenvereins. Sie dient dazu, die auf der Website gespeicherten Daten vor Verlust oder Beschädigung zu schützen.Digitale Daten bilden die Grundlage für die Funktion der Website und müssen daher regelmäßig gesichert werden. Ein Verlust oder eine Beschädigung der Daten kann zu erheblichen Problemen führen. Die Datensicherung ist ein wesentlicher Bestandteil der Website des Absolventenvereins. Sie dient dazu, die auf der Website gespeicherten Daten vor Verlust oder Beschädigung zu schützen.Digitale Daten bilden die Grundlage für die Funktion der Website und müssen daher regelmäßig gesichert werden. Ein Verlust oder eine Beschädigung der Daten kann zu erheblichen Problemen führen.
Mögliche Ursachen für einen Datenverlust sind unter anderem Hardwaredefekte, Softwarefehler, menschliches Versagen oder Cyberangriffe. Mögliche Ursachen für einen Datenverlust sind unter anderem Hardwaredefekte, Softwarefehler, menschliches Versagen oder Cyberangriffe.Ohne geeignete Sicherheitsmaßnahmen besteht das Risiko, dass Daten unwiederruflich verloren gehen oder nur mit erheblichen Kosten wiederhergestellt werden können.
Neben der technischen besteht ebennfalls eine rechtliche und organisatorische Notwendigkeit der Datensicherung. Gemäß Art. 5 Abs. 1 lit. f DSGVO müssen personenbezogene Daten gesichert werden, um die Integrität und Vertraulichkeit der Daten zu gewährleisten. Dazu zählt auch der Schutz vor unbeabsichtigtem Verlust. Eine Backup-Sicherung ist daher ein notwendiger Schritt, um die Daten vor Verlust zu schützen.
## 5.2 Konzeption des Backup-Systems ## 5.2 Konzeption des Backup-Systems
Ziel des Backu-Systems war es im Falle eines DAtenverlustes oder eines Systemausfalls die Daten wiederherstellen zu können. Es wurde eine Startegie enwickelt die es ermöglicht sowohl die Datenbank als auch die Dateien auf dem Server zu sichern.
Der Umfang des Backups besteht aus zwei zentralen Komponenten:
1. Die Postgres Datenbank
2. Der Ordner "oqtane.server" auf dem Server
Damit wird eine vollständige Sicherung der Website ermöglicht, da bei einer reinen Datenbanksicherung wichtige Anwendungsdatein fehlen würden
Alle Backups werden lokal auf dem Server gespeichert. Diese Backups sind alle zu finden unter: /var/backups
Für jedes Backup wird in diesem Verzeichnis ein Unterordner erstellt,dessen Name einen UTC-Zeitstempel enthält.Das ermöglicht eine saubere Trennung der Sicherungsstände und einfache chronologische Zuordnung.
TS="$(date -u +'%Y%m%dT%H%M%SZ')"
DEST_DIR="${BACKUP_ROOT}/${TS}"
date -u erzeugt einen Zeitstempel in UTC, wodurch die Bennenung unabhängig von Zeitzonen eindeutig bleibt.
Der Zeitstempel wird als Ordnername verwendet, sodass jedes Backup separat abgelegt wird (z. B. 20260226T023000Z).
Die Sicherung der PostgreSQL-Datenbank erfolgt mithilfe des Programms pg_dump, welches ein vollständiges Abbild der Datenbank erzeugt:
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -Fc \
-f "${DEST_DIR}/db_${PGDATABASE}.dump"
Hierbei werden Host, Port, Benutzer und Datenbankname über Variablen definiert, wodurch das Skript flexibel einsetzbar bleibt. Der Parameter -Fc erstellt das Backup im sogenannten Custom-Format. Dieses Format bietet den Vorteil, dass beim Wiederherstellen einzelne Tabellen oder Datenbankobjekte selektiv importiert werden können. Zudem ist es komprimiert und effizienter als ein reines SQL-Textdump. Die Ausgabe wird direkt in eine Datei im zuvor erstellten Backup-Ordner geschrieben. Die Authentifizierung erfolgt über eine .pgpass-Datei, wodurch das Backup automatisiert und ohne manuelle Passworteingabe ausgeführt werden kann.
Neben der Datenbank wird auch der gesamte Anwendungsordner gesichert. Dies geschieht mithilfe des tar-Programms, das Dateien zu einem komprimierten Archiv zusammenfasst:
tar -cvpzf "${DEST_DIR}/oqtane_files.tar.gz" \
--absolute-names "$OQTANE_DIR"
Das Programm tar steht für Tape Archive und wurde ursprünglich entwickelt, um mehrere Dateien auf Magnetbändern zu sichern. Heute wird es unter Linux und Unix-Systemen verwendet, um mehrere Dateien und Verzeichnisse zu einem einzigen Archiv zusammenzufassen. Dabei kann das Archiv optional komprimiert werden.
Der Parameter -c erstellt ein neues Archiv, -v gibt die verarbeiteten Dateien aus, -p erhält die ursprünglichen Dateiberechtigungen und -z aktiviert die gzip-Komprimierung. Die Komprimierung reduziert den Speicherbedarf der Sicherung erheblich. Besonders relevant ist die Option --absolute-names, da dadurch die vollständigen Pfadangaben im Archiv gespeichert werden. Dies erleichtert eine spätere exakte Wiederherstellung der ursprünglichen Verzeichnisstruktur. Durch die Kombination aus Datenbank-Dump und komprimiertem Dateisystem-Archiv entsteht ein vollständiges Vollbackup des Systems.
Ein Cronjob ist eine zeitgesteuerte Aufgabe unter Linux-Systemen, die automatisch zu einem festgelegten Zeitpunkt ausgeführt wird. Er basiert auf dem Hintergrunddienst cron, der regelmäßig überprüft, ob geplante Befehle gestartet werden müssen.
Im vorliegenden Projekt wird ein Cronjob verwendet, um das Backup-Skript täglich automatisiert auszuführen. Der entsprechende Eintrag lautet:
30 2 * * * /home/florian/backup-script.sh >> /home/florian/backup.log 2>&1
Die ersten fünf Felder definieren den Ausführungszeitpunkt. In diesem Fall bedeutet die Konfiguration, dass das Skript täglich um 02:30 Uhr gestartet wird. Die nächtliche Ausführung reduziert die Systembelastung während der regulären Nutzung.
Der hintere Teil des Befehls (>> /home/florian/backup.log 2>&1) sorgt dafür, dass sowohl normale Ausgaben als auch Fehlermeldungen in einer Logdatei gespeichert werden. Dadurch kann überprüft werden, ob das Backup erfolgreich durchgeführt wurde.
Durch den Einsatz eines Cronjobs wird eine regelmäßige und zuverlässige Datensicherung ohne manuelles Eingreifen gewährleistet.
Ein weiterer Bestandteil des Backup-Systems ist ein automatischer Mechanismus zur Verwaltung alter Sicherungen. Da Backups langfristig Speicherplatz beanspruchen können, wurde eine Funktion implementiert, die nur eine definierte Anzahl an Sicherungen beibehält. In der Konfiguration des Skripts wird festgelegt, dass maximal 30 Backups gespeichert werden.
RETAIN=30
Während der Ausführung überprüft das Skript, wie viele Backup-Ordner im Backup-Verzeichnis vorhanden sind. Die Ordner werden anhand ihres Zeitstempels sortiert, wodurch die chronologische Reihenfolge der Sicherungen eindeutig bestimmt werden kann. Sollte die Anzahl der vorhandenen Backups den definierten Wert überschreiten, werden automatisch die ältesten Sicherungen gelöscht.
if (( ${#backups[@]} > RETAIN )); then
for old in "${backups[@]:RETAIN}"; do
rm -rf -- "$old"
done
fi
Durch diesen Rotationsmechanismus wird verhindert, dass sich im Laufe der Zeit eine unbegrenzte Anzahl von Backups ansammelt und dadurch der verfügbare Speicherplatz auf dem Server erschöpft wird. Gleichzeitig bleibt eine ausreichende Anzahl an Sicherungsständen erhalten, um im Fehlerfall auf frühere Systemzustände zurückgreifen zu können.
Die gewählte Anzahl von 30 Sicherungen ermöglicht es, bei täglicher Ausführung des Backup-Skripts ungefähr einen Monat an Wiederherstellungspunkten vorzuhalten. Dadurch wird ein sinnvoller Kompromiss zwischen Datensicherheit und effizienter Speicherverwaltung erreicht.
## 5.3 Implementierung der Backup-Skripte ## 5.3 Implementierung der Backup-Skripte
Die Implementierung des Backup-Systems erfolgte mittels eines Bash-Skripts, das auf dem Linux-Server ausgeführt wird. Bash eignet sich besonders für administrative Aufgaben, da sie direkten Zugriff auf Systembefehle, Dateien und Prozesse bietet. Das Skript wurde so aufgebaut, dass es robust, automatisierbar und nachvollziehbar arbeitet.
Zu Beginn des Skripts wird folgende Konfiguration gesetzt:
#!/usr/bin/env bash
set -euo pipefail
Die erste Zeile definiert den Interpreter, mit dem das Skript ausgeführt wird. Dadurch wird sichergestellt, dass unabhängig von der Systemkonfiguration die korrekte Bash-Version verwendet wird.
Die Anweisung set -euo pipefail erhöht die Sicherheit und Stabilität des Skripts:
-e beendet das Skript sofort, wenn ein Befehl fehlschlägt
-u verhindert die Verwendung nicht definierter Variablen
-o pipefail sorgt dafür, dass auch Fehler innerhalb von Befehls-Pipelines erkannt werden
Diese Einstellungen verhindern, dass das Backup bei Fehlern unbemerkt unvollständig ausgeführt wird.
Im nächsten Schritt werden zentrale Konfigurationsvariablen definiert:
BACKUP_ROOT="/var/backups/fullserver"
RETAIN=30
PGHOST="localhost"
PGPORT="5432"
PGUSER="florian"
PGDATABASE="oqtane"
Durch die Verwendung von Variablen bleibt das Skript flexibel und wartbar. Änderungen, beispielsweise am Speicherort oder an der Datenbank, können an einer zentralen Stelle vorgenommen werden, ohne den restlichen Code anzupassen.
Die Sicherung der PostgreSQL-Datenbank erfolgt mittels pg_dump im Custom-Format. Dieses Format ist besonders geeignet, da es eine flexible Wiederherstellung ermöglicht. Die Ausgabe wird direkt in den jeweiligen Backup-Ordner geschrieben. Die Authentifizierung erfolgt über eine .pgpass-Datei, wodurch das Backup ohne interaktive Passworteingabe automatisiert durchgeführt werden kann.
Anschließend wird der gesamte Anwendungsordner mit tar archiviert und komprimiert. Dabei werden Dateirechte und Verzeichnisstrukturen vollständig erhalten. Dies ist insbesondere bei Webanwendungen wichtig, da falsche Berechtigungen nach einer Wiederherstellung zu Funktionsstörungen führen könnten.
Ein weiterer wichtiger Bestandteil der Implementierung ist das integrierte Rotationsprinzip. Das Skript überprüft automatisch, wie viele Backup-Ordner vorhanden sind. Falls die definierte Grenze von 30 Sicherungen überschritten wird, werden die ältesten Backups gelöscht. Dadurch wird verhindert, dass der verfügbare Speicherplatz durch alte Sicherungen dauerhaft belegt wird. Die Sortierung erfolgt anhand der Zeitstempel im Ordnernamen, wodurch die chronologische Reihenfolge eindeutig bestimmbar ist.
Zusätzlich gibt das Skript Statusmeldungen mit Zeitangaben aus:
echo "[$(date -Iseconds)] Backup-Job abgeschlossen."
Diese Meldungen werden bei automatischer Ausführung über den Cronjob in eine Logdatei geschrieben. Dadurch kann jederzeit überprüft werden, ob das Backup erfolgreich abgeschlossen wurde oder ob Fehler aufgetreten sind.
Die Implementierung zeichnet sich durch folgende Eigenschaften aus:
vollständige Sicherung von Datenbank und Anwendungsdateien
automatische tägliche Ausführung
integrierte Fehlerbehandlung
strukturierte Archivierung mit Zeitstempel
automatische Speicherverwaltung durch Rotationsmechanismus
Durch diese Umsetzung wurde ein zuverlässiges und wartbares Backup-System geschaffen, das den kontinuierlichen Betrieb der Webanwendung unterstützt und im Fehlerfall eine schnelle Wiederherstellung ermöglicht.
## 5.4 Wiederherstellung von Daten mittels Restore-Skripten ## 5.4 Wiederherstellung von Daten mittels Restore-Skripten
Neben der Datensicherung stellt die strukturierte Wiederherstellung der Daten einen zentralen Bestandteil des Backup-Systems dar. Zu diesem Zweck wurde ein eigenes Bash-Skript implementiert, das sowohl die PostgreSQL-Datenbank als auch die Anwendungsdateien aus einem gewählten Backup-Ordner wiederherstellt
Resolve-Skript
. Ziel war es, einen klar definierten und kontrollierten Prozess zu schaffen, der im Fehlerfall eine vollständige Rücksetzung des Systems ermöglicht.
Das Skript beginnt analog zum Backup-Skript mit einer sicheren Ausführungskonfiguration:
#!/usr/bin/env bash
set -euo pipefail
Auch hier sorgt set -euo pipefail dafür, dass das Skript bei Fehlern sofort abbricht, keine undefinierten Variablen verwendet werden und Fehler innerhalb von Befehls-Pipelines korrekt erkannt werden. Dadurch wird verhindert, dass ein unvollständiger oder fehlerhafter Restore durchgeführt wird.
Zu Beginn wird festgelegt, welcher Backup-Ordner verwendet werden soll. Wird beim Start ein Argument übergeben, wird dieses als Backup-Verzeichnis interpretiert. Andernfalls wird automatisch das neueste Backup ausgewählt:
if [[ $# -ge 1 ]]; then
BACKUP_SUBDIR="$1"
else
BACKUP_SUBDIR="$(ls -1 "${BACKUP_ROOT}" | sort | tail -n 1)"
fi
Diese Logik ermöglicht eine flexible Nutzung des Skripts. Administratoren können gezielt einen bestimmten Sicherungsstand auswählen oder standardmäßig den aktuellsten verwenden.
Vor Beginn der Wiederherstellung erfolgt eine deutliche Sicherheitswarnung:
read -rp "Fortfahren? (ja/nein): " answer
if [[ "$answer" != "ja" ]]; then
exit 0
fi
Diese Abfrage dient als Schutzmechanismus, da der Restore-Prozess bestehende Daten überschreibt. Erst nach expliziter Bestätigung wird die Wiederherstellung gestartet.
Die Wiederherstellung der PostgreSQL-Datenbank erfolgt in mehreren Schritten. Zunächst wird falls vorhanden die bestehende Datenbank gelöscht:
dropdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" "$PGDATABASE"
Anschließend wird eine neue, leere Datenbank erstellt:
createdb -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" "$PGDATABASE"
Danach wird der zuvor gesicherte Dump eingespielt:
pg_restore -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
-d "$PGDATABASE" -c "$DUMP_FILE"
Das Programm pg_restore liest das im Custom-Format gespeicherte Backup ein und stellt sämtliche Tabellen, Indizes und Datenbankobjekte wieder her. Die Option -c sorgt dafür, dass bestehende Objekte vor dem Import entfernt werden, wodurch Konflikte vermieden werden.
Neben der Datenbank wird auch der Anwendungsordner wiederhergestellt. Bevor die neuen Dateien entpackt werden, wird der aktuell vorhandene Ordner zur Sicherheit umbenannt:
mv "$OQTANE_DIR" "${OQTANE_DIR}.old.$(date +%s)"
Diese Maßnahme verhindert einen vollständigen Datenverlust, falls beim Restore ein Problem auftreten sollte. Anschließend wird das Archiv entpackt:
tar -xvpzf "$FILES_ARCHIVE"
Der Parameter -x extrahiert das Archiv, -p stellt die ursprünglichen Dateiberechtigungen wieder her und -z dekomprimiert das gzip-Archiv. Dadurch wird die komplette Ordnerstruktur in ihrem ursprünglichen Zustand wiederhergestellt.
Zusammenfassend ermöglicht das Restore-Skript eine vollständige Rücksetzung der Webanwendung auf einen definierten Sicherungsstand. Durch die Kombination aus Sicherheitsabfrage, kontrollierter Datenbank-Neuerstellung und strukturiertem Dateirestore wird ein zuverlässiger und nachvollziehbarer Wiederherstellungsprozess gewährleistet. Das System ergänzt somit das Backup-Konzept um eine praxisnahe und technisch saubere Lösung für den Ernstfall.
## 5.5 Fazit zur Datensicherung
Durch die Implementierung eines automatisierten Backup- und Restore-Systems wurde eine zuverlässige Grundlage für die Datensicherheit der Webanwendung geschaffen. Das entwickelte System ermöglicht eine regelmäßige Sicherung sowohl der PostgreSQL-Datenbank als auch der vollständigen Anwendungsdateien. Durch die automatisierte Ausführung mittels Cronjob wird sichergestellt, dass die Backups ohne manuelles Eingreifen täglich erstellt werden.
Zusätzlich sorgt das integrierte Rotationsprinzip dafür, dass ältere Sicherungen automatisch entfernt werden und der verfügbare Speicherplatz effizient genutzt wird. Die strukturierte Ablage der Backups mit Zeitstempeln erleichtert die Auswahl eines bestimmten Sicherungsstandes.
Neben der Datensicherung wurde auch ein Restore-Skript implementiert, das eine vollständige Wiederherstellung des Systems ermöglicht. Dabei werden sowohl die Datenbank als auch die Anwendungsdateien aus einem ausgewählten Backup wiederhergestellt. Sicherheitsmechanismen wie Bestätigungsabfragen und das temporäre Umbenennen bestehender Daten reduzieren das Risiko von Datenverlust während des Wiederherstellungsprozesses.
Insgesamt stellt das entwickelte Backup-System eine robuste und praxisnahe Lösung dar, die den sicheren Betrieb der Webanwendung unterstützt und im Fehlerfall eine schnelle Wiederherstellung der Daten ermöglicht.
# 6. Benutzerverwaltung und Authentifizierung per LinkedIn # 6. Benutzerverwaltung und Authentifizierung per LinkedIn
## 6.1 Anmeldung über LinkedIn ## 6.1 Authentifizierung mittels OAuth 2.0
## 6.2 Profilabgleich und Benutzerverifikation Zur Anmeldung auf der entwickelten Webplattform wurde eine Authentifizierung über das Business-Netzwerk LinkedIn implementiert. Dabei kommt das standardisierte Autorisierungsprotokoll OAuth 2.0 zum Einsatz. Dieses Verfahren ermöglicht es, Benutzer über externe Identitätsanbieter zu authentifizieren, ohne dass deren Zugangsdaten direkt an die Webanwendung übertragen werden müssen.
# 7. Octane OAuth 2.0 basiert auf dem Prinzip der delegierten Autorisierung. Dabei erlaubt ein Benutzer einer Anwendung, bestimmte Informationen seines Kontos bei einem externen Dienst zu verwenden. Die eigentlichen Zugangsdaten beispielsweise das LinkedIn-Passwort verbleiben dabei ausschließlich beim Identitätsanbieter.
## 7.1 Einführung in das verwendete CMS Octane Der grundlegende Ablauf einer OAuth-Authentifizierung ist in Abbildung X dargestellt. Dabei wird der Benutzer zunächst zur Login-Seite des externen Anbieters weitergeleitet. Nach erfolgreicher Anmeldung bestätigt der Benutzer, dass die Anwendung Zugriff auf bestimmte Profildaten erhalten darf.
## 7.2 Modulares System von Octane ![alt text](image.png) Erstellt von ChatGbt
## 7.3 Entwicklung eigener Module
## 7.4 Vorteile der modularen Erweiterbarkeit
# 8. Implementierung des Premium-Bereichs Die Abbildung zeigt den Ablauf der OAuth-Authentifizierung. Zunächst erfolgt die Weiterleitung zur LinkedIn-Anmeldeseite, anschließend wird ein Autorisierungscode an die Webanwendung zurückgegeben, welcher gegen einen Zugriffstoken ausgetauscht wird.
## 8.1 Ziel und Zweck des Premium-Bereichs Nach der Autorisierung sendet LinkedIn einen sogenannten Authorization Code an die Webanwendung zurück. Dieser Code wird anschließend vom Server der Anwendung gegen einen Access Token ausgetauscht. Mit Hilfe dieses Tokens kann die Webanwendung anschließend die freigegebenen Benutzerdaten vom LinkedIn-Server abrufen.
## 8.2 Funktionen und Features des Premium-Zugangs
## 8.3 Zugriffsbeschränkung und Benutzerrechte
## 8.4 Mehrwert für registrierte Mitglieder
# 9. Technologien Der Vorteil dieses Verfahrens liegt darin, dass die Webanwendung zu keinem Zeitpunkt Zugriff auf das Passwort des Benutzers erhält. Dadurch wird ein höheres Sicherheitsniveau erreicht und gleichzeitig die Benutzerfreundlichkeit verbessert, da sich Benutzer mit ihrem bestehenden LinkedIn-Konto anmelden können.
## 6.2 Technische Umsetzung der LinkedIn-Anmeldung in Oqtane
Die technische Umsetzung der LinkedIn-Authentifizierung erfolgte über das integrierte External Login System des Content-Management-Systems Oqtane. Dieses System ermöglicht die Integration externer Identitätsanbieter über standardisierte Protokolle wie OAuth 2.0.
## 9.1 ASP.NET und .NET Für die Verbindung mit LinkedIn wurde zunächst eine Entwickleranwendung im LinkedIn Developer Portal erstellt. Dabei werden zwei zentrale Zugangsdaten generiert:
## 9.2 Postgres Datenbank
## 9.3 LinkedIn OAuth 2.0 Client ID
Client Secret
Diese dienen zur Identifikation der Webanwendung gegenüber den LinkedIn-Servern.
Innerhalb der Oqtane-Konfiguration wurde anschließend ein neuer OAuth-Provider eingerichtet. Dabei mussten mehrere Parameter definiert werden, darunter die Autorisierungs- und Token-Endpunkte von LinkedIn.
Beispielsweise wird für die Benutzerautorisierung folgende Adresse verwendet:
https://www.linkedin.com/oauth/v2/authorization
Nach erfolgreicher Anmeldung stellt LinkedIn über folgenden Endpunkt ein Zugriffstoken bereit:
https://www.linkedin.com/oauth/v2/accessToken
Zusätzlich wurde eine sogenannte Redirect URL definiert. Diese URL wird von LinkedIn verwendet, um den Benutzer nach erfolgreicher Authentifizierung wieder zurück zur Webanwendung zu leiten.
In der Konfiguration wurde außerdem festgelegt, welche Benutzerdaten von LinkedIn übernommen werden sollen. Dazu gehören unter anderem:
Benutzername
E-Mail-Adresse
eindeutige Benutzer-ID
Diese Daten werden von Oqtane als sogenannte Claims verarbeitet und anschließend dem Benutzerkonto der Plattform zugeordnet.
Darüber hinaus wurde in den Einstellungen aktiviert, dass bei der ersten Anmeldung automatisch ein neues Benutzerkonto erstellt werden kann. Dadurch können sich neue Nutzer direkt über ihr LinkedIn-Konto registrieren.
## 6.3 Ablauf des Anmeldeprozesses
Der eigentliche Anmeldevorgang erfolgt in mehreren aufeinanderfolgenden Schritten. Zunächst wählt der Benutzer auf der Login-Seite der Webplattform die Option zur Anmeldung über LinkedIn aus.
Daraufhin wird der Benutzer zur LinkedIn-Authentifizierungsseite weitergeleitet. Dort meldet sich der Benutzer mit seinem LinkedIn-Konto an und bestätigt die Autorisierung der Anwendung.
Nach erfolgreicher Authentifizierung sendet LinkedIn eine Antwort an die zuvor definierte Redirect-URL der Webanwendung. Diese Antwort enthält einen Autorisierungscode, der anschließend vom Server der Webanwendung gegen einen Access Token ausgetauscht wird.
Mit Hilfe dieses Tokens kann die Anwendung anschließend die freigegebenen Benutzerdaten vom LinkedIn-Server abrufen. Diese Daten werden verwendet, um den Benutzer in der lokalen Datenbank zu identifizieren oder ein neues Benutzerkonto zu erstellen.
# 7. Implementierung des Premium-Bereichs
## 7.1 Ziel und Zweck des Premium-Bereichs
## 7.2 Funktionalität und Features des Premium-Bereichs
## 7.3 Zugriffsbeschränkung und Benutzerrechte
## 7.4 Mehrwert für registrierte Mitglieder
# 8. Technologien
## 8.1 ASP.NET und .NET
## 8.2 Postgres Datenbank
## 8.3 LinkedIn OAuth 2.0
# 10. Learnigs # 10. Learnigs
## 10.1 Technische und fachliche Erkenntnisse ## 10.1 Technische und fachliche Erkenntnisse
@@ -160,4 +391,9 @@ Mögliche Ursachen für einen Datenverlust sind unter anderem Hardwaredefekte, S
[^3]: DSB Österreichische Datenschutzbehörde: https://dsb.gv.at/ [^3]: DSB Österreichische Datenschutzbehörde: https://dsb.gv.at/
[^4]:Datensicherung und Datenverlust: https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Daten-sichern-verschluesseln-und-loeschen/Datensicherung-und-Datenverlust/Datensicherung-wie-geht-das/datensicherung-wie-geht-das_node.html
[^5]: ChatGPT Notwendigkeit der Datensicherung: https://chatgpt.com/c/69a06632-e4fc-8384-bd6c-a543d7bbd00d

View File

@@ -3,14 +3,59 @@ include_toc: true
gitea: none gitea: none
--- ---
## 1. Einleitung des individuellen Teils
In diesem Abschnitt wird meine persönliche Aufgabenstellung im Rahmen des Projektes (`Alumnihub`) beschrieben.
### Auftrag / persönliche Aufgabenstellungen
Meine Zuständigkeiten und Verantwortlichkeiten:
- Product Owner
- Infrastruktur
- Entwicklung
- Auswertungen
- Schwarzes Brett
### Motivation
Lernen von ASP.NET und der Entwicklung mit Blazor und Oqtane. Ich habe Interesse an dem Thema Webentwicklung. Privat entwickel ich schon seit Jahren viel mit React.JS.
## 2. Anforderungen an das entwickelte Modul bzw. die Funktionalität
- funktionale / nichtfunktionale Anforderungen
- Use Cases
## 3. Technische Grundlagen
Mein Aufgabenbereich umfasst einerseits die Entwicklung eigener Module, sowie das Bereitstellen des Services. Als Betriebssystem habe ich mich für Linux entschieden, einfach, da ich mit Linux im Serverumfeld die meisten und besten Erfahrungen gemacht habe.
(Diese Entscheidung wurde gemeinsam getroffen:)
Auch steht die Wahl der Programmiersprache und des CMS an. Nachdem wir im Unterricht fast ausschließlich mit C# entwickelt haben und nicht in eine komplett unbekannte Entwicklungsumgebung abdriften wollten, haben wir uns für Webentwicklung mit ASP.NET Core 9 und (Upgrade im Lauf der Diplomarbeit auf .NET Core 10) dem CMS Oqtane entschieden. Auch hier gab es einige Kandidaten:
- Piranha CMS
>Piranha erscheint auf den ersten Blick nicht so flexibel wie Oqtane, basiert auf .NET 8.0 und wird nicht so aktiv gewartet.
- Umbraco
>Viel Arbeit mit Partials, welche in der Admin Oberfläche geschieht, aber sehr gut dokumentiert. Im großen und ganzen wirkt Umbraco nicht so flexibel.
- DNN / Dot Net Nuke
>Platzhirsch. Kennt man, wird von der DNN Foundation gewartet. Arbeitet mit dem Dotnet Framework, welches nicht unter Linux läuft. Und ein Windows Server würde ich ich nicht einfach so ins Internet, abgesehen von den Lizenzkosten, die das kosten würde.
- Oqtane
>Schlecht dokumentiert, auf den ersten Blick sehr modular und flexibel.
Am Ende haben wir uns für das Oqtane Framework trotz seiner schlechten Dokumentation entschieden.
Im Bereich der Datenbanken musste ich mir ein paar Fragen stellen.
1. Auf welche Art Datenbank setzen wir? SQL, NoSQL, Graph, ...
2. Mit welcher speziellen implementiereung bekommen wir Support und haben Wissen im Team?
3. Ist das auserkorene System kompatibel mit dem CMS auf dem wir aufbauen?
Es war von Anfang an klar, dass es ein SQL basiertes System wird, da wir im Team nur mit SQL-basierten Systemen erfahrungen haben. Außerdem unterstützt unser CMS (Oqtane) nur SQL basierte Systeme. In der Linuxwelt kommen jetzt nur noch ein paar Datenbanken in die Auswahl: PostgreSQL, MySQL / MariaDB, SQLite. Da ist die Wahl auf PostgreSQL gefallen. Grund dafür war meine Vorerfahrung mit diesem DBMS, welche ich im Nebenjob errungen habe.
# Technologie # Technologie
## Entwicklung mit Asp.Net (Was ist Blazor? / Was ist Razor? / Kestrel) ## Entwicklung mit ASP.NET (Was ist Blazor? / Was ist Razor? / Kestrel)
## Was ist Oqtane? Architektur von Oqtane? ## Was ist Oqtane? Architektur von Oqtane?
Oqtane ist ein Framework und CMS zur Entwicklung von Webseiten mithilfe von ASP.Net und Blazor. [^5] Ein Oqtane System besteht aus mehreren Komponenten. Oqtane ist ein Framework und CMS zur Entwicklung von Webseiten mithilfe von ASP.NET und Blazor. [^5] Ein Oqtane-System besteht aus mehreren Komponenten.
In dieser Diplomarbeit fokussieren wir uns hauptsächlich auf `Themes` und `Modules`, aber es gibt auch `Language Packs` und `Pure Extensions`. [^6] In dieser Diplomarbeit fokussieren wir uns hauptsächlich auf `Themes` und `Modules`, aber es gibt auch `Language Packs` und `Pure Extensions`. [^6]
Ein Modul soll neue Funktoinalitäten in das CMS hinzufügen und ein Theme soll die ganze Gestaltung der Website (die Shell) festlegen. [^6] Ein `Module` (Modul) soll neue Funktionalitäten in das CMS hinzufügen und ein `Theme` soll die ganze Gestaltung der Website (die Shell) festlegen. [^6]
[^5]: https://www.oqtane.org/#about [^5]: https://www.oqtane.org/#about
[^6]: https://docs.oqtane.org/dev/extensions/index.html [^6]: https://docs.oqtane.org/dev/extensions/index.html
@@ -18,17 +63,20 @@ Ein Modul soll neue Funktoinalitäten in das CMS hinzufügen und ein Theme soll
### Architektur eines Moduls ### Architektur eines Moduls
Ein Modul in Oqtane besteht aus 4 Projekten. Server, Client, Shared und Package. Ein Modul in Oqtane besteht aus 4 Projekten. Server, Client, Shared und Package.
Im Server Projekt liegt Source Code, welcher Serverseitig ausgeführt werden soll. In der Praxis bedeutet das: alle Repositories, Controller, Manager, Migratoinen und Server-Services (entwickelt nach einem Interface definiert im Client) und Server-Startup logik. Im Server-Projekt liegt Sourcecode, welcher serverseitig ausgeführt werden soll. In der Praxis bedeutet das: alle Repositories, Controller, Manager, Migrationen und Server-Services (entwickelt nach einem Interface definiert im Client) und Server-Startuplogik.
Im Client Projekt liegen Code und Razor Komponenten für den Client. Also Client-Staruplogik, Client-Services (+ Inferfaces dafür, die Services hier sollen lediglich die Server-Services über HTTP aufrufen), Ressource-Dateien (.resx), die Komponenten / das User Interface und die Moduldefinitionen für jedes Modul. Im Client-Projekt liegen Code und Razor-Komponenten für den Client. Also Client-Staruplogik, Client-Services (+ Inferfaces dafür, die Services hier sollen lediglich die Server-Services über HTTP aufrufen), Ressourcendateien (.resx), die Komponenten / das User Interface und die Moduldefinitionen für jedes Modul.
Im Shared Projekt wird geteilter Source-Code abgelegt, der Server und Clientseitig verwendet wird. In der Praxis bleibt's hierbei bei den EntityFramework Modellen zum speichern der Daten im Arbeitsspeicher. Im Shared-Projekt wird geteilter Sourcecode abgelegt, der server- und clientseitig verwendet wird. In der Praxis bleibt es hierbei bei den EntityFramework-Modellen zum Speichern der Daten im Arbeitsspeicher.
Im Package Projekt findet man Skripte zum debuggen und releasen eines Moduls. Und die Nuget Spezifikation. Im Package Projekt findet man Skripte zum Debuggen und Releasen eines Moduls. Und die NuGet-Spezifikation.
- Beim Debug werden die dlls, pdbs und statischen Assets wie Skripte und Stylesheets der 3 anderen Projekte in den bereits gebauten Oqtane.Server `oqtane.framework/oqtane.server/bin/debug/net10.0/...` kopiert. - Beim Debug werden die DLLs, PDBs und statischen Assets wie Skripte und Stylesheets der 3 anderen Projekte in den bereits gebauten Oqtane.Server `oqtane.framework/oqtane.server/bin/debug/net10.0/...` kopiert.
- Beim Release wird ein Nuget Paket erstellt und unter oqtane.framework/oqtane.server/Packages abgelegt. Dort abgelegte Nuget's werden beim nächsten Start des Oqtane Servers installiert (DB Migrationen werden gemacht und nuget's entpackt). - Beim Release wird ein NuGet-Paket erstellt und unter oqtane.framework/oqtane.server/Packages abgelegt. Dort abgelegte NuGet-Pakete werden beim nächsten Start des Oqtane Servers installiert (DB Migrationen werden gemacht und die Pakete entpackt).
## Systemarchitektur (Postgres / Oqtane / Nginx)
In diesem Kapitel erkläre ich wie die ausgewählten Komponenten zusammenspielen. Wir verwenden NginX als Reverse Proxy, welcher bei uns die SSL Terminierung macht. Das Zertifikat, welches verwendet wird, wird von Let's Encrypt bereit gestellt und mitels HTTP-Challanges und dem Certbot auf dem Server aktualisiert und verwaltet. Oqtane selber läuft als Systemd-Service im Kestrel Backend. Kestrel ist ein kleiner Webserver, welcher in das ASP.NET Core Framework eingebaut worden ist. Oqtane (bzw. der ASP.NET Core Server "Kestrel") hört auf der Loopback IP und Port 5000. Damit ist Oqtane nur durch NginX erreichbar. PostgreSQL ist die Datenbank in dem System: Sie hört wieder auf der Loopback IP und Port 5432.
## Systemarchitektur (Postgres / Oqtane / Nginx )
```mermaid ```mermaid
architecture-beta architecture-beta
group server(server)[Server] group server(server)[Server]
@@ -43,19 +91,22 @@ architecture-beta
nginx:R <--> L:oqtane nginx:R <--> L:oqtane
oqtane:R <--> L:db oqtane:R <--> L:db
``` ```
Zusätzlich gab es einen Administrationszugang zu den Servern, welcher über SSH möglich war. Dieser wurde für die Installation und Konfiguration der einzelnen Komponenten verwendet. Bei Hetzner war dieser Zugang nur über eine Wireguard VPN erreichbar. Bei der Schule war dieser Zugang direkt (über einen Hight-Port) möglich. Und bei LiveDesign war dieser Zugang wieder über eine VPN geregelt. Anfänglich mit SSL VPN, später mit einem IPSEC / Strongswan.
## Websockets und HTTP 1.1 ## Websockets und HTTP 1.1
## Dependency injection ## Dependency injection
### Dependency Inversion Principle [^1] ### Dependency Inversion Principle [^1]
Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den 5 `SOLID` Prinzipien in der Softwareentwicklung. Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den fünf `SOLID` Prinzipien in der Softwareentwicklung.
Das DIP unterscheidet zwischen high-level und low-level Modulen. Das DIP unterscheidet zwischen high-level und low-level Modulen.
- Die high-level Module beschreiben die Applikations- / Buisnesslogik, ohne direkt mit den low-level Modulen zu interagieren, sondern lediglich auf abstraktionen. [^3] - Die High-Level-Module beschreiben die Applikations- / Businesslogik, ohne direkt mit den Low-Level-Modulen zu interagieren, sondern lediglich auf Abstraktionen. [^3]
- Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die low-level Implementierung sollen gemäß der Abstraktionsschickt implemetiert werden. [^3] - Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die Low-Level-Implementierung sollen gemäß der Abstraktionsschicht implementiert werden. [^3]
Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph Model. Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph-Modell.
```mermaid ```mermaid
architecture-beta architecture-beta
@@ -67,7 +118,7 @@ architecture-beta
b:R --> L:c b:R --> L:c
``` ```
Bei diesem Beispiel ist die Klasse A ein high-level Modul, welches direkt auf die Klasse B referenziert, was das DI-Prinzip verbietet. Bei diesem Beispiel ist die Klasse A ein high-level Modul, welches direkt auf die Klasse B referenziert, was das DI-Prinzip verbietet.
Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst. Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das Austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst.
```mermaid ```mermaid
architecture-beta architecture-beta
@@ -82,14 +133,14 @@ architecture-beta
b:B --> T:ic b:B --> T:ic
ic:R <-- L:c ic:R <-- L:c
``` ```
Das high-level Modul ruft lediglich eine Abstraktion eines low-level Moduls auf, welche von einem, oder mehreren low-level Modulen implementiert worden ist. Für das high-level Modul ist es hier egal, welches low-level Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar, testbar. Genau diese Modularität macht dependency injection möglich. Das High-Level-Modul ruft lediglich eine Abstraktion eines Low-Level-Moduls auf, welche von einem, oder mehreren Low-Level-Modulen implementiert wurde. Für das High-Level-Modul ist es hier egal, welches Low-Level-Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar und testbar. Genau diese Modularität macht Dependency Injection möglich.
### Microsoft Dependency Injection Framework ### Microsoft Dependency Injection Framework
Dependency Injektion ist in .Net genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [^4] Dependency Injektion ist in .NET genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [^4]
Alle Dependencies werden in einem `service container` zur verwaltung registriert. .Net hat einen eingebauten `service container` (eine Implementierung des `IServiceProvider`). [^4] Alle Dependencies werden in einem `Service-Container` zur Verwaltung registriert. .NET hat einen eingebauten `Service-Container` (eine Implementierung des `IServiceProvider`). [^4]
Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim instanzieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziert und dannach bereitgestellt. [^4] Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden Instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim Instanziieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziiert und dannach bereitgestellt. [^4]
Hier ein Beispiel aus der Dokumentation von Microsoft: [^4] Hier ein Beispiel aus der Dokumentation von Microsoft: [^4]
```c# ```c#
@@ -127,13 +178,13 @@ public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
} }
} }
``` ```
Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessage Writer). Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessageWriter).
Bei Programstart wird zuerst manuell der `Service Container` erstellt, dannach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)). Bei Programmstart wird zuerst manuell der `Service-Container` erstellt, dannach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)).
Mit dem Aufruf von `builder.Build()` wird intern ein Dependency Graph erstellt und mit `host.Run()` wird versucht die Klasse Worker zu instanzieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat wird über den zuvor erstelltem Dependency Graph die implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden. Mit dem Aufruf von `builder.Build()` wird intern ein Dependency Graph erstellt und mit `host.Run()` wird versucht die Klasse Worker zu instanziieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat, wird über den zuvor erstellten Dependency-Graph die Implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziiert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden.
So sehen der Abhängigkeitsgraph bei diesem Beispiel aus. So sieht der Abhängigkeitsgraph bei diesem Beispiel aus.
```mermaid ```mermaid
architecture-beta architecture-beta
@@ -152,6 +203,29 @@ architecture-beta
[^3]: https://www.oodesign.com/dependency-inversion-principle [^3]: https://www.oodesign.com/dependency-inversion-principle
[^4]: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/overview [^4]: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/overview
# Continuous Integration
## Automatisierter Build und Release Prozess mithilfe von Gitea Actions.
Gitea, das Versionskontrollsystem dieser Diplomarbeit, hat einen Continuous-Integration-System eingebaut. Im Kern ist es baugleich zu den GitHub-Pipelines. Man kann im `.gitea/workflow` Ordner `.yml` Dateien ablegen, welche dann das Verhalten der Workflows definieren.
Man kann definieren auf welcher Änderung im Git Repository die Pipeline losgetreten wird (Keyword: `on`) und entweder eigene Kommandos aufreihen, oder auf bestehende `actions` zurückgreifen, welche dann der Reihe nach ausgeführt werden (Keyword: `jobs`).
Die meisten Pipelines sind folgendermaßen Aufgebaut:
Clone -> Checkout -> Submodule Checkout (optional) -> Dependencies einrichten (zum Beispiel das dotnet SDK) -> Build ausführen. -> Release erstellen und Artefakte veröffentlichen (z.B. in Registries). Aber man kann auch andere Dinge tun, z.B. mithilfe von Künstlicher Intelligenz Code und Dokumentation überprüfen.
Anwendungen von Gitea Actions bei dieser Diplomarbeit:
- APT-Package Repository:
> Zum Bauen von Oqtane und allen Modulen, verpacken in ein .deb Paket und in die Registry pushen.
- Interfaces Projekt
> Zum Bauen vom Interfaces-Projekt, verpacken in ein NuGet Paket und in die Registry pushen.
- ursprünglich: oqtane.framework
> Zum bauen und Verpacken in einen Docker Container und in die Registry pushen.
- PM Repository:
> Zum automatischen Überprüfen der Dokumente, unter anderem, mithilfe von KI, wie zum Beispiel Gemini.
# Projektmanagement # Projektmanagement
## Scrum ## Scrum
## YouTrack ## YouTrack
@@ -164,7 +238,7 @@ architecture-beta
## Arbeitszeiteinschätzung (Zeitverzug) ## Arbeitszeiteinschätzung (Zeitverzug)
## Teamleitung (Motivation / Downsizing) ## Teamleitung (Motivation / Downsizing)
## Produktion != Staging ## Produktion != Staging
## Sprints und Meetings (in Zukunft ja Asyncron ## Sprints und Meetings (in Zukunft ja asynchron)
# Modules # Modules
## Mass Mailer ## Mass Mailer

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -14,6 +15,12 @@ import (
"google.golang.org/api/option" "google.golang.org/api/option"
) )
type Suggestion struct {
LineNumber uint64 `json:"line_number"`
Comment string `json:"comment"`
Snippet string `json:"snippet,omitempty"`
}
func main() { func main() {
apiKey := os.Getenv("GEMINI_API_KEY") apiKey := os.Getenv("GEMINI_API_KEY")
token := os.Getenv("GITEA_TOKEN") token := os.Getenv("GITEA_TOKEN")
@@ -55,7 +62,7 @@ func main() {
} }
defer geminiClient.Close() defer geminiClient.Close()
model := geminiClient.GenerativeModel("gemini-1.5-flash") model := geminiClient.GenerativeModel("gemini-3-flash-preview")
// Get PR files // Get PR files
files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{}) files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{})
@@ -63,7 +70,7 @@ func main() {
log.Fatalf("Failed to get PR files: %v", err) log.Fatalf("Failed to get PR files: %v", err)
} }
var reviews []string var giteaComments []gitea.CreatePullReviewComment
for _, file := range files { for _, file := range files {
if !strings.HasSuffix(file.Filename, ".md") { if !strings.HasSuffix(file.Filename, ".md") {
continue continue
@@ -76,24 +83,35 @@ func main() {
continue continue
} }
review, err := getGeminiReview(ctx, model, content) suggestions, err := getGeminiReview(ctx, model, content)
if err != nil { if err != nil {
fmt.Printf("Error getting review for %s: %v\n", file.Filename, err) fmt.Printf("Error getting review for %s: %v\n", file.Filename, err)
continue continue
} }
reviews = append(reviews, fmt.Sprintf("#### Review for `%s`\n\n%s", file.Filename, review)) for _, s := range suggestions {
body := s.Comment
if s.Snippet != "" {
body += fmt.Sprintf("\n\n```suggestion\n%s\n```", s.Snippet)
}
giteaComments = append(giteaComments, gitea.CreatePullReviewComment{
Path: file.Filename,
Body: body,
NewLineNum: int64(s.LineNumber),
})
}
} }
if len(reviews) > 0 { if len(giteaComments) > 0 {
commentBody := "### 🤖 Gemini Writing Review\n\n" + strings.Join(reviews, "\n\n---\n\n") _, _, err = client.CreatePullReview(owner, repo, prNumber, gitea.CreatePullReviewOptions{
_, _, err = client.CreateIssueComment(owner, repo, prNumber, gitea.CreateIssueCommentOption{ State: gitea.ReviewStateRequestChanges,
Body: commentBody, Body: "### 🤖 Gemini Writing Review\n\nI've found some areas for improvement in the documentation. Please see the inline comments below.",
Comments: giteaComments,
}) })
if err != nil { if err != nil {
log.Fatalf("Failed to post PR comment: %v", err) log.Fatalf("Failed to create PR review: %v", err)
} }
fmt.Println("Successfully posted review comments.") fmt.Printf("Successfully created PR review with %d inline comments.\n", len(giteaComments))
} else { } else {
fmt.Println("No Markdown files to review or no suggestions found.") fmt.Println("No Markdown files to review or no suggestions found.")
} }
@@ -113,32 +131,56 @@ func readFile(path string) (string, error) {
return string(content), nil return string(content), nil
} }
func getGeminiReview(ctx context.Context, model *genai.GenerativeModel, content string) (string, error) { func getGeminiReview(ctx context.Context, model *genai.GenerativeModel, content string) ([]Suggestion, error) {
prompt := fmt.Sprintf(` prompt := fmt.Sprintf(`
Review the following Markdown content for spelling errors, grammar mistakes, and style improvements. Review the following Markdown content for spelling errors, grammar mistakes, and style improvements.
Provide your feedback as a list of bullet points. For each point: Analyze the text line by line.
1. Identify the issue.
2. Provide the original text.
3. Suggest an alternative or fix.
Content: For each issue found, provide a suggestion in JSON format with:
- "line_number": The 1-indexed line number where the issue occurs.
- "comment": A brief explanation of the problem and the suggested fix.
- "snippet": The corrected text for that line (optional, but highly recommended for spell/grammar fixes).
Return ONLY a JSON array of suggestions. If no issues are found, return "[]".
Content with line numbers for reference:
%s %s
`, content) `, addLineNumbers(content))
resp, err := model.GenerateContent(ctx, genai.Text(prompt)) resp, err := model.GenerateContent(ctx, genai.Text(prompt))
if err != nil { if err != nil {
return "", err return nil, err
} }
if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 { if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
return "No suggestions found.", nil return nil, nil
} }
var result strings.Builder var rawJS strings.Builder
for _, part := range resp.Candidates[0].Content.Parts { for _, part := range resp.Candidates[0].Content.Parts {
if text, ok := part.(genai.Text); ok { if text, ok := part.(genai.Text); ok {
result.WriteString(string(text)) rawJS.WriteString(string(text))
} }
} }
return result.String(), nil
// Clean up markdown code blocks if present
js := strings.TrimSpace(rawJS.String())
js = strings.TrimPrefix(js, "```json")
js = strings.TrimSuffix(js, "```")
js = strings.TrimSpace(js)
var suggestions []Suggestion
if err := json.Unmarshal([]byte(js), &suggestions); err != nil {
return nil, fmt.Errorf("failed to unmarshal suggestions: %v (raw response: %s)", err, js)
}
return suggestions, nil
}
func addLineNumbers(text string) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
lines[i] = fmt.Sprintf("%d: %s", i+1, line)
}
return strings.Join(lines, "\n")
} }

97
scripts/wordcount/main.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
)
func main() {
token := os.Getenv("GITEA_TOKEN")
baseURL := os.Getenv("GITEA_URL")
repoFullName := os.Getenv("GITHUB_REPOSITORY")
prNumberStr := os.Getenv("PR_NUMBER")
if token == "" || repoFullName == "" || prNumberStr == "" {
log.Fatal("Missing required environment variables: GITEA_TOKEN, GITHUB_REPOSITORY, PR_NUMBER")
}
if baseURL == "" {
baseURL = "https://gitea.com"
}
prNumber, err := strconv.ParseInt(prNumberStr, 10, 64)
if err != nil {
log.Fatalf("Invalid PR_NUMBER: %v", err)
}
repoParts := strings.Split(repoFullName, "/")
if len(repoParts) != 2 {
log.Fatalf("Invalid GITHUB_REPOSITORY format: %s", repoFullName)
}
owner, repo := repoParts[0], repoParts[1]
client, err := gitea.NewClient(baseURL, gitea.SetToken(token))
if err != nil {
log.Fatalf("Failed to create Gitea client: %v", err)
}
// Get PR files
files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{})
if err != nil {
log.Fatalf("Failed to get PR files: %v", err)
}
var counts []string
for _, file := range files {
if !strings.HasSuffix(file.Filename, ".md") {
continue
}
fmt.Printf("Reviewing file: %s\n", file.Filename)
content, err := readFile(file.Filename)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", file.Filename, err)
continue
}
count := countWords(content)
counts = append(counts, fmt.Sprintf("#### Word count for `%s`\n\nWord count: %d", file.Filename, count))
}
if len(counts) > 0 {
commentBody := "### 🤖 Word Count Report\n\n" + strings.Join(counts, "\n\n---\n\n")
_, _, err = client.CreateIssueComment(owner, repo, prNumber, gitea.CreateIssueCommentOption{
Body: commentBody,
})
if err != nil {
log.Fatalf("Failed to post PR comment: %v", err)
}
fmt.Println("Successfully posted review comments.")
} else {
fmt.Println("No Markdown files to review or no suggestions found.")
}
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(content), nil
}
func countWords(text string) int {
return len(strings.Fields(text))
}