diff --git a/Watcher/Controllers/AuthController.cs b/Watcher/Controllers/AuthController.cs index 2394675..66ff5f1 100644 --- a/Watcher/Controllers/AuthController.cs +++ b/Watcher/Controllers/AuthController.cs @@ -1,16 +1,61 @@ +using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Watcher.Data; +using Watcher.ViewModels; namespace Watcher.Controllers; public class AuthController : Controller { - public IActionResult Login() + private readonly AppDbContext _context; + + public AuthController(AppDbContext context) { - return View(); + _context = context; } + [HttpGet] + public IActionResult Login(string? returnUrl = null) + { + var model = new LoginViewModel + { + ReturnUrl = returnUrl + }; + return View(model); + } + + + [HttpPost] + public async Task Login(LoginViewModel model) + { + if (!ModelState.IsValid) + return View(model); + + var user = await _context.Users.FirstOrDefaultAsync(u => u.PreferredUsername == model.Username); + if (user == null || !BCrypt.Net.BCrypt.Verify(model.Password, user.Password)) + { + ModelState.AddModelError("", "Benutzername oder Passwort ist falsch."); + return View(model); + } + + var claims = new List + { + new Claim(ClaimTypes.Name, user.PreferredUsername), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + }; + + var identity = new ClaimsIdentity(claims, "local"); + var principal = new ClaimsPrincipal(identity); + + await HttpContext.SignInAsync("Cookies", principal); + + return Redirect("Home/Index"); + } + + public IActionResult SignIn() { return Challenge(new AuthenticationProperties @@ -20,20 +65,20 @@ public class AuthController : Controller } [HttpPost] -[ValidateAntiForgeryToken] -public async Task Logout() -{ - // Lokales Cookie löschen - await HttpContext.SignOutAsync("Cookies"); - - // Externes OIDC-Logout initiieren - await HttpContext.SignOutAsync("oidc", new AuthenticationProperties + [ValidateAntiForgeryToken] + public async Task Logout() { - RedirectUri = "/" // Nach Logout zurück zur Startseite oder Login-Seite - }); + var props = new AuthenticationProperties + { + RedirectUri = Url.Action("Login", "Auth") + }; + + await HttpContext.SignOutAsync("Cookies"); + await HttpContext.SignOutAsync("oidc", props); + + return Redirect("/"); // nur als Fallback + } - return RedirectToAction("Index", "Home"); -} [Authorize] public IActionResult Info() @@ -46,4 +91,46 @@ public async Task Logout() return View(); } + + // Edit-Form anzeigen + [Authorize] + [HttpGet] + public IActionResult Edit() + { + var username = User.Identity?.Name; + var user = _context.Users.FirstOrDefault(u => u.PreferredUsername == username); + if (user == null) return NotFound(); + + var model = new EditUserViewModel + { + Username = user.PreferredUsername + }; + return View(model); + } + + // Edit speichern + [Authorize] + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult Edit(EditUserViewModel model) + { + if (!ModelState.IsValid) return View(model); + + var username = User.Identity?.Name; + var user = _context.Users.FirstOrDefault(u => u.PreferredUsername == username); + if (user == null) return NotFound(); + + user.PreferredUsername = model.Username; + + if (!string.IsNullOrWhiteSpace(model.NewPassword)) + { + user.PreferredUsername = BCrypt.Net.BCrypt.HashPassword(model.NewPassword); + } + + _context.SaveChanges(); + + // Eventuell hier das Auth-Cookie erneuern, wenn Username sich ändert + + return RedirectToAction("Index", "Home"); + } } diff --git a/Watcher/Controllers/ContainerController.cs b/Watcher/Controllers/ContainerController.cs index 624a1a8..e65c127 100644 --- a/Watcher/Controllers/ContainerController.cs +++ b/Watcher/Controllers/ContainerController.cs @@ -7,7 +7,7 @@ using Watcher.ViewModels; namespace Watcher.Controllers; - +[Authorize] public class ContainerController : Controller { private readonly AppDbContext _context; diff --git a/Watcher/Migrations/20250617174242_UserPasswordAdded.Designer.cs b/Watcher/Migrations/20250617174242_UserPasswordAdded.Designer.cs new file mode 100644 index 0000000..123e1fa --- /dev/null +++ b/Watcher/Migrations/20250617174242_UserPasswordAdded.Designer.cs @@ -0,0 +1,306 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Watcher.Data; + +#nullable disable + +namespace Watcher.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250617174242_UserPasswordAdded")] + partial class UserPasswordAdded + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + modelBuilder.Entity("Watcher.Models.Container", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Hostname") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("IsRunning") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("TagId"); + + b.ToTable("Containers"); + }); + + modelBuilder.Entity("Watcher.Models.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Watcher.Models.LogEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContainerId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("ServerId"); + + b.ToTable("LogEvents"); + }); + + modelBuilder.Entity("Watcher.Models.Metric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContainerId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("ServerId"); + + b.ToTable("Metrics"); + }); + + modelBuilder.Entity("Watcher.Models.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CpuCores") + .HasColumnType("INTEGER"); + + b.Property("CpuType") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("GpuType") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsOnline") + .HasColumnType("INTEGER"); + + b.Property("LastSeen") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RamSize") + .HasColumnType("REAL"); + + b.Property("TagId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TagId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Watcher.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Watcher.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("IdentityProvider") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLogin") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PocketId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PreferredUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Watcher.Models.Container", b => + { + b.HasOne("Watcher.Models.Image", "Image") + .WithMany("Containers") + .HasForeignKey("ImageId"); + + b.HasOne("Watcher.Models.Tag", null) + .WithMany("Containers") + .HasForeignKey("TagId"); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("Watcher.Models.LogEvent", b => + { + b.HasOne("Watcher.Models.Container", "Container") + .WithMany() + .HasForeignKey("ContainerId"); + + b.HasOne("Watcher.Models.Server", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Container"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Watcher.Models.Metric", b => + { + b.HasOne("Watcher.Models.Container", "Container") + .WithMany() + .HasForeignKey("ContainerId"); + + b.HasOne("Watcher.Models.Server", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Container"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Watcher.Models.Server", b => + { + b.HasOne("Watcher.Models.Tag", null) + .WithMany("Servers") + .HasForeignKey("TagId"); + }); + + modelBuilder.Entity("Watcher.Models.Image", b => + { + b.Navigation("Containers"); + }); + + modelBuilder.Entity("Watcher.Models.Tag", b => + { + b.Navigation("Containers"); + + b.Navigation("Servers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Watcher/Migrations/20250617174242_UserPasswordAdded.cs b/Watcher/Migrations/20250617174242_UserPasswordAdded.cs new file mode 100644 index 0000000..04ef332 --- /dev/null +++ b/Watcher/Migrations/20250617174242_UserPasswordAdded.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Watcher.Migrations +{ + /// + public partial class UserPasswordAdded : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IdentityProvider", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Password", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IdentityProvider", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Password", + table: "Users"); + } + } +} diff --git a/Watcher/Migrations/AppDbContextModelSnapshot.cs b/Watcher/Migrations/AppDbContextModelSnapshot.cs index 48bd63a..1231da3 100644 --- a/Watcher/Migrations/AppDbContextModelSnapshot.cs +++ b/Watcher/Migrations/AppDbContextModelSnapshot.cs @@ -212,9 +212,17 @@ namespace Watcher.Migrations b.Property("Email") .HasColumnType("TEXT"); + b.Property("IdentityProvider") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("LastLogin") .HasColumnType("TEXT"); + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("PocketId") .IsRequired() .HasColumnType("TEXT"); diff --git a/Watcher/Models/User.cs b/Watcher/Models/User.cs index 12903fc..10d0f0c 100644 --- a/Watcher/Models/User.cs +++ b/Watcher/Models/User.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace Watcher.Models; public class User @@ -7,4 +9,11 @@ public class User public string PreferredUsername { get; set; } = null!; public string? Email { get; set; } public DateTime LastLogin { get; set; } + + [Required] + public string IdentityProvider { get; set; } = "local"; + + [Required] + [DataType(DataType.Password)] + public String? Password { get; set; } = string.Empty; } diff --git a/Watcher/Program.cs b/Watcher/Program.cs index 9fd427a..3f301cb 100644 --- a/Watcher/Program.cs +++ b/Watcher/Program.cs @@ -59,7 +59,7 @@ builder.Services.AddAuthentication(options => }) .AddCookie("Cookies", options => { - options.LoginPath = "/Account/Login"; // Falls eigene Login-Seite + options.LoginPath = "/Auth/Login"; }); // PocketID nur konfigurieren, wenn aktiviert @@ -108,7 +108,9 @@ if (pocketIdEnabled) PocketId = pocketId, PreferredUsername = preferredUsername ?? "", Email = email, - LastLogin = DateTime.UtcNow + LastLogin = DateTime.UtcNow, + IdentityProvider = "oidc", + Password = string.Empty }; db.Users.Add(user); } @@ -130,6 +132,47 @@ if (pocketIdEnabled) var app = builder.Build(); + + +// Migrationen anwenden (für SQLite oder andere DBs) +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + + +// Standart-User in Datenbank schreiben +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + + Console.WriteLine("Checking for users..."); + + if (!db.Users.Any()) + { + Console.WriteLine("No users found, creating default user..."); + + var defaultUser = new User + { + PocketId = string.Empty, + PreferredUsername = "admin", + Email = string.Empty, + LastLogin = DateTime.UtcNow, + IdentityProvider = "local", + Password = BCrypt.Net.BCrypt.HashPassword("changeme") + }; + db.Users.Add(defaultUser); + db.SaveChanges(); + + Console.WriteLine("Default user created."); + } + else + { + Console.WriteLine("Users already exist."); + } +} + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { @@ -138,12 +181,10 @@ if (!app.Environment.IsDevelopment()) app.UseHsts(); } -// Migrationen anwenden (für SQLite oder andere DBs) -using (var scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} + + + + app.UseHttpsRedirection(); app.UseRouting(); diff --git a/Watcher/ViewModels/EditUserViewModel.cs b/Watcher/ViewModels/EditUserViewModel.cs new file mode 100644 index 0000000..30e3fe4 --- /dev/null +++ b/Watcher/ViewModels/EditUserViewModel.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Watcher.ViewModels; + +public class EditUserViewModel +{ + [Required] + public string? Username { get; set; } + + [Required] + [DataType(DataType.Password)] + public string? NewPassword { get; set; } + + [Required] + [DataType(DataType.Password)] + [Compare("NewPassword", ErrorMessage = "Passwörter stimmen nicht überein.")] + public string? ConfirmPassword { get; set; } +} diff --git a/Watcher/Views/Auth/EditUser.cshtml b/Watcher/Views/Auth/EditUser.cshtml new file mode 100644 index 0000000..e69de29 diff --git a/Watcher/Views/Auth/Info.cshtml b/Watcher/Views/Auth/Info.cshtml index 95f7267..709c002 100644 --- a/Watcher/Views/Auth/Info.cshtml +++ b/Watcher/Views/Auth/Info.cshtml @@ -76,11 +76,29 @@ +@{ -

Alle Claims

-
    -@foreach (var claim in User.Claims) -{ -
  • @claim.Type: @claim.Value
  • } -
\ No newline at end of file +@if (User.Identity.Name == "admin") +{ +

Benutzerdaten ändern

+
+
+ + +
+
+ + +
+
+ + +
+ +
+} +else +{ +

Benutzerdaten können nur für lokal angemeldete Nutzer geändert werden.

+} diff --git a/Watcher/Views/Auth/Login.cshtml b/Watcher/Views/Auth/Login.cshtml index 4a5e8e1..9d5a5c7 100644 --- a/Watcher/Views/Auth/Login.cshtml +++ b/Watcher/Views/Auth/Login.cshtml @@ -6,7 +6,9 @@