diff --git a/Watcher/Controllers/AuthController.cs b/Watcher/Controllers/AuthController.cs index 72fbac3..971a3ff 100644 --- a/Watcher/Controllers/AuthController.cs +++ b/Watcher/Controllers/AuthController.cs @@ -19,9 +19,12 @@ public class AuthController : Controller }, "oidc"); } - public IActionResult Logout() + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Logout() { - return SignOut("Cookies", "oidc"); + await HttpContext.SignOutAsync(); + return RedirectToAction("Index", "Home"); } [Authorize] diff --git a/Watcher/Migrations/20250614173150_UserChanges.Designer.cs b/Watcher/Migrations/20250614173150_UserChanges.Designer.cs new file mode 100644 index 0000000..dd22ffb --- /dev/null +++ b/Watcher/Migrations/20250614173150_UserChanges.Designer.cs @@ -0,0 +1,278 @@ +// +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("20250614173150_UserChanges")] + partial class UserChanges + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Watcher.Models.Container", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Hostname") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TagId") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("TagId"); + + b.ToTable("Containers"); + }); + + modelBuilder.Entity("Watcher.Models.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Tag") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Watcher.Models.LogEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ContainerId") + .HasColumnType("int"); + + b.Property("Level") + .HasColumnType("longtext"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("ServerId"); + + b.ToTable("LogEvents"); + }); + + modelBuilder.Entity("Watcher.Models.Metric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ContainerId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("longtext"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("ServerId"); + + b.ToTable("Metrics"); + }); + + modelBuilder.Entity("Watcher.Models.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Hostname") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TagId") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("TagId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Watcher.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Watcher.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("LastLogin") + .HasColumnType("datetime(6)"); + + b.Property("PocketId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PreferredUsername") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Watcher.Models.Container", b => + { + b.HasOne("Watcher.Models.Image", null) + .WithMany("Containers") + .HasForeignKey("ImageId"); + + b.HasOne("Watcher.Models.Tag", null) + .WithMany("Containers") + .HasForeignKey("TagId"); + }); + + 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/20250614173150_UserChanges.cs b/Watcher/Migrations/20250614173150_UserChanges.cs new file mode 100644 index 0000000..e288a35 --- /dev/null +++ b/Watcher/Migrations/20250614173150_UserChanges.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Watcher.Migrations +{ + /// + public partial class UserChanges : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Role", + table: "Users", + newName: "PreferredUsername"); + + migrationBuilder.UpdateData( + table: "Users", + keyColumn: "PocketId", + keyValue: null, + column: "PocketId", + value: ""); + + migrationBuilder.AlterColumn( + name: "PocketId", + table: "Users", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Email", + table: "Users", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "LastLogin", + table: "Users", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Email", + table: "Users"); + + migrationBuilder.DropColumn( + name: "LastLogin", + table: "Users"); + + migrationBuilder.RenameColumn( + name: "PreferredUsername", + table: "Users", + newName: "Role"); + + migrationBuilder.AlterColumn( + name: "PocketId", + table: "Users", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Watcher/Migrations/AppDbContextModelSnapshot.cs b/Watcher/Migrations/AppDbContextModelSnapshot.cs index 7a6f33f..888f967 100644 --- a/Watcher/Migrations/AppDbContextModelSnapshot.cs +++ b/Watcher/Migrations/AppDbContextModelSnapshot.cs @@ -191,10 +191,17 @@ namespace Watcher.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); - b.Property("PocketId") + b.Property("Email") .HasColumnType("longtext"); - b.Property("Role") + b.Property("LastLogin") + .HasColumnType("datetime(6)"); + + b.Property("PocketId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PreferredUsername") .IsRequired() .HasColumnType("longtext"); diff --git a/Watcher/Models/User.cs b/Watcher/Models/User.cs index 81395b9..12903fc 100644 --- a/Watcher/Models/User.cs +++ b/Watcher/Models/User.cs @@ -2,7 +2,9 @@ namespace Watcher.Models; public class User { - public int Id { get; set; } - public string? PocketId { get; set; } // z.B. externe ID vom PocketID-Login - public string Role { get; set; } = "User"; // "Admin", "Viewer", etc. + public int Id { get; set; } // PK + public string PocketId { get; set; } = null!; + public string PreferredUsername { get; set; } = null!; + public string? Email { get; set; } + public DateTime LastLogin { get; set; } } diff --git a/Watcher/Program.cs b/Watcher/Program.cs index 92bb1d6..a2e01b4 100644 --- a/Watcher/Program.cs +++ b/Watcher/Program.cs @@ -1,8 +1,10 @@ +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Watcher.Data; +using Watcher.Models; var builder = WebApplication.CreateBuilder(args); @@ -40,13 +42,52 @@ builder.Services.AddAuthentication(options => options.CallbackPath = config["CallbackPath"]; options.SaveTokens = true; - options.TokenValidationParameters = new TokenValidationParameters - { - NameClaimType = "name", // oder "preferred_username" oder der Wert, den du im Schritt 1 gesehen hast - }; + options.GetClaimsFromUserInfoEndpoint = true; + options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); + options.Scope.Add("email"); + + options.Events = new OpenIdConnectEvents +{ + OnTokenValidated = async ctx => + { + var db = ctx.HttpContext.RequestServices.GetRequiredService(); + + var principal = ctx.Principal; + var pocketId = principal.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value; + var preferredUsername = principal.FindFirst("preferred_username")?.Value; + var email = principal.FindFirst("email")?.Value; + + if (string.IsNullOrEmpty(pocketId)) + return; + + var user = await db.Users.FirstOrDefaultAsync(u => u.PocketId == pocketId); + + if (user == null) + { + user = new User + { + PocketId = pocketId, + PreferredUsername = preferredUsername ?? "", + Email = email, + LastLogin = DateTime.UtcNow + }; + db.Users.Add(user); + } + else + { + user.LastLogin = DateTime.UtcNow; + user.PreferredUsername = preferredUsername ?? user.PreferredUsername; + user.Email = email ?? user.Email; + db.Users.Update(user); + } + + await db.SaveChangesAsync(); + } +}; + }); diff --git a/Watcher/Views/Auth/Info.cshtml b/Watcher/Views/Auth/Info.cshtml index 98c146c..95f7267 100644 --- a/Watcher/Views/Auth/Info.cshtml +++ b/Watcher/Views/Auth/Info.cshtml @@ -1,15 +1,86 @@ @{ ViewData["Title"] = "Account Info"; + var pictureUrl = User.Claims.FirstOrDefault(c => c.Type == "picture")?.Value ?? "123"; } -

Account-Informationen

+

Account Info

-

Benutzername: @ViewBag.Name

- -

Claims:

-
    - @foreach (var claim in ViewBag.Claims) +
    + @if (!string.IsNullOrEmpty(pictureUrl)) { -
  • @claim.Type: @claim.Value
  • + Profilbild } -
+ else + { +
+ @(User.Identity?.Name?.Substring(0,1).ToUpper() ?? "?") +
+ } + +

@(User.FindFirst("name")?.Value ?? "Unbekannter Nutzer")

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Username@(@User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value ?? "Nicht verfügbar")
E-Mail@(@User.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value ?? "Nicht verfügbar")
Benutzer-ID@(User.FindFirst("sub")?.Value ?? "Nicht verfügbar")
Login-Zeit@(User.FindFirst("iat") != null + ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(User.FindFirst("iat").Value)).ToLocalTime().ToString() + : "Nicht verfügbar") +
Token läuft ab@(User.FindFirst("exp") != null + ? DateTimeOffset.FromUnixTimeSeconds(long.Parse(User.FindFirst("exp").Value)).ToLocalTime().ToString() + : "Nicht verfügbar") +
Rollen + @{ + var roles = User.FindAll("role").Select(r => r.Value); + if (!roles.Any()) + { + Keine Rollen + } + else + { +
    + @foreach (var role in roles) + { +
  • @role
  • + } +
+ } + } +
+ +
+ +
+ + + +

Alle Claims

+
    +@foreach (var claim in User.Claims) +{ +
  • @claim.Type: @claim.Value
  • +} +
\ No newline at end of file diff --git a/Watcher/Views/Home/Servers.cshtml b/Watcher/Views/Home/Servers.cshtml new file mode 100644 index 0000000..e69de29 diff --git a/Watcher/Views/Shared/_Layout.cshtml b/Watcher/Views/Shared/_Layout.cshtml index dd6f776..1911a3f 100644 --- a/Watcher/Views/Shared/_Layout.cshtml +++ b/Watcher/Views/Shared/_Layout.cshtml @@ -3,6 +3,11 @@ @using Microsoft.AspNetCore.Http @inject IHttpContextAccessor HttpContextAccessor +@{ + var pictureUrl = User.FindFirst("picture")?.Value; + var preferredUsername = User.FindFirst("preferred_username")?.Value ?? "User"; +} + @@ -63,7 +68,7 @@ Uptime