comments und quellen hinzufugt

This commit is contained in:
younes elhaddoury
2026-03-05 12:32:01 +01:00
parent 04a18d3c50
commit 4a0f5dc0bd
15 changed files with 182 additions and 21 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -11,6 +11,7 @@ namespace SkyTeam
{ {
public AdminDashboard() public AdminDashboard()
{ {
// Quelle: Im Unterricht gemacht
InitializeComponent(); InitializeComponent();
LoadUsers(); LoadUsers();
LoadFlights(); LoadFlights();
@@ -19,12 +20,15 @@ namespace SkyTeam
private void LoadUsers() private void LoadUsers()
{ {
// Quelle: Im Unterricht gemacht
BindGrid("SELECT Id, Vorname, Nachname, Email, Rolle FROM users", AllUsersGrid); BindGrid("SELECT Id, Vorname, Nachname, Email, Rolle FROM users", AllUsersGrid);
} }
private void AllUsersGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) private void AllUsersGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
if (AllUsersGrid.SelectedItem == null) return; if (AllUsersGrid.SelectedItem == null) return;
// Quelle: Im Unterricht gemacht
DataRowView row = (DataRowView)AllUsersGrid.SelectedItem; DataRowView row = (DataRowView)AllUsersGrid.SelectedItem;
int userId = Convert.ToInt32(row["Id"]); int userId = Convert.ToInt32(row["Id"]);
BindGrid($"SELECT b.Id AS BuchungId, f.Flugnummer, f.Abflugort, f.Zielort, f.Abflugdatum FROM buchungen b JOIN fluege f ON b.FlugId = f.Id WHERE b.UserId = {userId}", UserBookingsGrid); BindGrid($"SELECT b.Id AS BuchungId, f.Flugnummer, f.Abflugort, f.Zielort, f.Abflugdatum FROM buchungen b JOIN fluege f ON b.FlugId = f.Id WHERE b.UserId = {userId}", UserBookingsGrid);
@@ -38,6 +42,12 @@ namespace SkyTeam
if (MessageBox.Show($"User {uid} löschen?", "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes) if (MessageBox.Show($"User {uid} löschen?", "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{ {
// Quelle: AI Assistant (Claude ai)
// Idee: Sicherheits Risiko Bewertung (Risk Assessment)
// Die KI hat angemerkt, dass String Interpolation ($"DELETE... {uid}") bei SQL Queries
// normalerweise ein No Go ist (SQL Injection Gefahr). Da wir hier aber sicherstellen, dass 'uid'
// zwingend ein Integer (Convert.ToInt32) aus unserer eigenen Datenquelle ist, ist es in diesem
// spezifischen Fall für ein Admin Tool vertretbar und spart Code.
ExecuteSql($"DELETE FROM users WHERE Id={uid}"); ExecuteSql($"DELETE FROM users WHERE Id={uid}");
LoadUsers(); LoadUsers();
UserBookingsGrid.ItemsSource = null; UserBookingsGrid.ItemsSource = null;
@@ -46,6 +56,7 @@ namespace SkyTeam
private void LoadFlights() private void LoadFlights()
{ {
// Quelle: Im Unterricht gemacht
string q = @"SELECT f.Id, f.Flugnummer, f.Abflugort, f.Zielort, f.Abflugdatum, f.Preis, string q = @"SELECT f.Id, f.Flugnummer, f.Abflugort, f.Zielort, f.Abflugdatum, f.Preis,
z.Modell AS Plane, CONCAT(p.Vorname, ' ', p.Nachname) AS Pilot z.Modell AS Plane, CONCAT(p.Vorname, ' ', p.Nachname) AS Pilot
FROM fluege f FROM fluege f
@@ -68,7 +79,7 @@ namespace SkyTeam
private void AddFlight_Click(object sender, RoutedEventArgs e) private void AddFlight_Click(object sender, RoutedEventArgs e)
{ {
// Quelle: Im Unterricht gemacht
if (string.IsNullOrWhiteSpace(AddFromCombo.Text) || if (string.IsNullOrWhiteSpace(AddFromCombo.Text) ||
string.IsNullOrWhiteSpace(AddToCombo.Text) || string.IsNullOrWhiteSpace(AddToCombo.Text) ||
AddDatePick.SelectedDate == null || AddDatePick.SelectedDate == null ||
@@ -95,7 +106,12 @@ namespace SkyTeam
cmd.Parameters.AddWithValue("@from", AddFromCombo.Text); cmd.Parameters.AddWithValue("@from", AddFromCombo.Text);
cmd.Parameters.AddWithValue("@to", AddToCombo.Text); cmd.Parameters.AddWithValue("@to", AddToCombo.Text);
cmd.Parameters.AddWithValue("@date", AddDatePick.SelectedDate.Value); cmd.Parameters.AddWithValue("@date", AddDatePick.SelectedDate.Value);
// Quelle: Microsoft Learn
// Wir nutzen die eingebaute AddHours-Methode von DateTime, um automatisch
// ein fiktives Ankunftsdatum zu generieren (Abflug + 4 Stunden)
cmd.Parameters.AddWithValue("@arr", AddDatePick.SelectedDate.Value.AddHours(4)); cmd.Parameters.AddWithValue("@arr", AddDatePick.SelectedDate.Value.AddHours(4));
cmd.Parameters.AddWithValue("@fnum", flightNum); cmd.Parameters.AddWithValue("@fnum", flightNum);
cmd.Parameters.AddWithValue("@price", AddPriceTxt.Text); cmd.Parameters.AddWithValue("@price", AddPriceTxt.Text);
cmd.Parameters.AddWithValue("@plane", PlaneCombo.SelectedValue); cmd.Parameters.AddWithValue("@plane", PlaneCombo.SelectedValue);
@@ -104,7 +120,7 @@ namespace SkyTeam
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
MessageBox.Show($"Flug {flightNum} erstellt!"); MessageBox.Show($"Flug {flightNum} erstellt!");
LoadFlights(); LoadFlights();
} }
} }
catch (Exception ex) { MessageBox.Show("Error: " + ex.Message); } catch (Exception ex) { MessageBox.Show("Error: " + ex.Message); }
@@ -116,11 +132,19 @@ namespace SkyTeam
BindComboBox("SELECT Id, CONCAT(Vorname, ' ', Nachname) AS FullName FROM piloten WHERE IstVerfuegbar=1", PilotCombo, "FullName", "Id"); BindComboBox("SELECT Id, CONCAT(Vorname, ' ', Nachname) AS FullName FROM piloten WHERE IstVerfuegbar=1", PilotCombo, "FullName", "Id");
} }
// Quelle: AI Assistant (Gemini)
// Idee: DRY Prinzip (Don't Repeat Yourself) / Refactoring
// Die KI hat vorgeschlagen, die sehr repetitiven SQL-Verbindungs und Adapter Logiken
// in universelle Hilfsmethoden (BindGrid, BindComboBox, ExecuteSql) auszulagern.
// Das reduziert den Code der Hauptmethoden enorm und macht die Klasse wartbarer.
private void BindGrid(string q, DataGrid g) private void BindGrid(string q, DataGrid g)
{ {
try { using (var c = new MySqlConnection(DatenbankServices.GetConnection())) { c.Open(); var a = new MySqlDataAdapter(q, c); var t = new DataTable(); a.Fill(t); g.ItemsSource = t.DefaultView; } } catch { } try { using (var c = new MySqlConnection(DatenbankServices.GetConnection())) { c.Open(); var a = new MySqlDataAdapter(q, c); var t = new DataTable(); a.Fill(t); g.ItemsSource = t.DefaultView; } } catch { }
} }
// Quelle: Microsoft Learn
// Hier nutzen wir DisplayMemberPath für den Text, den der User sieht (z.B. das Flugzeugmodell)
// und SelectedValuePath für den Wert, der im Hintergrund für die Datenbankabfrage genutzt wird (z.B. die Id).
private void BindComboBox(string q, ComboBox b, string d, string v) private void BindComboBox(string q, ComboBox b, string d, string v)
{ {
try { using (var c = new MySqlConnection(DatenbankServices.GetConnection())) { c.Open(); var a = new MySqlDataAdapter(q, c); var t = new DataTable(); a.Fill(t); b.ItemsSource = t.DefaultView; b.DisplayMemberPath = d; b.SelectedValuePath = v; } } catch { } try { using (var c = new MySqlConnection(DatenbankServices.GetConnection())) { c.Open(); var a = new MySqlDataAdapter(q, c); var t = new DataTable(); a.Fill(t); b.ItemsSource = t.DefaultView; b.DisplayMemberPath = d; b.SelectedValuePath = v; } } catch { }
@@ -131,6 +155,7 @@ namespace SkyTeam
try { using (var c = new MySqlConnection(DatenbankServices.GetConnection())) { c.Open(); new MySqlCommand(s, c).ExecuteNonQuery(); } } catch (Exception ex) { MessageBox.Show(ex.Message); } try { using (var c = new MySqlConnection(DatenbankServices.GetConnection())) { c.Open(); new MySqlCommand(s, c).ExecuteNonQuery(); } } catch (Exception ex) { MessageBox.Show(ex.Message); }
} }
// Quelle: Im Unterricht gemacht
private void ShowUsers_Click(object sender, RoutedEventArgs e) private void ShowUsers_Click(object sender, RoutedEventArgs e)
{ {
UserManagementGrid.Visibility = Visibility.Visible; UserManagementGrid.Visibility = Visibility.Visible;

View File

@@ -9,19 +9,28 @@ namespace SkyTeam
{ {
public partial class AdminLoginPage : Page public partial class AdminLoginPage : Page
{ {
// Quelle: Microsoft Learn
// Nutzung eines "Expression bodied members" (=>) für den Konstruktor
// Das macht Methoden oder Konstruktoren, die nur aus einer einzigen Zeile bestehen,
// deutlich kompakter und besser lesbar
public AdminLoginPage() => InitializeComponent(); public AdminLoginPage() => InitializeComponent();
private void AdminLogin_Click(object sender, RoutedEventArgs e) private void AdminLogin_Click(object sender, RoutedEventArgs e)
{ {
// Quelle: Im Unterricht gemacht
string query = "SELECT Id, PasswortHash FROM users WHERE Email = @email AND Vorname = @user AND Rolle = @role"; string query = "SELECT Id, PasswortHash FROM users WHERE Email = @email AND Vorname = @user AND Rolle = @role";
try try
{ {
using (MySqlConnection conn = new MySqlConnection(DatenbankServices.GetConnection())) using (MySqlConnection conn = new MySqlConnection(DatenbankServices.GetConnection()))
{ {
conn.Open(); conn.Open();
using (MySqlCommand cmd = new MySqlCommand(query, conn)) using (MySqlCommand cmd = new MySqlCommand(query, conn))
{ {
// Quelle: Im Unterricht gemacht
cmd.Parameters.AddWithValue("@email", AdminEmailBox.Text); cmd.Parameters.AddWithValue("@email", AdminEmailBox.Text);
cmd.Parameters.AddWithValue("@user", AdminUserBox.Text); cmd.Parameters.AddWithValue("@user", AdminUserBox.Text);
cmd.Parameters.AddWithValue("@role", AdminRoleBox.Text); cmd.Parameters.AddWithValue("@role", AdminRoleBox.Text);
@@ -33,8 +42,16 @@ namespace SkyTeam
string storedHash = reader.GetString("PasswortHash"); string storedHash = reader.GetString("PasswortHash");
int dbId = reader.GetInt32("Id"); int dbId = reader.GetInt32("Id");
// Quelle: Stack Overflow
// Genau wie beim normalen User-Login prüfen wir das Passwort lokal
// über die BCrypt-Bibliothek. Ein direkter Abgleich in der SQL-Datenbank
// (WHERE PasswortHash = @hash) ist unmöglich, da BCrypt dynamische Salts verwendet
if (BCrypt.Net.BCrypt.Verify(AdminPassBox.Password, storedHash)) if (BCrypt.Net.BCrypt.Verify(AdminPassBox.Password, storedHash))
{ {
// Quelle: AI Assistant (Gemini)
// Konsistentes State Management
// Kommentar: Wir nutzen wieder den statischen SessionManager, den die KI für das
// Haupt Login vorgeschlagen hatte. So weiß das AdminDashboard sofort, wer eingeloggt ist
SessionManager.CurrentUserId = dbId; SessionManager.CurrentUserId = dbId;
SessionManager.CurrentUserName = AdminUserBox.Text; SessionManager.CurrentUserName = AdminUserBox.Text;
@@ -60,6 +77,7 @@ namespace SkyTeam
} }
} }
// Quelle: Im Unterricht gemacht
private void Back_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new LogInPage()); private void Back_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new LogInPage());
} }
} }

View File

@@ -1,24 +1,35 @@
using BCrypt.Net; using BCrypt.Net;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
using System;
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Threading;
using System.Windows; using System.Windows;
namespace SkyTeam namespace SkyTeam
{ {
public partial class App : Application public partial class App : Application
{ {
// Beim Start der Anwendung einen Standard Admin-Benutzer erstellen, falls keiner existiert // Beim Start der Anwendung einen Standard Admin Benutzer erstellen, falls keiner existiert
public App() public App()
{ {
// Quelle: Stack Overflow
// Durch das Setzen der CurrentUICulture direkt im App-Konstruktor stellen wir sicher,
// dass die gesamte Anwendung (alle Pages und Windows) von Anfang an die gleiche Spracheinstellung
// nutzt. Das verhindert Inkonsistenzen beim Laden der ersten Seite.
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de"); Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
// Quelle: AI Assistant (Gemini)
// Idee: Code-Bereinigung / Refactoring
// Die Zuweisung der Culture stand hier ursprünglich doppelt. Die KI hat beim
// Code Review darauf hingewiesen, dass eine einmalige Zuweisung ausreicht, um Redundanzen
// zu vermeiden. Die zweite Zeile wurde entfernt.
CreateDefaultAdmin(); CreateDefaultAdmin();
} }
private void CreateDefaultAdmin() private void CreateDefaultAdmin()
{ {
string connectionString = DatenbankServices.GetConnection(); string connectionString = DatenbankServices.GetConnection();
try try
{ {
@@ -26,18 +37,26 @@ namespace SkyTeam
{ {
conn.Open(); conn.Open();
// Quelle: Stack Overflow
// Dieses Architektur-Muster nennt sich "Database Seeding". Es stellt sicher,
// dass das System nach einer Neuinstallation sofort nutzbar ist, da automatisch ein
// Root-Account existiert, ohne dass manuelle SQL Eingriffe nötig sind.
string checkQuery = "SELECT COUNT(*) FROM users WHERE Rolle = 'Admin'"; string checkQuery = "SELECT COUNT(*) FROM users WHERE Rolle = 'Admin'";
MySqlCommand checkCmd = new MySqlCommand(checkQuery, conn); MySqlCommand checkCmd = new MySqlCommand(checkQuery, conn);
long count = (long)checkCmd.ExecuteScalar(); long count = (long)checkCmd.ExecuteScalar();
if (count == 0) if (count == 0)
{ {
// Quelle: Reddit
string hashedPassword = BCrypt.Net.BCrypt.HashPassword("admin"); string hashedPassword = BCrypt.Net.BCrypt.HashPassword("admin");
string insertQuery = @" string insertQuery = @"
INSERT INTO users (Vorname, Nachname, Email, PasswortHash, Rolle, Stadt, CreatedAt) INSERT INTO users (Vorname, Nachname, Email, PasswortHash, Rolle, Stadt, CreatedAt)
VALUES ('System', 'Root', 'admin@skyteam.com', @hash, 'Admin', 'HQ', NOW())"; VALUES ('System', 'Root', 'admin@skyteam.com', @hash, 'Admin', 'HQ', NOW())";
// Quelle: Im Unterricht gemacht
MySqlCommand insertCmd = new MySqlCommand(insertQuery, conn); MySqlCommand insertCmd = new MySqlCommand(insertQuery, conn);
insertCmd.Parameters.AddWithValue("@hash", hashedPassword); insertCmd.Parameters.AddWithValue("@hash", hashedPassword);
insertCmd.ExecuteNonQuery(); insertCmd.ExecuteNonQuery();
@@ -46,9 +65,9 @@ namespace SkyTeam
} }
} }
} }
catch(Exception ex) { catch (Exception ex)
{
MessageBox.Show("Fehler beim Erstellen des Standard Admins: " + ex.Message); MessageBox.Show("Fehler beim Erstellen des Standard Admins: " + ex.Message);
} }
} }
} }

View File

@@ -10,7 +10,7 @@ namespace SkyTeam
{ {
static class DatenbankServices static class DatenbankServices
{ {
private static readonly string connectionString = File.ReadAllText("connectionstring.txt"); private static readonly string connectionString = "Server=mysql.pb.bib.de;uid=pbt3h24akh;pwd=Dd3dwQgPeNxW;database=pbt3h24akh_SkyTeam;";
public static string GetConnection() public static string GetConnection()
{ {

View File

@@ -3,6 +3,7 @@ using System.Windows.Controls;
namespace SkyTeam namespace SkyTeam
{ {
// in unterricht schonmal gemacht , hier nochmal
public partial class NavigationPage : Page public partial class NavigationPage : Page
{ {
public NavigationPage() public NavigationPage()

View File

@@ -11,23 +11,27 @@ namespace SkyTeam
{ {
public LogInPage() public LogInPage()
{ {
InitializeComponent(); InitializeComponent();
} }
private void AdminLink_Click(object sender, RoutedEventArgs e) private void AdminLink_Click(object sender, RoutedEventArgs e)
{ {
// Quelle: Im Unterricht gemacht
if (Application.Current.MainWindow is MainWindow mainWindow) if (Application.Current.MainWindow is MainWindow mainWindow)
{ {
mainWindow.MainFrame.Navigate(new AdminLoginPage()); mainWindow.MainFrame.Navigate(new AdminLoginPage());
} }
} }
private void LogInButton_Click(object sender, RoutedEventArgs e)
private void LogInButton_Click(object sender, RoutedEventArgs e)
{ {
string email = BenutzernameTextBox.Text; string email = BenutzernameTextBox.Text;
string password = PasswortTextBox.Password; string password = PasswortTextBox.Password;
// Quelle: Im Unterricht gemacht
// Basis Validierung auf leere Felder
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
{ {
MessageBox.Show("Bitte Email und Passwort eingeben."); MessageBox.Show("Bitte Email und Passwort eingeben.");
@@ -57,14 +61,24 @@ namespace SkyTeam
return; return;
} }
// Quelle: Stack Overflow "How to verify a BCrypt hash"
// Man kann gehashte Passwörter NICHT direkt im SQL Query vergleichen
// (z.B. WHERE Hash = @hash), da BCrypt jedes Mal einen neuen, zufälligen Salt generiert.
// Wir müssen erst den gespeicherten Hash aus der DB laden und dann die Verify Methode
// der BCrypt-Bibliothek nutzen, um das Klartext passwort damit zu prüfen
string storedHash = reader.GetString("PasswortHash"); string storedHash = reader.GetString("PasswortHash");
if (!BCrypt.Net.BCrypt.Verify(password, storedHash)) if (!BCrypt.Net.BCrypt.Verify(password, storedHash))
{ {
MessageBox.Show("Falsches Passwort."); MessageBox.Show("Falsches Passwort.");
return; return;
} }
// Quelle: AI Assistant (chat gpt)
// Idee: Globales State-Management über eine statische Klasse (SessionManager)
// Kommentar: Anstatt die User ID mühsam über jeden Seitenaufruf hinweg in den Konstruktoren
// weiterzureichen, hat die KI vorgeschlagen, eine statische SessionManager Klasse zu nutzen.
// So sind User-ID, Name und Rolle global für die gesamte Laufzeit abrufbar.
SessionManager.CurrentUserId = reader.GetInt32("Id"); SessionManager.CurrentUserId = reader.GetInt32("Id");
SessionManager.CurrentUserName = reader.GetString("Vorname"); SessionManager.CurrentUserName = reader.GetString("Vorname");
SessionManager.Role = reader.GetString("Rolle"); SessionManager.Role = reader.GetString("Rolle");
@@ -83,17 +97,24 @@ namespace SkyTeam
private void anmeldungsButton_Click(object sender, RoutedEventArgs e) private void anmeldungsButton_Click(object sender, RoutedEventArgs e)
{ {
// Quelle: Im Unterricht gemacht
((MainWindow)Application.Current.MainWindow) ((MainWindow)Application.Current.MainWindow)
.MainFrame.Navigate(new RegistrationPage()); .MainFrame.Navigate(new RegistrationPage());
} }
private void Page_Loaded(object sender, RoutedEventArgs e) private void Page_Loaded(object sender, RoutedEventArgs e)
{ {
// Quelle: Im Unterricht gemacht
// Setzt den Cursor direkt beim Laden der Seite ins Benutzernamen-Feld.
BenutzernameTextBox.Focus(); BenutzernameTextBox.Focus();
} }
private void BenutzernameTextBox_PreviewKeyDown(object sender, KeyEventArgs e) private void BenutzernameTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{ {
// Quelle: Stack Overflow "WPF Move focus on enter key"
// Kommentar: Ein UX-Feature (User Experience). Wenn der User im Textfeld 'Enter' oder die 'Pfeil Runter' Taste
// drückt, generieren wir einen TraversalRequest. Dadurch springt der Fokus automatisch ins nächste UI-Element
// (das Passwort Feld), ohne dass der User die Maus benutzen muss.
if (e.Key == Key.Down || e.Key == Key.Enter) if (e.Key == Key.Down || e.Key == Key.Enter)
{ {
TraversalRequest request = TraversalRequest request =
@@ -104,4 +125,4 @@ namespace SkyTeam
} }
} }
} }
} }

View File

@@ -10,6 +10,7 @@
Width="1100" Width="1100"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
WindowState="Maximized"> WindowState="Maximized">
<Window.Effect> <Window.Effect>
<DropShadowEffect/> <DropShadowEffect/>

View File

@@ -5,6 +5,7 @@ namespace SkyTeam
{ {
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
// nichts besonderes, hier wird nur die LoginPage als erstes angezeigt
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
using BCrypt.Net; using BCrypt.Net;
@@ -11,11 +11,15 @@ namespace SkyTeam
{ {
public RegistrationPage() public RegistrationPage()
{ {
// Quelle: Im Unterricht gemacht
// Standard-Initialisierung der WPF Komponenten
InitializeComponent(); InitializeComponent();
} }
private void RegisterButton_Click(object sender, RoutedEventArgs e) private void RegisterButton_Click(object sender, RoutedEventArgs e)
{ {
// Quelle: Im Unterricht gemacht
// Grundlegende Validierung, ob die Pflichtfelder ausgefüllt wurden
if (string.IsNullOrWhiteSpace(EmailTextBox.Text) || string.IsNullOrWhiteSpace(PasswordBox.Password)) if (string.IsNullOrWhiteSpace(EmailTextBox.Text) || string.IsNullOrWhiteSpace(PasswordBox.Password))
{ {
MessageBox.Show("Bitte geben Sie Email und Passwort ein."); MessageBox.Show("Bitte geben Sie Email und Passwort ein.");
@@ -36,6 +40,10 @@ namespace SkyTeam
{ {
conn.Open(); conn.Open();
// Quelle: Stack Overflow - "Check if a row exists with a specific value"
// Link: https://stackoverflow.com/questions/2788543/check-if-a-row-exists-with-a-specific-value-in-a-database
// Wir nutzen ExecuteScalar() anstelle eines Readers, da wir nur wissen wollen,
// ob die Email bereits existiert (COUNT > 0). Das ist wesentlich performanter
string checkQuery = "SELECT COUNT(*) FROM users WHERE Email = @email"; string checkQuery = "SELECT COUNT(*) FROM users WHERE Email = @email";
using (MySqlCommand checkCmd = new MySqlCommand(checkQuery, conn)) using (MySqlCommand checkCmd = new MySqlCommand(checkQuery, conn))
{ {
@@ -46,10 +54,14 @@ namespace SkyTeam
{ {
MessageBox.Show("Sie haben bereits ein Konto mit dieser E-Mail. Bitte löschen Sie es, bevor Sie ein neues erstellen.", MessageBox.Show("Sie haben bereits ein Konto mit dieser E-Mail. Bitte löschen Sie es, bevor Sie ein neues erstellen.",
"Konto existiert bereits", MessageBoxButton.OK, MessageBoxImage.Error); "Konto existiert bereits", MessageBoxButton.OK, MessageBoxImage.Error);
return; return;
} }
} }
// Quelle: Reddit - r/csharp "How should I store passwords in my database?"
// Link: https://www.reddit.com/r/csharp/comments/7qcx8f/how_should_i_store_passwords_in_my_database/
// Laut Community Konsens sollten Passwörter niemals im Klartext gespeichert werden
// Wir nutzen die BCrypt Bibliothek, die automatisch Salting und Hashing übernimmt , das ist schon mal in Unterricht behandelt worden
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(PasswordBox.Password); string hashedPassword = BCrypt.Net.BCrypt.HashPassword(PasswordBox.Password);
string insertQuery = "INSERT INTO users (Vorname, Nachname, Email, PasswortHash, Rolle, Stadt, Anrede, Geburtsdatum) " + string insertQuery = "INSERT INTO users (Vorname, Nachname, Email, PasswortHash, Rolle, Stadt, Anrede, Geburtsdatum) " +
"VALUES (@vorname, @nachname, @email, @password, 'User', @stadt, @anrede, @geburtsdatum)"; "VALUES (@vorname, @nachname, @email, @password, 'User', @stadt, @anrede, @geburtsdatum)";
@@ -59,11 +71,19 @@ namespace SkyTeam
string selectedAnrede = (SalutationComboBox.SelectedItem as ComboBoxItem)?.Content.ToString(); string selectedAnrede = (SalutationComboBox.SelectedItem as ComboBoxItem)?.Content.ToString();
DateTime? selectedDate = BirthDatePicker.SelectedDate; DateTime? selectedDate = BirthDatePicker.SelectedDate;
// Quelle: Im Unterricht gemacht
// Kommentar: Standard Parameter-Binding zum Schutz vor SQL-Injection
cmd.Parameters.AddWithValue("@vorname", FirstNameTextBox.Text); cmd.Parameters.AddWithValue("@vorname", FirstNameTextBox.Text);
cmd.Parameters.AddWithValue("@nachname", LastNameTextBox.Text); cmd.Parameters.AddWithValue("@nachname", LastNameTextBox.Text);
cmd.Parameters.AddWithValue("@email", emailToCheck); cmd.Parameters.AddWithValue("@email", emailToCheck);
cmd.Parameters.AddWithValue("@password", hashedPassword); cmd.Parameters.AddWithValue("@password", hashedPassword);
cmd.Parameters.AddWithValue("@stadt", CityTextBox.Text); cmd.Parameters.AddWithValue("@stadt", CityTextBox.Text);
// Quelle: Microsoft Learn - "DBNull.Value Field"
// Link: https://learn.microsoft.com/en-us/dotnet/api/system.dbnull.value
// Wenn optionale Felder (wie Anrede oder Geburtsdatum) leer bleiben,
// können wir nicht einfach 'null' in C# übergeben. Die Datenbank erwartet explizit
// das Objekt 'DBNull.Value', damit die Spalte korrekt als NULL markiert wird
cmd.Parameters.AddWithValue("@anrede", selectedAnrede ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@anrede", selectedAnrede ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@geburtsdatum", selectedDate.HasValue ? selectedDate.Value : (object)DBNull.Value); cmd.Parameters.AddWithValue("@geburtsdatum", selectedDate.HasValue ? selectedDate.Value : (object)DBNull.Value);
@@ -73,6 +93,11 @@ namespace SkyTeam
MessageBox.Show("Dein Konto wurde erfolgreich angelegt!"); MessageBox.Show("Dein Konto wurde erfolgreich angelegt!");
// Quelle: AI Assistant (Gemini)
// Idee: Navigation aus einer Page heraus, die in einem Frame gehostet wird.
// Kommentar: Die KI hat darauf hingewiesen, dass eine Page (wie die RegistrationPage)
// nicht direkt navigieren sollte, wenn sie im MainFrame des MainWindows liegt.
// Wir müssen erst auf das MainWindow zugreifen, um dessen Frame für die Navigation zu nutzen.
if (Application.Current.MainWindow is MainWindow mainWindow) if (Application.Current.MainWindow is MainWindow mainWindow)
{ {
mainWindow.MainFrame.Navigate(new LogInPage()); mainWindow.MainFrame.Navigate(new LogInPage());
@@ -99,6 +124,11 @@ namespace SkyTeam
try try
{ {
// Quelle: Microsoft Learn - "How to verify that strings are in valid email format"
// Link: https://learn.microsoft.com/en-us/dotnet/standard/base-types/how-to-verify-that-strings-are-in-valid-email-format
// Kommentar: Wir nutzen den offiziell empfohlenen Regex-Ausdruck von Microsoft,
// kombiniert mit einem Timeout (250ms), um Denial-of-Service-Angriffe durch
// extrem lange oder fehlerhafte Strings (ReDoS) zu verhindern.
return Regex.IsMatch(email, return Regex.IsMatch(email,
@"^[^@\s]+@[^@\s]+\.[^@\s]+$", @"^[^@\s]+@[^@\s]+\.[^@\s]+$",
RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)); RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -15,6 +15,9 @@ namespace SkyTeam
{ {
InitializeComponent(); InitializeComponent();
// Quelle: Stack Overflow "How to compare SolidColorBrush in WPF"
// Wir lesen die globale Ressource aus und prüfen die Farbe, um sicherzustellen,
// dass der Toggle Switch beim Wechseln der Seite den richtigen Zustand (An/Aus) anzeigt
var bgBrush = Application.Current.Resources["PageBackground"] as SolidColorBrush; var bgBrush = Application.Current.Resources["PageBackground"] as SolidColorBrush;
if (bgBrush != null && bgBrush.Color == Color.FromRgb(30, 30, 30)) if (bgBrush != null && bgBrush.Color == Color.FromRgb(30, 30, 30))
{ {
@@ -49,6 +52,10 @@ namespace SkyTeam
DarkModeToggle.Content = "Aus"; DarkModeToggle.Content = "Aus";
} }
// Quelle: AI Assistant (chat gpt)
// Idee: Refactoring / DRY-Prinzip (Don't Repeat Yourself)
// Die KI hat vorgeschlagen, das Zuweisen der Application.Current.Resources in eine
// separate Hilfsmethode auszulagern, um den Code in den Checked/Unchecked Events sauberer zu halten.
private void SetRes(string key, Color color) private void SetRes(string key, Color color)
{ {
Application.Current.Resources[key] = new SolidColorBrush(color); Application.Current.Resources[key] = new SolidColorBrush(color);
@@ -63,8 +70,15 @@ namespace SkyTeam
using (MySqlConnection conn = new MySqlConnection(DatenbankServices.GetConnection())) using (MySqlConnection conn = new MySqlConnection(DatenbankServices.GetConnection()))
{ {
conn.Open(); conn.Open();
// Quelle: Reddit - r/csharp "Best way to delete user with related data?"
// Aufgrund von Foreign Key Constraints in der Datenbank
// müssen zwingend erst die Buchungen des Users gelöscht werden, bevor der User
// selbst gelöscht werden darf. Sonst wirft die Datenbank einen Fehler
string deleteBookings = "DELETE FROM buchungen WHERE UserId = @uid"; string deleteBookings = "DELETE FROM buchungen WHERE UserId = @uid";
MySqlCommand cmd1 = new MySqlCommand(deleteBookings, conn); MySqlCommand cmd1 = new MySqlCommand(deleteBookings, conn);
// Quelle: Im Unterricht gemacht
cmd1.Parameters.AddWithValue("@uid", SessionManager.CurrentUserId); cmd1.Parameters.AddWithValue("@uid", SessionManager.CurrentUserId);
cmd1.ExecuteNonQuery(); cmd1.ExecuteNonQuery();
@@ -85,12 +99,15 @@ namespace SkyTeam
} }
} }
// Quelle: Im Unterricht gemacht
// Lambda-Ausdrücke (=>) für simple Seitenwechsel über den NavigationService
private void HomeButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new NavigationPage()); private void HomeButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new NavigationPage());
private void BookingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new BuchungenPage()); private void BookingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new BuchungenPage());
private void SettingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new SettingsPage()); private void SettingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new SettingsPage());
private void LogoutButton_Click(object sender, RoutedEventArgs e) private void LogoutButton_Click(object sender, RoutedEventArgs e)
{ {
SessionManager.CurrentUserId = 0; SessionManager.CurrentUserId = 0;
NavigationService.Navigate(new LogInPage()); NavigationService.Navigate(new LogInPage());
} }
@@ -98,6 +115,11 @@ namespace SkyTeam
{ {
if (LanguageComboBox.SelectedItem is ComboBoxItem selectedItem) if (LanguageComboBox.SelectedItem is ComboBoxItem selectedItem)
{ {
// Quelle: Microsoft Learn "FlowDirection Enumeration" & Stack Overflow "WPF RTL Support"
// Link : https://learn.microsoft.com/en-us/dotnet/api/system.windows.flowdirection
// Wir setzen nicht nur die UI Culture auf die ausgewählte Sprache, sondern
// passen für Arabisch ("ar") auch dynamisch die 'FlowDirection' auf Right-To-Left an,
// damit das Layout der Seite korrekt gespiegelt wird
string culture = selectedItem.Tag.ToString(); string culture = selectedItem.Tag.ToString();
Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
@@ -106,6 +128,9 @@ namespace SkyTeam
else else
this.FlowDirection = FlowDirection.LeftToRight; this.FlowDirection = FlowDirection.LeftToRight;
// Quelle: Stack Overflow "How to refresh WPF page after changing culture?"
// Durch das erneute Navigieren auf die gleiche Seite (SettingsPage) wird
// das UI gezwungen, sich mit der neu gesetzten Sprache und FlowDirection neu zu rendern
NavigationService.Navigate(new SettingsPage()); NavigationService.Navigate(new SettingsPage());
} }
} }

View File

@@ -1,13 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Content Include="Screenshot 2026-03-04 100331.ico" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" /> <PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />

View File

@@ -9,19 +9,27 @@ namespace SkyTeam
{ {
public ReservierungssuchePage() public ReservierungssuchePage()
{ {
InitializeComponent(); InitializeComponent();
} }
private void SearchFlights_Click(object sender, RoutedEventArgs e) private void SearchFlights_Click(object sender, RoutedEventArgs e)
{ {
// Simples Auslesen der Benutzereingaben aus den Textboxen und dem DatePicker
string from = FromBox.Text; string from = FromBox.Text;
string to = ToBox.Text; string to = ToBox.Text;
DateTime? date = DateBox.SelectedDate; DateTime? date = DateBox.SelectedDate;
// Quelle: Stack Overflow "Passing parameters between pages in WPF"
// Um Daten (wie Suchkriterien) sicher an die nächste Seite zu übergeben, nutzen wir hier
// den überladenen Konstruktor der Ziel-Page ("verfuegbareFluge"). Laut Community-Konsens ist das
// die sauberste und direkteste Methode für einfache Datenübergaben in Standard-WPF-Anwendungen.
NavigationService.Navigate(new verfuegbareFluge(from, to, date)); NavigationService.Navigate(new verfuegbareFluge(from, to, date));
} }
// Quelle: Im Unterricht gemacht
// Lambda-Ausdrücke (=>) für kurze und übersichtliche Seitenwechsel über den NavigationService
private void HomeButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new NavigationPage()); private void HomeButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new NavigationPage());
private void BookingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new BuchungenPage()); private void BookingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new BuchungenPage());
private void SettingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new SettingsPage()); private void SettingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new SettingsPage());

View File

@@ -26,11 +26,13 @@ namespace SkyTeam
private void LoadFlights() private void LoadFlights()
{ {
// Source: Stack Overflow "What is the purpose of using WHERE 1=1 in SQL statements?"
// Link: https://stackoverflow.com/questions/1264681/what-is-the-purpose-of-using-where-1-1-in-sql-statements
string query = @"SELECT f.Id, f.Flugnummer, f.Abflugort AS 'From', f.Zielort AS 'To', string query = @"SELECT f.Id, f.Flugnummer, f.Abflugort AS 'From', f.Zielort AS 'To',
z.Modell AS Plane, f.Abflugdatum AS Date z.Modell AS Plane, f.Abflugdatum AS Date
FROM fluege f FROM fluege f
JOIN flugzeuge z ON f.FlugzeugId = z.Id JOIN flugzeuge z ON f.FlugzeugId = z.Id
WHERE 1=1"; WHERE 1=1";
if (!string.IsNullOrWhiteSpace(_fromCity)) if (!string.IsNullOrWhiteSpace(_fromCity))
{ {
@@ -86,9 +88,13 @@ namespace SkyTeam
return; return;
} }
// Source: Stack Overflow "Get selected row item in DataGrid WPF"
// Link: https://stackoverflow.com/questions/3913580/get-selected-row-item-in-datagrid-wpf
DataRowView row = (DataRowView)AvailableFlightsDataGrid.SelectedItem; DataRowView row = (DataRowView)AvailableFlightsDataGrid.SelectedItem;
int flightId = Convert.ToInt32(row["Id"]); int flightId = Convert.ToInt32(row["Id"]);
if (SessionManager.CurrentUserId == 0) if (SessionManager.CurrentUserId == 0)
{ {
MessageBox.Show("Fehler: Nicht eingeloggt."); MessageBox.Show("Fehler: Nicht eingeloggt.");
@@ -117,6 +123,8 @@ namespace SkyTeam
} }
} }
// quelle: Microsoft Learn "NavigationService.Navigate Method"
private void HomeButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new NavigationPage()); private void HomeButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new NavigationPage());
private void BookingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new BuchungenPage()); private void BookingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new BuchungenPage());
private void SettingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new SettingsPage()); private void SettingsButton_Click(object sender, RoutedEventArgs e) => NavigationService.Navigate(new SettingsPage());