From 0ffe6da3c6dfe62312f8b8a318a7a906c89e4180 Mon Sep 17 00:00:00 2001 From: Viperion Date: Mon, 5 May 2025 14:43:11 +0200 Subject: [PATCH] admin view + secure Login (hash + Salt) --- .../ChronoFlow.Controller.csproj | 5 +- ChronoFlow.Model/User.cs | 28 +- ChronoFlow.Model/Zeiteintrag.cs | 29 +- .../ChronoFlow.Persistence.csproj | 5 +- .../SecurityReferenceTest.cs | 19 + .../SqliteZeiterfassungsService.cs | 442 ++++++++++++++---- .../ChronoFlow.Security.csproj | 9 + ChronoFlow.Security/PasswordHasher.cs | 67 +++ .../Admin/AbgeschlosseneProjekteView.axaml | 26 ++ .../Admin/AbgeschlosseneProjekteView.axaml.cs | 49 ++ ChronoFlow.View/Admin/AdminMainView.axaml | 59 +++ ChronoFlow.View/Admin/AdminMainView.axaml.cs | 106 +++++ ChronoFlow.View/Admin/AlleProjekteView.axaml | 31 ++ .../Admin/AlleProjekteView.axaml.cs | 89 ++++ ChronoFlow.View/Admin/ConfirmDialog.axaml | 15 + ChronoFlow.View/Admin/ConfirmDialog.axaml.cs | 27 ++ .../Admin/MitarbeiterBearbeitenDialog.axaml | 31 ++ .../MitarbeiterBearbeitenDialog.axaml.cs | 41 ++ .../Admin/MitarbeiterListeView.axaml | 29 ++ .../Admin/MitarbeiterListeView.axaml.cs | 143 ++++++ .../Admin/ProjektBearbeitenDialog.axaml | 28 ++ .../Admin/ProjektBearbeitenDialog.axaml.cs | 47 ++ .../Admin/ProjektErstellenView.axaml | 39 ++ .../Admin/ProjektErstellenView.axaml.cs | 102 ++++ ChronoFlow.View/App.axaml | 5 +- ChronoFlow.View/ChronoFlow.View.csproj | 36 +- ChronoFlow.View/LoginWindow.axaml | 20 +- ChronoFlow.View/LoginWindow.axaml.cs | 95 +++- ChronoFlow.View/MainWindow.axaml | 23 +- ChronoFlow.View/MainWindow.axaml.cs | 91 ++-- .../MitarbeiterHinzufuegenView.axaml | 1 + .../MitarbeiterHinzufuegenView.axaml.cs | 56 ++- ChronoFlow.View/Program.cs | 16 +- .../Security/PasswortAendernDialog.axaml | 23 + .../Security/PasswortAendernDialog.axaml.cs | 46 ++ ChronoFlow.View/ViewManager.cs | 57 ++- ChronoFlow.View/ZeiterfassungView.axaml | 44 -- ChronoFlow.View/ZeiterfassungView.axaml.cs | 134 ------ ChronoFlow.sln | 6 + 39 files changed, 1677 insertions(+), 442 deletions(-) create mode 100644 ChronoFlow.Persistence/SecurityReferenceTest.cs create mode 100644 ChronoFlow.Security/ChronoFlow.Security.csproj create mode 100644 ChronoFlow.Security/PasswordHasher.cs create mode 100644 ChronoFlow.View/Admin/AbgeschlosseneProjekteView.axaml create mode 100644 ChronoFlow.View/Admin/AbgeschlosseneProjekteView.axaml.cs create mode 100644 ChronoFlow.View/Admin/AdminMainView.axaml create mode 100644 ChronoFlow.View/Admin/AdminMainView.axaml.cs create mode 100644 ChronoFlow.View/Admin/AlleProjekteView.axaml create mode 100644 ChronoFlow.View/Admin/AlleProjekteView.axaml.cs create mode 100644 ChronoFlow.View/Admin/ConfirmDialog.axaml create mode 100644 ChronoFlow.View/Admin/ConfirmDialog.axaml.cs create mode 100644 ChronoFlow.View/Admin/MitarbeiterBearbeitenDialog.axaml create mode 100644 ChronoFlow.View/Admin/MitarbeiterBearbeitenDialog.axaml.cs create mode 100644 ChronoFlow.View/Admin/MitarbeiterListeView.axaml create mode 100644 ChronoFlow.View/Admin/MitarbeiterListeView.axaml.cs create mode 100644 ChronoFlow.View/Admin/ProjektBearbeitenDialog.axaml create mode 100644 ChronoFlow.View/Admin/ProjektBearbeitenDialog.axaml.cs create mode 100644 ChronoFlow.View/Admin/ProjektErstellenView.axaml create mode 100644 ChronoFlow.View/Admin/ProjektErstellenView.axaml.cs create mode 100644 ChronoFlow.View/Security/PasswortAendernDialog.axaml create mode 100644 ChronoFlow.View/Security/PasswortAendernDialog.axaml.cs delete mode 100644 ChronoFlow.View/ZeiterfassungView.axaml delete mode 100644 ChronoFlow.View/ZeiterfassungView.axaml.cs diff --git a/ChronoFlow.Controller/ChronoFlow.Controller.csproj b/ChronoFlow.Controller/ChronoFlow.Controller.csproj index fc966e6..f3d3af5 100644 --- a/ChronoFlow.Controller/ChronoFlow.Controller.csproj +++ b/ChronoFlow.Controller/ChronoFlow.Controller.csproj @@ -2,13 +2,14 @@ net9.0 + Library enable enable - - + + diff --git a/ChronoFlow.Model/User.cs b/ChronoFlow.Model/User.cs index a2247f6..e42e874 100644 --- a/ChronoFlow.Model/User.cs +++ b/ChronoFlow.Model/User.cs @@ -1,16 +1,26 @@ namespace ChronoFlow.Model { - - /// - /// Repräsentiert einen Benutzer mit Benutzernamen, Passwort und Rolle. - /// - public class User { public string Username { get; set; } - public string Password { get; set; } //vorerst im Klartext, wird später geändert - public string Role { get; set; } //"Admin" oder "Mitarbeiter" - public string Mitarbeiternummer { get; set; } = ""; - public string Abteilung { get; set; } = ""; + public string Password { get; set; } + public string Role { get; set; } + public string Mitarbeiternummer { get; set; } + public string Abteilung { get; set; } + + public int Id { get; set; } + public string OriginalUsername { get; set; } // wichtig für Updates + + public bool MussPasswortAendern { get; set; } + + public User() + { + Username = ""; + Password = ""; + Role = ""; + Mitarbeiternummer = ""; + Abteilung = ""; + OriginalUsername = ""; + } } } \ No newline at end of file diff --git a/ChronoFlow.Model/Zeiteintrag.cs b/ChronoFlow.Model/Zeiteintrag.cs index 891b441..f0cdbc6 100644 --- a/ChronoFlow.Model/Zeiteintrag.cs +++ b/ChronoFlow.Model/Zeiteintrag.cs @@ -1,26 +1,27 @@ -using System; - namespace ChronoFlow.Model { public class Zeiteintrag { + public int Id { get; set; } // <<< NEU + public string Mitarbeiter { get; set; } public DateTime Startzeit { get; set; } public DateTime Endzeit { get; set; } - public string? Projekt { get; set; } - public string? Kommentar { get; set; } - - public TimeSpan Dauer => Endzeit - Startzeit; - - //Felder für Mitarbeiter-Rückmeldung + public string Projekt { get; set; } + public string Kommentar { get; set; } public bool Erledigt { get; set; } - public string? MitarbeiterKommentar { get; set; } - + public string MitarbeiterKommentar { get; set; } - public override string ToString() + public Zeiteintrag() { - return $"{Mitarbeiter} - {Startzeit:HH:mm} - {Endzeit:HH:mm} | {Projekt}"; + Id = 0; + Mitarbeiter = ""; + Startzeit = DateTime.Now; + Endzeit = DateTime.Now; + Projekt = ""; + Kommentar = ""; + Erledigt = false; + MitarbeiterKommentar = ""; } - } -} +} \ No newline at end of file diff --git a/ChronoFlow.Persistence/ChronoFlow.Persistence.csproj b/ChronoFlow.Persistence/ChronoFlow.Persistence.csproj index 30dbf86..8e1dec1 100644 --- a/ChronoFlow.Persistence/ChronoFlow.Persistence.csproj +++ b/ChronoFlow.Persistence/ChronoFlow.Persistence.csproj @@ -7,11 +7,12 @@ - + - + + diff --git a/ChronoFlow.Persistence/SecurityReferenceTest.cs b/ChronoFlow.Persistence/SecurityReferenceTest.cs new file mode 100644 index 0000000..2578ef1 --- /dev/null +++ b/ChronoFlow.Persistence/SecurityReferenceTest.cs @@ -0,0 +1,19 @@ +using System; +using ChronoFlow.Security; + +namespace ChronoFlow.Persistence +{ + public class SecurityReferenceTest + { + public static void TestSecurityReference() + { + string testPasswort = "Test123!"; + string hashed = PasswordHasher.HashPassword(testPasswort); + + Console.WriteLine($"✅ Hash erfolgreich erzeugt: {hashed}"); + + bool isValid = PasswordHasher.VerifyPassword(testPasswort, hashed); + Console.WriteLine($"✅ Überprüfung erfolgreich: {isValid}"); + } + } +} \ No newline at end of file diff --git a/ChronoFlow.Persistence/SqliteZeiterfassungsService.cs b/ChronoFlow.Persistence/SqliteZeiterfassungsService.cs index c5147f4..8ba464e 100644 --- a/ChronoFlow.Persistence/SqliteZeiterfassungsService.cs +++ b/ChronoFlow.Persistence/SqliteZeiterfassungsService.cs @@ -1,21 +1,23 @@ using System; using System.Collections.Generic; using System.IO; -using System.Security.Cryptography; using Microsoft.Data.Sqlite; using ChronoFlow.Model; +using ChronoFlow.Security; namespace ChronoFlow.Persistence { public class SqliteZeiterfassungsService { - private readonly string _dbPath = "chrono_data_v2.sb"; + private readonly string _dbPath = "chrono_data.sb"; public SqliteZeiterfassungsService() { - // Prüfe, ob die DB existiert, sonst erstelle sie if (!File.Exists(_dbPath)) ErstelleDatenbank(); + + // IMMER prüfen, auch nach neuem Erstellen + PrüfeUndErweitereDatenbank(); } private void ErstelleDatenbank() @@ -27,31 +29,210 @@ namespace ChronoFlow.Persistence var cmd1 = connection.CreateCommand(); cmd1.CommandText = @" - CREATE TABLE IF NOT EXISTS Zeiteintraege ( - Id INTEGER PRIMARY KEY AUTOINCREMENT, - Mitarbeiter TEXT NOT NULL, - Startzeit TEXT NOT NULL, - Endzeit TEXT NOT NULL, - Projekt TEXT, - Kommentar TEXT, - Erledigt INTEGER, - MitarbeiterKommentar TEXT - );"; + CREATE TABLE IF NOT EXISTS Zeiteintraege ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Mitarbeiter TEXT NOT NULL, + Startzeit TEXT NOT NULL, + Endzeit TEXT NOT NULL, + Projekt TEXT, + Kommentar TEXT, + Erledigt INTEGER, + MitarbeiterKommentar TEXT + );"; cmd1.ExecuteNonQuery(); var cmd2 = connection.CreateCommand(); cmd2.CommandText = @" - CREATE TABLE IF NOT EXISTS Benutzer ( - Id INTEGER PRIMARY KEY AUTOINCREMENT, - Username TEXT NOT NULL, - Password TEXT NOT NULL, - Role TEXT NOT NULL, - Mitarbeiternummer TEXT, - Abteilung TEXT - );"; + CREATE TABLE IF NOT EXISTS Benutzer ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Username TEXT NOT NULL, + Password TEXT NOT NULL, + Role TEXT NOT NULL, + Mitarbeitennummer TEXT, + Abteilung TEXT, + MussPasswortAendern INTEGER DEFAULT 1 + );"; cmd2.ExecuteNonQuery(); } + + public void ErstelleNeuenBenutzer(User benutzer) + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + INSERT INTO Benutzer (Username, Password, Role, Mitarbeitennummer, Abteilung, MussPasswortAendern) + VALUES ($Username, $Password, $Role, $Mitarbeiternummer, $Abteilung, $MussPasswortAendern); + "; + + cmd.Parameters.AddWithValue("$Username", benutzer.Username); + cmd.Parameters.AddWithValue("$Password", benutzer.Password); + cmd.Parameters.AddWithValue("$Role", benutzer.Role); + cmd.Parameters.AddWithValue("$Mitarbeiternummer", benutzer.Mitarbeiternummer ?? ""); + cmd.Parameters.AddWithValue("$Abteilung", benutzer.Abteilung ?? ""); + cmd.Parameters.AddWithValue("$MussPasswortAendern", benutzer.MussPasswortAendern ? 1 : 0); + + int rowsAffected = cmd.ExecuteNonQuery(); + Console.WriteLine($"✅ Neuer Benutzer '{benutzer.Username}' wurde gespeichert (Rows affected: {rowsAffected})."); + } + + + + private void PrüfeUndErweitereDatenbank() + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + AddColumnIfMissing(connection, "Benutzer", "Mitarbeitennummer", "TEXT"); + AddColumnIfMissing(connection, "Benutzer", "Abteilung", "TEXT"); + AddColumnIfMissing(connection, "Benutzer", "MussPasswortAendern", "INTEGER DEFAULT 1"); + } + + private void AddColumnIfMissing(SqliteConnection connection, string tableName, string columnName, string columnType) + { + var checkCmd = connection.CreateCommand(); + checkCmd.CommandText = $"PRAGMA table_info({tableName});"; + + using var reader = checkCmd.ExecuteReader(); + bool columnExists = false; + + while (reader.Read()) + { + var existingColumnName = reader.GetString(1); + if (existingColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)) + { + columnExists = true; + break; + } + } + + if (!columnExists) + { + var alterCmd = connection.CreateCommand(); + alterCmd.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {columnName} {columnType};"; + alterCmd.ExecuteNonQuery(); + Console.WriteLine($"✅ Spalte '{columnName}' in Tabelle '{tableName}' hinzugefügt."); + } + } + + public void ErstelleStandardAdmin() + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var checkCmd = connection.CreateCommand(); + checkCmd.CommandText = "SELECT COUNT(*) FROM Benutzer WHERE Username = 'admin';"; + var count = Convert.ToInt32(checkCmd.ExecuteScalar()); + + if (count == 0) + { + var hashedPassword = PasswordHasher.HashPassword("admin"); + var insertCmd = connection.CreateCommand(); + insertCmd.CommandText = @" + INSERT INTO Benutzer (Username, Password, Role, Mitarbeitennummer, Abteilung, MussPasswortAendern) + VALUES ('admin', $Password, 'Admin', '0001', 'IT', 1); + "; + insertCmd.Parameters.AddWithValue("$Password", hashedPassword); + insertCmd.ExecuteNonQuery(); + + Console.WriteLine("✅ Standard-Admin erfolgreich eingefügt."); + } + else + { + Console.WriteLine("ℹ Standard-Admin existiert bereits – kein neuer Eintrag erstellt."); + } + } + + public List LadeAlleBenutzer() + { + var benutzerListe = new List(); + + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT Id, Username, Password, Role, Mitarbeitennummer, Abteilung, MussPasswortAendern FROM Benutzer;"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + benutzerListe.Add(new User + { + Id = reader.GetInt32(0), + Username = reader.GetString(1), + Password = reader.GetString(2), + Role = reader.GetString(3), + Mitarbeiternummer = reader.IsDBNull(4) ? "" : reader.GetString(4), + Abteilung = reader.IsDBNull(5) ? "" : reader.GetString(5), + MussPasswortAendern = reader.GetInt32(6) == 1, + OriginalUsername = reader.GetString(1) + }); + } + + return benutzerListe; + } + + public void UpdateBenutzer(User benutzer) + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + UPDATE Benutzer + SET Username = $NewUsername, + Password = $Password, + Role = $Role, + Mitarbeitennummer = $Mitarbeiternummer, + Abteilung = $Abteilung, + MussPasswortAendern = $MussPasswortAendern + WHERE Username = $OriginalUsername; + "; + + cmd.Parameters.AddWithValue("$NewUsername", benutzer.Username); + cmd.Parameters.AddWithValue("$Password", benutzer.Password); + cmd.Parameters.AddWithValue("$Role", benutzer.Role); + cmd.Parameters.AddWithValue("$Mitarbeiternummer", benutzer.Mitarbeiternummer ?? ""); + cmd.Parameters.AddWithValue("$Abteilung", benutzer.Abteilung ?? ""); + cmd.Parameters.AddWithValue("$MussPasswortAendern", benutzer.MussPasswortAendern ? 1 : 0); + cmd.Parameters.AddWithValue("$OriginalUsername", benutzer.OriginalUsername); + + int rowsAffected = cmd.ExecuteNonQuery(); + Console.WriteLine($"✏ Benutzer aktualisiert: {benutzer.Username} (Rows affected: {rowsAffected})"); + } + + public void LoescheBenutzer(string username) + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "DELETE FROM Benutzer WHERE Username = $Username;"; + cmd.Parameters.AddWithValue("$Username", username); + + int rowsAffected = cmd.ExecuteNonQuery(); + Console.WriteLine($"❌ Benutzer gelöscht: {username} (Rows affected: {rowsAffected})"); + } + + public List LadeAlleMitarbeiterNamen() + { + var namen = new List(); + + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT Username FROM Benutzer WHERE Role = 'Mitarbeiter';"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + namen.Add(reader.GetString(0)); + } + + return namen; + } public void SpeichereEintrag(Zeiteintrag eintrag) { @@ -74,6 +255,8 @@ namespace ChronoFlow.Persistence cmd.Parameters.AddWithValue("$MitarbeiterKommentar", eintrag.MitarbeiterKommentar ?? ""); cmd.ExecuteNonQuery(); + + Console.WriteLine($"✅ Zeiteintrag für {eintrag.Mitarbeiter} gespeichert."); } public List LadeAlleZeiteintraege() @@ -91,6 +274,7 @@ namespace ChronoFlow.Persistence { eintraege.Add(new Zeiteintrag { + Id = reader.GetInt32(0), Mitarbeiter = reader.GetString(1), Startzeit = DateTime.Parse(reader.GetString(2)), Endzeit = DateTime.Parse(reader.GetString(3)), @@ -100,85 +284,179 @@ namespace ChronoFlow.Persistence MitarbeiterKommentar = reader.GetString(7) }); } - + return eintraege; } - public List LadeAlleMitarbeiterNamen() - { - var namen = new List(); - - using var connection = new SqliteConnection($"Data Source={_dbPath}"); - connection.Open(); - - var cmd = connection.CreateCommand(); - cmd.CommandText = "SELECT Username From Benutzer Where Role = 'Mitarbeiter';"; - - using var reader = cmd.ExecuteReader(); - while (reader.Read()) - { - namen.Add(reader.GetString(0)); - } - return namen; - } - public List LadeAlleBenutzer() - { - var benutzerListe = new List(); - using var connection = new SqliteConnection($"Data Source={_dbPath}"); - connection.Open(); - - var cmd = connection.CreateCommand(); - cmd.CommandText = "SELECT Username, Password, Role, Mitarbeiternummer, Abteilung FROM Benutzer;"; - - using var reader = cmd.ExecuteReader(); - while (reader.Read()) - { - benutzerListe.Add(new User - { - Username = reader.GetString(0), - Password = reader.GetString(1), - Role = reader.GetString(2), - Mitarbeiternummer = reader.IsDBNull(3) ? "" : reader.GetString(3), - Abteilung = reader.IsDBNull(4) ? "" : reader.GetString(4) - }); - } - - return benutzerListe; - } - - public void ErstelleStandardAdmin() + public void UpdateProjekt(Zeiteintrag projekt) { using var connection = new SqliteConnection($"Data Source={_dbPath}"); connection.Open(); var cmd = connection.CreateCommand(); cmd.CommandText = @" - INSERT INTO Benutzer (Username, Password, Role, Mitarbeiternummer, Abteilung) - VALUES ('admin', 'admin', 'Admin', '0001', 'IT'); - "; + UPDATE Zeiteintraege + SET Projekt = $Projekt, + Kommentar = $Kommentar, + Startzeit = $Startzeit, + Endzeit = $Endzeit, + Mitarbeiter = $Mitarbeiter, + Erledigt = $Erledigt + WHERE Id = $Id; + "; - cmd.ExecuteNonQuery(); - - Console.WriteLine("✅ Standard-Admin erfolgreich eingefügt."); + cmd.Parameters.AddWithValue("$Projekt", projekt.Projekt); + cmd.Parameters.AddWithValue("$Kommentar", projekt.Kommentar ?? ""); + cmd.Parameters.AddWithValue("$Startzeit", projekt.Startzeit.ToString("o")); + cmd.Parameters.AddWithValue("$Endzeit", projekt.Endzeit.ToString("o")); + cmd.Parameters.AddWithValue("$Mitarbeiter", projekt.Mitarbeiter); + cmd.Parameters.AddWithValue("$Erledigt", projekt.Erledigt ? 1 : 0); + cmd.Parameters.AddWithValue("$Id", projekt.Id); + + int rowsAffected = cmd.ExecuteNonQuery(); + Console.WriteLine($"✏ Projekt aktualisiert (Id={projekt.Id}, Rows affected: {rowsAffected})"); } - - public List LadeAlleBenutzernamen() - { - var benutzernamen = new List(); + public void LoescheProjekt(int id) + { using var connection = new SqliteConnection($"Data Source={_dbPath}"); connection.Open(); - - using var cmd = connection.CreateCommand();//<--- Wir erstellen cmd - cmd.CommandText = "SELECT Username From Benutzer;"; - + + var cmd = connection.CreateCommand(); + cmd.CommandText = "DELETE FROM Zeiteintraege WHERE Id = $Id;"; + cmd.Parameters.AddWithValue("$Id", id); + + int rows = cmd.ExecuteNonQuery(); + Console.WriteLine($"❌ Projekt gelöscht (Id: {id}, Rows: {rows})"); + } + + public void UpdateProjektStatus(int id, bool erledigt) + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + UPDATE Zeiteintraege + SET Erledigt = $Erledigt + WHERE Id = $Id; + "; + + cmd.Parameters.AddWithValue("$Erledigt", erledigt ? 1 : 0); + cmd.Parameters.AddWithValue("$Id", id); + + int rowsAffected = cmd.ExecuteNonQuery(); + Console.WriteLine($"✅ Projektstatus aktualisiert (Id={id}, Erledigt={erledigt}, Rows affected: {rowsAffected})"); + } + + public List LadeAbgeschlosseneProjekte() + { + var abgeschlossene = new List(); + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM Zeiteintraege WHERE Erledigt = 1;"; + using var reader = cmd.ExecuteReader(); while (reader.Read()) { - benutzernamen.Add(reader.GetString(0)); + abgeschlossene.Add(new Zeiteintrag + { + Id = reader.GetInt32(0), + Mitarbeiter = reader.GetString(1), + Startzeit = DateTime.Parse(reader.GetString(2)), + Endzeit = DateTime.Parse(reader.GetString(3)), + Projekt = reader.GetString(4), + Kommentar = reader.GetString(5), + Erledigt = Convert.ToInt32(reader["Erledigt"]) == 1, + MitarbeiterKommentar = reader.GetString(7) + }); } - return benutzernamen; + + return abgeschlossene; + } + + public List LadeLetzteProjekte(int anzahl = 3) + { + var projekte = new List(); + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + SELECT * FROM Zeiteintraege + ORDER BY Id DESC + LIMIT $Anzahl; + "; + cmd.Parameters.AddWithValue("$Anzahl", anzahl); + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + projekte.Add(new Zeiteintrag + { + Id = reader.GetInt32(0), + Mitarbeiter = reader.GetString(1), + Startzeit = DateTime.Parse(reader.GetString(2)), + Endzeit = DateTime.Parse(reader.GetString(3)), + Projekt = reader.GetString(4), + Kommentar = reader.GetString(5), + Erledigt = Convert.ToInt32(reader["Erledigt"]) == 1, + MitarbeiterKommentar = reader.GetString(7) + }); + } + + return projekte; + } + + public List LadeOffeneProjekte() + { + var offene = new List(); + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT * FROM Zeiteintraege WHERE Erledigt = 0;"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + offene.Add(new Zeiteintrag + { + Id = reader.GetInt32(0), + Mitarbeiter = reader.GetString(1), + Startzeit = DateTime.Parse(reader.GetString(2)), + Endzeit = DateTime.Parse(reader.GetString(3)), + Projekt = reader.GetString(4), + Kommentar = reader.GetString(5), + Erledigt = Convert.ToInt32(reader["Erledigt"]) == 1, + MitarbeiterKommentar = reader.GetString(7) + }); + } + + return offene; + } + + public void ResetBenutzerPasswort(string username) + { + using var connection = new SqliteConnection($"Data Source={_dbPath}"); + connection.Open(); + + var hashedDefault = PasswordHasher.HashPassword("changeme"); + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + UPDATE Benutzer + SET Password = $Password, + MussPasswortAendern = 1 + WHERE Username = $Username; + "; + + cmd.Parameters.AddWithValue("$Password", hashedDefault); + cmd.Parameters.AddWithValue("$Username", username); + + int rowsAffected = cmd.ExecuteNonQuery(); + Console.WriteLine($"🔒 Passwort für Benutzer '{username}' zurückgesetzt (Rows affected: {rowsAffected})"); } - } } diff --git a/ChronoFlow.Security/ChronoFlow.Security.csproj b/ChronoFlow.Security/ChronoFlow.Security.csproj new file mode 100644 index 0000000..17b910f --- /dev/null +++ b/ChronoFlow.Security/ChronoFlow.Security.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/ChronoFlow.Security/PasswordHasher.cs b/ChronoFlow.Security/PasswordHasher.cs new file mode 100644 index 0000000..c95c757 --- /dev/null +++ b/ChronoFlow.Security/PasswordHasher.cs @@ -0,0 +1,67 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace ChronoFlow.Security +{ + /// + /// Diese Klasse bietet Funktionen zum sicheren Hashen und Überprüfen von Passwörtern. + /// + public static class PasswordHasher + { + /// + /// Erstellt einen sicheren Hash aus einem Passwort unter Verwendung von PBKDF2. + /// + /// Das Klartextpasswort + /// Ein kombinierter Hash-String (Salt + Hash) + public static string HashPassword(string password) + { + using var rng = RandomNumberGenerator.Create(); + byte[] salt = new byte[16]; + rng.GetBytes(salt); + + var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256); + byte[] hash = pbkdf2.GetBytes(32); + + // Kombiniere Salt + Hash in einen String (Base64-encodiert) + byte[] hashBytes = new byte[48]; + Array.Copy(salt, 0, hashBytes, 0, 16); + Array.Copy(hash, 0, hashBytes, 16, 32); + + return Convert.ToBase64String(hashBytes); + } + + /// + /// Überprüft, ob ein Passwort zu einem gegebenen Hash passt. + /// + /// Das eingegebene Klartextpasswort + /// Der gespeicherte kombinierte Hash (Base64, Salt + Hash) + /// True, wenn das Passwort stimmt, sonst false + public static bool VerifyPassword(string password, string storedHash) + { + try + { + byte[] hashBytes = Convert.FromBase64String(storedHash); + + byte[] salt = new byte[16]; + Array.Copy(hashBytes, 0, salt, 0, 16); + + var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256); + byte[] hash = pbkdf2.GetBytes(32); + + for (int i = 0; i < 32; i++) + { + if (hashBytes[i + 16] != hash[i]) + return false; + } + + return true; + } + catch + { + // Falls der gespeicherte Hash beschädigt oder kein Base64 ist + return false; + } + } + } +} diff --git a/ChronoFlow.View/Admin/AbgeschlosseneProjekteView.axaml b/ChronoFlow.View/Admin/AbgeschlosseneProjekteView.axaml new file mode 100644 index 0000000..3d9ea3e --- /dev/null +++ b/ChronoFlow.View/Admin/AbgeschlosseneProjekteView.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + +