19 Commits

Author SHA1 Message Date
a2c6071960 aa
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 10m4s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Successful in 11m22s
Gitea CI/CD / Create Tag (push) Successful in 6s
2026-01-09 18:09:43 +01:00
4523867a61 Added Container_Card to dashboard
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 10m2s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Successful in 11m30s
Gitea CI/CD / Create Tag (push) Successful in 5s
2026-01-09 12:57:48 +01:00
8727aff861 AddServer Call for Development 2026-01-09 12:41:32 +01:00
301d2309c9 get-server api call returns serverlist 2026-01-09 11:57:02 +01:00
7a096ee29c removed possible null reference 2026-01-09 11:38:31 +01:00
1d734f2951 Swagger Configration and move Agent API Calls to APIController 2026-01-09 10:36:47 +01:00
d8b164e3eb Added Authentication with user-auth and apikey-auth
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 10m5s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Successful in 11m28s
Gitea CI/CD / Create Tag (push) Successful in 5s
2026-01-09 10:18:06 +01:00
05e5a209da logs eingefügt
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 10m40s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Successful in 12m32s
Gitea CI/CD / Create Tag (push) Successful in 5s
2026-01-08 14:54:26 +01:00
0b88292a85 Delete Calls for Containers and Servers 2026-01-08 12:48:26 +01:00
5bae9328d9 Added Containers to Database 2026-01-08 12:29:03 +01:00
3a872980da removed registration key 2026-01-08 12:15:02 +01:00
8cda82111d Server gets created at registration now 2026-01-08 12:13:49 +01:00
6e17dcb270 removed old sample data 2026-01-08 12:08:13 +01:00
a1f9a2008f Fixed RegistrationDto 2026-01-08 12:07:59 +01:00
31da3d14a3 Pipeline update for act runner cache and registration endpoint name change
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 10m40s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Successful in 13m9s
Gitea CI/CD / Create Tag (push) Successful in 5s
2026-01-08 08:58:57 +01:00
ae8b60687a docker build fails at db migrations
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 42s
Gitea CI/CD / Set Tag Name (push) Successful in 4s
Gitea CI/CD / docker-build-and-push (push) Successful in 1m33s
Gitea CI/CD / Create Tag (push) Successful in 4s
2026-01-07 14:24:47 +01:00
3fff8654fe container registry url
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 40s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Successful in 1m53s
Gitea CI/CD / Create Tag (push) Successful in 5s
2026-01-07 13:55:51 +01:00
9cfe9e2ab2 changed docker registry url
Some checks failed
Gitea CI/CD / dotnet-build-and-test (push) Successful in 43s
Gitea CI/CD / Set Tag Name (push) Successful in 5s
Gitea CI/CD / docker-build-and-push (push) Failing after 16s
Gitea CI/CD / Create Tag (push) Has been skipped
2026-01-07 13:52:45 +01:00
fd799d6158 removed arm64 build
All checks were successful
Gitea CI/CD / dotnet-build-and-test (push) Successful in 41s
Gitea CI/CD / Set Tag Name (push) Successful in 7s
Gitea CI/CD / docker-build-and-push (push) Successful in 1m49s
Gitea CI/CD / Create Tag (push) Successful in 5s
2026-01-07 13:38:21 +01:00
36 changed files with 2446 additions and 117 deletions

View File

@@ -10,7 +10,7 @@ env:
DOTNET_VERSION: '8.0.x'
DOCKER_IMAGE_NAME: watcher-server
REGISTRY_URL: git.triggermeelmo.com
DOCKER_PLATFORMS: 'linux/amd64,linux/arm64'
DOCKER_PLATFORMS: 'linux/amd64'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -25,12 +25,14 @@ jobs:
with:
submodules: false
- name: List files for debugging
run: |
pwd
ls -la
find . -name "*.csproj"
find . -name "*.sln"
# NuGet Cache
- name: Cache NuGet packages
uses: actions/cache@v3
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
@@ -40,10 +42,16 @@ jobs:
- name: Restore dependencies
run: dotnet restore watcher-monitoring.sln
- name: Build
run: dotnet build watcher-monitoring.sln --configuration Release --no-restore
- name: Test
run: dotnet test watcher-monitoring.sln --no-build --verbosity normal
continue-on-error: true
set-tag:
name: Set Tag Name
needs: [dotnet-build-and-test]
#if: ${{ !failure() && !cancelled() && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.set_tag.outputs.tag_name }}
@@ -105,6 +113,15 @@ jobs:
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
# Docker Layer Cache
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
@@ -112,13 +129,23 @@ jobs:
username: ${{ secrets.AUTOMATION_USERNAME }}
password: ${{ secrets.AUTOMATION_PASSWORD }}
- name: Build and Push Multi-Arch Docker Image
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
platforms: ${{ env.DOCKER_PLATFORMS }}
push: true
tags: ${{ env.REGISTRY_URL }}/triggermeelmo/${{ env.DOCKER_IMAGE_NAME }}:${{ needs.set-tag.outputs.tag_name }}
build-args: |
VERSION=${{ needs.set-tag.outputs.tag_name }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Workaround für Cache-Größe
- name: Move cache
run: |
docker buildx build \
--platform ${{ env.DOCKER_PLATFORMS }} \
--build-arg VERSION=${{ needs.set-tag.outputs.tag_name }} \
-t ${{ env.REGISTRY_URL }}/watcher/${{ env.DOCKER_IMAGE_NAME }}:${{ needs.set-tag.outputs.tag_name }} \
--push .
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
tag:
name: Create Tag

View File

@@ -1,6 +1,6 @@
services:
watcher:
image: git.triggermeelmo.com/watcher/watcher-server:latest
image: git.triggermeelmo.com/triggermeelmo/watcher-server:latest
container_name: watcher
# Resource Management
@@ -8,7 +8,6 @@ services:
resources:
limits:
memory: 200M
cpus: '0.5'
restart: unless-stopped

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using watcher_monitoring.Data;
namespace watcher_monitoring.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAuthAttribute : Attribute, IAsyncActionFilter
{
private const string ApiKeyHeaderName = "X-API-Key";
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
context.Result = new UnauthorizedObjectResult(new { error = "API-Key fehlt im Header" });
return;
}
var apiKeyString = extractedApiKey.ToString();
var dbContext = context.HttpContext.RequestServices.GetRequiredService<WatcherDbContext>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiKeyAuthAttribute>>();
var apiKey = await dbContext.ApiKeys
.FirstOrDefaultAsync(k => k.Key == apiKeyString);
if (apiKey == null)
{
logger.LogWarning("Ungültiger API-Key verwendet: {ApiKey}", apiKeyString);
context.Result = new UnauthorizedObjectResult(new { error = "Ungültiger API-Key" });
return;
}
if (!apiKey.IsActive)
{
logger.LogWarning("Inaktiver API-Key verwendet: {Name}", apiKey.Name);
context.Result = new UnauthorizedObjectResult(new { error = "API-Key ist deaktiviert" });
return;
}
if (apiKey.IsExpired)
{
logger.LogWarning("Abgelaufener API-Key verwendet: {Name}", apiKey.Name);
context.Result = new UnauthorizedObjectResult(new { error = "API-Key ist abgelaufen" });
return;
}
// Letzten Verwendungszeitpunkt aktualisieren
apiKey.LastUsedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync();
logger.LogInformation("API-Zugriff mit Key: {Name}", apiKey.Name);
await next();
}
}

View File

@@ -1,2 +1,281 @@
// Get Methoden um Metrics abzugreifen
// Get Methden um Informationen über den Status des Servers einzuholen
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using watcher_monitoring.Models;
using watcher_monitoring.Data;
using watcher_monitoring.Attributes;
using watcher_monitoring.Payloads;
using System.Net;
namespace watcher_monitoring.Controllers;
[ApiController]
[Route("[controller]")]
[ApiKeyAuth]
public class APIController : Controller
{
private readonly WatcherDbContext _context;
private readonly ILogger<APIController> _logger;
public APIController(WatcherDbContext context, ILogger<APIController> logger)
{
_context = context;
_logger = logger;
}
// API Calls
[HttpGet("get-server")]
public async Task<IActionResult> Servers()
{
List<Server> servers = await _context.Servers.ToListAsync();
return Ok(servers);
}
// DEVELOPMENT ONLY
[HttpPost("add-server")]
public async Task<IActionResult> AddServer([FromBody] Server serverDto)
{
// payload check
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("Invalid server payload");
return BadRequest(new { error = "Invalid server payload", details = errors });
}
try
{
// Check if server with same IP already exists
var existingServer = await _context.Servers
.FirstOrDefaultAsync(s => s.IPAddress == serverDto.IPAddress);
if (existingServer != null)
{
_logger.LogWarning("Server mit IP-Adresse {ip} existiert bereits", serverDto.IPAddress);
return BadRequest(new { error = "Server mit dieser IP-Adresse existiert bereits" });
}
Server server = new Server
{
Name = serverDto.Name,
IPAddress = serverDto.IPAddress,
CpuType = serverDto.CpuType,
CpuCores = serverDto.CpuCores,
GpuType = serverDto.GpuType,
RamSize = serverDto.RamSize,
DiskSpace = serverDto.DiskSpace,
IsOnline = serverDto.IsOnline,
IsVerified = serverDto.IsVerified
};
_context.Servers.Add(server);
await _context.SaveChangesAsync();
_logger.LogInformation("Server '{name}' mit IP {ip} erfolgreich hinzugefügt", server.Name, server.IPAddress);
return Ok(new { message = "Server erfolgreich hinzugefügt", serverId = server.Id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Fehler beim Hinzufügen des Servers");
return BadRequest(new { error = "Fehler beim Hinzufügen des Servers", details = ex.Message });
}
}
[HttpDelete("delete-server/{id}")]
public async Task<IActionResult> DeleteServer(int id)
{
var server = await _context.Servers.FindAsync(id);
if (server == null)
{
_logger.LogError("Server nicht gefunden");
return BadRequest();
}
_context.Servers.Remove(server);
await _context.SaveChangesAsync();
_logger.LogInformation("Server '{server}' erfolgreich gelöscht", server.Name);
return Ok();
}
[HttpPut("edit-server")]
public async Task<IActionResult> EditServer()
{
return Ok();
}
// Container Calls
[HttpGet("get-container")]
public async Task<IActionResult> Containers()
{
List<Container> containers = await _context.Containers.ToListAsync();
return Ok(containers);
}
[HttpDelete("delete-container")]
public async Task<IActionResult> DeleteContainer(int id)
{
var container = await _context.Containers.FindAsync(id);
if (container == null)
{
_logger.LogError("Server nicht gefunden");
return BadRequest();
}
try
{
_context.Containers.Remove(container);
await _context.SaveChangesAsync();
_logger.LogInformation("Container '{container}' erfolgreich gelöscht", container.Id);
return Ok();
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return BadRequest();
}
}
// Agent Calls
// Registration Endpoint for watcher-agent
[HttpPost("agent-register")]
public async Task<IActionResult> Register([FromBody] RegistrationDto dto)
{
// payload check
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("Invalid registration payload");
return BadRequest(new { error = "Invalid registration payload", details = errors });
}
try {
Server newServer = new Server
{
Name = dto.hostName,
IPAddress = dto.ipAddress
};
_context.Servers.Add(newServer);
await _context.SaveChangesAsync();
var server = await _context.Servers.FindAsync(dto.ipAddress);
return Ok(server.Id);
} catch (Exception ex)
{
Console.WriteLine(ex.Message);
_logger.LogError(ex.Message);
return BadRequest();
}
}
// Hardware Configuration Endpoint for watcher-agent
[HttpPost("agent-hardware")]
public async Task<IActionResult> HardwareConfiguration ([FromBody] HardwareDto dto)
{
// payload check
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("Invalid hardware configuration");
return BadRequest(new { error = "Invalid Hardware Configuration Payload", details = errors });
}
try
{
// Find Server in Database
var server = await _context.Servers.FindAsync(dto.id);
if (server == null)
{
_logger.LogError("Server not found");
return BadRequest("Server not found");
}
// Add Hardware Configuration
server.CpuType = dto.cpuType;
server.CpuCores = dto.cpuCores;
server.GpuType = dto.gpuType;
server.RamSize = dto.ramSize;
// TODO: Diskspace fehlt
await _context.SaveChangesAsync();
_logger.LogInformation("Harware configuration successfull for server {server}", server.Name);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return BadRequest(ex.Message);
}
return Ok();
}
// Server-Metrics endpoint for watcher-agent
[HttpPost("agent-server-metrics/{id}")]
public async Task<IActionResult> ServerMetrics ([FromBody] MetricDto dto)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("Invalid monitoring payload");
return BadRequest(new { error = "Invalid monitoring payload", details = errors });
}
var server = await _context.Servers.FindAsync(dto.id);
if (server != null)
{
// neues Objekt mit Typ Metric anlegen
// Metric in Datenbank eintragen
return Ok();
}
else
{
_logger.LogError("metric cannot be added to database");
return BadRequest();
}
}
// Service-Detection endpoint for watcher-agent
[HttpPost("agent-container-detection")]
public async Task<IActionResult> ContainerDetection ([FromBody] HardwareDto dto)
{
return Ok();
}
// Container-Metrics endpoint for watcher-agent
[HttpPost("agent-container-metrics")]
public async Task<IActionResult> ContainerMetrics ([FromBody] HardwareDto dto)
{
return Ok();
}
}

View File

@@ -0,0 +1,153 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using watcher_monitoring.Data;
using watcher_monitoring.Models;
namespace watcher_monitoring.Controllers;
[ApiController]
[Route("[controller]")]
public class ApiKeyController : Controller
{
private readonly WatcherDbContext _context;
private readonly ILogger<ApiKeyController> _logger;
public ApiKeyController(WatcherDbContext context, ILogger<ApiKeyController> logger)
{
_context = context;
_logger = logger;
}
// Generiert einen neuen API-Key
private static string GenerateApiKey()
{
var randomBytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes).Replace("+", "").Replace("/", "").Replace("=", "");
}
[HttpPost("create")]
public async Task<IActionResult> CreateApiKey([FromBody] CreateApiKeyRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
{
return BadRequest(new { error = "Name ist erforderlich" });
}
var apiKey = new ApiKey
{
Key = GenerateApiKey(),
Name = request.Name,
Description = request.Description,
ExpiresAt = request.ExpiresAt,
IsActive = true
};
_context.ApiKeys.Add(apiKey);
await _context.SaveChangesAsync();
_logger.LogInformation("Neuer API-Key erstellt: {Name}", apiKey.Name);
return Ok(new
{
id = apiKey.Id,
key = apiKey.Key, // Wird nur einmal zurückgegeben!
name = apiKey.Name,
description = apiKey.Description,
createdAt = apiKey.CreatedAt,
expiresAt = apiKey.ExpiresAt,
isActive = apiKey.IsActive
});
}
[HttpGet("list")]
public async Task<IActionResult> ListApiKeys()
{
var apiKeys = await _context.ApiKeys
.OrderByDescending(k => k.CreatedAt)
.Select(k => new
{
id = k.Id,
name = k.Name,
description = k.Description,
createdAt = k.CreatedAt,
expiresAt = k.ExpiresAt,
lastUsedAt = k.LastUsedAt,
isActive = k.IsActive,
isExpired = k.IsExpired,
keyPreview = k.Key.Substring(0, Math.Min(8, k.Key.Length)) + "..." // Nur die ersten 8 Zeichen
})
.ToListAsync();
return Ok(apiKeys);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetApiKey(int id)
{
var apiKey = await _context.ApiKeys.FindAsync(id);
if (apiKey == null)
{
return NotFound(new { error = "API-Key nicht gefunden" });
}
return Ok(new
{
id = apiKey.Id,
name = apiKey.Name,
description = apiKey.Description,
createdAt = apiKey.CreatedAt,
expiresAt = apiKey.ExpiresAt,
lastUsedAt = apiKey.LastUsedAt,
isActive = apiKey.IsActive,
isExpired = apiKey.IsExpired,
keyPreview = apiKey.Key.Substring(0, Math.Min(8, apiKey.Key.Length)) + "..."
});
}
[HttpPut("{id}/toggle")]
public async Task<IActionResult> ToggleApiKey(int id)
{
var apiKey = await _context.ApiKeys.FindAsync(id);
if (apiKey == null)
{
return NotFound(new { error = "API-Key nicht gefunden" });
}
apiKey.IsActive = !apiKey.IsActive;
await _context.SaveChangesAsync();
_logger.LogInformation("API-Key {Name} wurde {Status}", apiKey.Name, apiKey.IsActive ? "aktiviert" : "deaktiviert");
return Ok(new { isActive = apiKey.IsActive });
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteApiKey(int id)
{
var apiKey = await _context.ApiKeys.FindAsync(id);
if (apiKey == null)
{
return NotFound(new { error = "API-Key nicht gefunden" });
}
_context.ApiKeys.Remove(apiKey);
await _context.SaveChangesAsync();
_logger.LogInformation("API-Key gelöscht: {Name}", apiKey.Name);
return Ok(new { message = "API-Key erfolgreich gelöscht" });
}
}
public class CreateApiKeyRequest
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime? ExpiresAt { get; set; }
}

View File

@@ -0,0 +1,133 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using watcher_monitoring.Data;
using watcher_monitoring.Models;
namespace watcher_monitoring.Controllers;
public class AuthController : Controller
{
private readonly WatcherDbContext _context;
private readonly ILogger<AuthController> _logger;
public AuthController(WatcherDbContext context, ILogger<AuthController> logger)
{
_context = context;
_logger = logger;
}
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string? returnUrl = null)
{
// Wenn der Benutzer bereits angemeldet ist, zur Startseite weiterleiten
if (User.Identity?.IsAuthenticated == true)
{
return RedirectToAction("Index", "Home");
}
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string? returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (!ModelState.IsValid)
{
return View(model);
}
try
{
// Benutzer suchen
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Username == model.Username && u.IsActive);
if (user == null)
{
_logger.LogWarning("Login-Versuch mit ungültigem Benutzernamen: {Username}", model.Username);
TempData["Error"] = "Ungültiger Benutzername oder Passwort";
return View(model);
}
// Passwort überprüfen (BCrypt)
if (!BCrypt.Net.BCrypt.Verify(model.Password, user.Password))
{
_logger.LogWarning("Login-Versuch mit falschem Passwort für Benutzer: {Username}", model.Username);
TempData["Error"] = "Ungültiger Benutzername oder Passwort";
return View(model);
}
// LastLogin aktualisieren
user.LastLogin = DateTime.UtcNow;
await _context.SaveChangesAsync();
// Claims erstellen
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email),
new Claim("LastLogin", user.LastLogin.ToString("o"))
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
var authProperties = new AuthenticationProperties
{
IsPersistent = model.RememberMe,
ExpiresUtc = model.RememberMe ? DateTimeOffset.UtcNow.AddDays(30) : DateTimeOffset.UtcNow.AddHours(8)
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
claimsPrincipal,
authProperties);
_logger.LogInformation("Benutzer {Username} erfolgreich angemeldet", user.Username);
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
catch (Exception ex)
{
_logger.LogError(ex, "Fehler beim Login-Vorgang");
TempData["Error"] = "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.";
return View(model);
}
}
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
var username = User.Identity?.Name ?? "Unbekannt";
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
_logger.LogInformation("Benutzer {Username} erfolgreich abgemeldet", username);
return RedirectToAction("Login", "Auth");
}
[AllowAnonymous]
public IActionResult AccessDenied()
{
return View();
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using watcher_monitoring.Models;
@@ -8,35 +9,30 @@ using Microsoft.EntityFrameworkCore;
namespace watcher_monitoring.Controllers;
[Authorize]
public class HomeController : Controller
{
private readonly WatcherDbContext _dbContext;
private readonly WatcherDbContext _context;
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger, WatcherDbContext dbContext)
{
_logger = logger;
_dbContext = dbContext;
_context = dbContext;
}
// Dashboard
public async Task<IActionResult> Index()
{
List<Server> servers = await _dbContext.Servers.ToListAsync();
List<Server> servers = await _context.Servers.ToListAsync();
List<Container> containers = await _context.Containers.ToListAsync();
var servers1 = new List<dynamic>
{
new { Name = "Web Server 01", IPAddress = "192.168.1.10", IsOnline = true },
new { Name = "Database Server", IPAddress = "192.168.1.20", IsOnline = true },
new { Name = "API Gateway", IPAddress = "192.168.1.30", IsOnline = true },
new { Name = "Cache Server", IPAddress = "192.168.1.40", IsOnline = false },
new { Name = "Backup Server", IPAddress = "192.168.1.50", IsOnline = true }
};
ViewBag.Containers = containers;
ViewBag.ContainerCount = containers.Count();
ViewBag.TotalServers = servers.Count;
ViewBag.OnlineServers = servers.Count(s => s.IsOnline);
ViewBag.OfflineServers = servers.Count(s => !s.IsOnline);
ViewBag.ServiceCount = 8;
ViewBag.Servers = servers;
return View();

View File

@@ -1,11 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using watcher_monitoring.Payloads;
using watcher_monitoring.Data;
using watcher_monitoring.Models;
namespace watcher_monitoring.Controllers;
[Authorize]
[Route("[controller]")]
public class MonitoringController : Controller
{
@@ -19,73 +19,10 @@ public class MonitoringController : Controller
_logger = logger;
}
// Registration Endpoint for watcher-agent
[HttpPost("registration")]
public async Task<IActionResult> Registration([FromBody] RegistrationDto dto)
[HttpGet("container")]
public async Task<IActionResult> ContainerIndex()
{
// payload check
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("Invalid registration payload");
return BadRequest(new { error = "Invalid registration payload", details = errors });
}
return Ok();
return View();
}
// Hardware Configuration Endpoint for watcher-agent
[HttpPost("hardware-configuration")]
public async Task<IActionResult> HardwareConfiguration ([FromBody] HardwareDto dto)
{
// payload check
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("Invalid hardware configuration");
return BadRequest(new { error = "Invalid Hardware Configuration Payload", details = errors });
}
try
{
// Find Server in Database
Server server = await _context.Servers.FindAsync(dto.Id);
// Add Hardware Configuration
try
{
server.CpuType = dto.CpuType;
server.CpuCores = dto.CpuCores;
server.GpuType = dto.GpuType;
server.RamSize = dto.RamSize;
// Diskspace fehlt
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
return Ok();
}
// Server-Metrics endpoint for watcher-agent
// Service-Detection endpoint for watcher-agent
// Service-Metrics endpoint for watcher-agent
}

View File

@@ -0,0 +1,311 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using watcher_monitoring.Data;
using watcher_monitoring.Models;
using BCrypt.Net;
namespace watcher_monitoring.Controllers;
[Authorize]
[Route("[controller]")]
public class UserController : Controller
{
private readonly WatcherDbContext _context;
private readonly ILogger<UserController> _logger;
public UserController(WatcherDbContext context, ILogger<UserController> logger)
{
_context = context;
_logger = logger;
}
// GET: /User
[HttpGet]
public async Task<IActionResult> Index()
{
var users = await _context.Users
.Include(u => u.ApiKeys)
.OrderByDescending(u => u.CreatedAt)
.ToListAsync();
return View(users);
}
// GET: /User/Details/{id}
[HttpGet("Details/{id}")]
public async Task<IActionResult> Details(int id)
{
var user = await _context.Users
.Include(u => u.ApiKeys)
.FirstOrDefaultAsync(u => u.Id == id);
if (user == null)
{
return NotFound();
}
return View(user);
}
// GET: /User/Create
[HttpGet("Create")]
public IActionResult Create()
{
return View();
}
// POST: /User/Create
[HttpPost("Create")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Username,Email,Password")] User user)
{
if (ModelState.IsValid)
{
// Prüfen, ob Username oder Email bereits existiert
var existingUser = await _context.Users
.FirstOrDefaultAsync(u => u.Username == user.Username || u.Email == user.Email);
if (existingUser != null)
{
if (existingUser.Username == user.Username)
{
ModelState.AddModelError("Username", "Benutzername ist bereits vergeben");
}
if (existingUser.Email == user.Email)
{
ModelState.AddModelError("Email", "E-Mail-Adresse ist bereits registriert");
}
return View(user);
}
// Passwort hashen mit BCrypt
user.Password = BCrypt.Net.BCrypt.HashPassword(user.Password);
user.CreatedAt = DateTime.UtcNow;
user.LastLogin = DateTime.UtcNow;
user.IsActive = true;
_context.Users.Add(user);
await _context.SaveChangesAsync();
_logger.LogInformation("Neuer User erstellt: {Username}", user.Username);
return RedirectToAction(nameof(Index));
}
return View(user);
}
// GET: /User/Edit/{id}
[HttpGet("Edit/{id}")]
public async Task<IActionResult> Edit(int id)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
return View(user);
}
// POST: /User/Edit/{id}
[HttpPost("Edit/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Username,Email,IsActive")] User updatedUser)
{
if (id != updatedUser.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
// Prüfen, ob neuer Username oder Email bereits von anderem User verwendet wird
var duplicate = await _context.Users
.FirstOrDefaultAsync(u => u.Id != id && (u.Username == updatedUser.Username || u.Email == updatedUser.Email));
if (duplicate != null)
{
if (duplicate.Username == updatedUser.Username)
{
ModelState.AddModelError("Username", "Benutzername ist bereits vergeben");
}
if (duplicate.Email == updatedUser.Email)
{
ModelState.AddModelError("Email", "E-Mail-Adresse ist bereits registriert");
}
return View(updatedUser);
}
user.Username = updatedUser.Username;
user.Email = updatedUser.Email;
user.IsActive = updatedUser.IsActive;
_context.Update(user);
await _context.SaveChangesAsync();
_logger.LogInformation("User aktualisiert: {Username}", user.Username);
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException)
{
if (!await UserExists(updatedUser.Id))
{
return NotFound();
}
throw;
}
}
return View(updatedUser);
}
// POST: /User/Delete/{id}
[HttpPost("Delete/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var user = await _context.Users
.Include(u => u.ApiKeys)
.FirstOrDefaultAsync(u => u.Id == id);
if (user == null)
{
return NotFound();
}
// Alle API-Keys des Users löschen
_context.ApiKeys.RemoveRange(user.ApiKeys);
_context.Users.Remove(user);
await _context.SaveChangesAsync();
_logger.LogInformation("User gelöscht: {Username}", user.Username);
return RedirectToAction(nameof(Index));
}
// POST: /User/ToggleActive/{id}
[HttpPost("ToggleActive/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ToggleActive(int id)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
user.IsActive = !user.IsActive;
await _context.SaveChangesAsync();
_logger.LogInformation("User {Username} wurde {Status}", user.Username, user.IsActive ? "aktiviert" : "deaktiviert");
return RedirectToAction(nameof(Details), new { id });
}
// POST: /User/GenerateApiKey/{id}
[HttpPost("GenerateApiKey/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> GenerateApiKey(int id, string keyName, string? description, DateTime? expiresAt)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
{
return NotFound();
}
if (string.IsNullOrWhiteSpace(keyName))
{
TempData["Error"] = "API-Key-Name ist erforderlich";
return RedirectToAction(nameof(Details), new { id });
}
var apiKey = new ApiKey
{
Key = GenerateApiKeyString(),
Name = keyName,
Description = description,
ExpiresAt = expiresAt,
IsActive = true,
UserId = user.Id
};
_context.ApiKeys.Add(apiKey);
await _context.SaveChangesAsync();
_logger.LogInformation("Neuer API-Key für User {Username} erstellt: {KeyName}", user.Username, keyName);
TempData["Success"] = $"API-Key erstellt: {apiKey.Key}";
TempData["NewApiKey"] = apiKey.Key;
return RedirectToAction(nameof(Details), new { id });
}
// POST: /User/DeleteApiKey/{userId}/{keyId}
[HttpPost("DeleteApiKey/{userId}/{keyId}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteApiKey(int userId, int keyId)
{
var apiKey = await _context.ApiKeys
.FirstOrDefaultAsync(k => k.Id == keyId && k.UserId == userId);
if (apiKey == null)
{
return NotFound();
}
_context.ApiKeys.Remove(apiKey);
await _context.SaveChangesAsync();
_logger.LogInformation("API-Key gelöscht: {KeyName}", apiKey.Name);
TempData["Success"] = "API-Key erfolgreich gelöscht";
return RedirectToAction(nameof(Details), new { id = userId });
}
// POST: /User/ToggleApiKey/{userId}/{keyId}
[HttpPost("ToggleApiKey/{userId}/{keyId}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ToggleApiKey(int userId, int keyId)
{
var apiKey = await _context.ApiKeys
.FirstOrDefaultAsync(k => k.Id == keyId && k.UserId == userId);
if (apiKey == null)
{
return NotFound();
}
apiKey.IsActive = !apiKey.IsActive;
await _context.SaveChangesAsync();
_logger.LogInformation("API-Key {KeyName} wurde {Status}", apiKey.Name, apiKey.IsActive ? "aktiviert" : "deaktiviert");
return RedirectToAction(nameof(Details), new { id = userId });
}
private async Task<bool> UserExists(int id)
{
return await _context.Users.AnyAsync(u => u.Id == id);
}
private static string GenerateApiKeyString()
{
var randomBytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes).Replace("+", "").Replace("/", "").Replace("=", "");
}
}

View File

@@ -19,4 +19,8 @@ public class WatcherDbContext : DbContext
public DbSet<Server> Servers { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Container> Containers { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
}

View File

@@ -0,0 +1,114 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using watcher_monitoring.Data;
#nullable disable
namespace watcher_monitoring.Migrations
{
[DbContext(typeof(WatcherDbContext))]
[Migration("20260108112653_containers-new")]
partial class containersnew
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("watcher_monitoring.Models.Container", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContainerName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Containers");
});
modelBuilder.Entity("watcher_monitoring.Models.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CpuCores")
.HasColumnType("INTEGER");
b.Property<string>("CpuType")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DiskSpace")
.HasColumnType("TEXT");
b.Property<string>("GpuType")
.HasColumnType("TEXT");
b.Property<string>("IPAddress")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsOnline")
.HasColumnType("INTEGER");
b.Property<bool>("IsVerified")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastSeen")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("RamSize")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("Servers");
});
modelBuilder.Entity("watcher_monitoring.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("LastLogin")
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace watcher_monitoring.Migrations
{
/// <inheritdoc />
public partial class containersnew : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Containers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ContainerName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Containers", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Containers");
}
}
}

View File

@@ -0,0 +1,150 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using watcher_monitoring.Data;
#nullable disable
namespace watcher_monitoring.Migrations
{
[DbContext(typeof(WatcherDbContext))]
[Migration("20260109080705_AddApiKeys")]
partial class AddApiKeys
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("watcher_monitoring.Models.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("watcher_monitoring.Models.Container", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContainerName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Containers");
});
modelBuilder.Entity("watcher_monitoring.Models.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CpuCores")
.HasColumnType("INTEGER");
b.Property<string>("CpuType")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DiskSpace")
.HasColumnType("TEXT");
b.Property<string>("GpuType")
.HasColumnType("TEXT");
b.Property<string>("IPAddress")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsOnline")
.HasColumnType("INTEGER");
b.Property<bool>("IsVerified")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastSeen")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("RamSize")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("Servers");
});
modelBuilder.Entity("watcher_monitoring.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("LastLogin")
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace watcher_monitoring.Migrations
{
/// <inheritdoc />
public partial class AddApiKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiKeys",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
LastUsedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKeys");
}
}
}

View File

@@ -0,0 +1,177 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using watcher_monitoring.Data;
#nullable disable
namespace watcher_monitoring.Migrations
{
[DbContext(typeof(WatcherDbContext))]
[Migration("20260109081821_AddUserApiKeyRelationship")]
partial class AddUserApiKeyRelationship
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("watcher_monitoring.Models.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("watcher_monitoring.Models.Container", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContainerName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Containers");
});
modelBuilder.Entity("watcher_monitoring.Models.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CpuCores")
.HasColumnType("INTEGER");
b.Property<string>("CpuType")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DiskSpace")
.HasColumnType("TEXT");
b.Property<string>("GpuType")
.HasColumnType("TEXT");
b.Property<string>("IPAddress")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsOnline")
.HasColumnType("INTEGER");
b.Property<bool>("IsVerified")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastSeen")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("RamSize")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("Servers");
});
modelBuilder.Entity("watcher_monitoring.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastLogin")
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("watcher_monitoring.Models.ApiKey", b =>
{
b.HasOne("watcher_monitoring.Models.User", "User")
.WithMany("ApiKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("watcher_monitoring.Models.User", b =>
{
b.Navigation("ApiKeys");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace watcher_monitoring.Migrations
{
/// <inheritdoc />
public partial class AddUserApiKeyRelationship : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Vorhandene API-Keys löschen, da sie keinem User zugeordnet werden können
migrationBuilder.Sql("DELETE FROM ApiKeys;");
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Users",
type: "TEXT",
nullable: false,
defaultValue: DateTime.UtcNow);
migrationBuilder.AddColumn<bool>(
name: "IsActive",
table: "Users",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "ApiKeys",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_ApiKeys_UserId",
table: "ApiKeys",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_ApiKeys_Users_UserId",
table: "ApiKeys",
column: "UserId",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ApiKeys_Users_UserId",
table: "ApiKeys");
migrationBuilder.DropIndex(
name: "IX_ApiKeys_UserId",
table: "ApiKeys");
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Users");
migrationBuilder.DropColumn(
name: "IsActive",
table: "Users");
migrationBuilder.DropColumn(
name: "UserId",
table: "ApiKeys");
}
}
}

View File

@@ -17,6 +17,63 @@ namespace watcher_monitoring.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("watcher_monitoring.Models.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("watcher_monitoring.Models.Container", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContainerName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Containers");
});
modelBuilder.Entity("watcher_monitoring.Models.Server", b =>
{
b.Property<int>("Id")
@@ -69,10 +126,16 @@ namespace watcher_monitoring.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastLogin")
.HasColumnType("TEXT");
@@ -89,6 +152,22 @@ namespace watcher_monitoring.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("watcher_monitoring.Models.ApiKey", b =>
{
b.HasOne("watcher_monitoring.Models.User", "User")
.WithMany("ApiKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("watcher_monitoring.Models.User", b =>
{
b.Navigation("ApiKeys");
});
#pragma warning restore 612, 618
}
}

View File

@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
namespace watcher_monitoring.Models;
public class ApiKey
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(64)]
public string Key { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
public DateTime? LastUsedAt { get; set; }
public bool IsActive { get; set; } = true;
public bool IsExpired => ExpiresAt.HasValue && ExpiresAt.Value < DateTime.UtcNow;
// Foreign Key: Jeder API-Key gehört zu einem User
public int UserId { get; set; }
// Navigation Property
public User User { get; set; } = null!;
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace watcher_monitoring.Models;
public class Container
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[StringLength(50)]
public required string ContainerName { get; set; } = null!;
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace watcher_monitoring.Models;
public class LoginViewModel
{
[Required(ErrorMessage = "Benutzername ist erforderlich")]
[Display(Name = "Benutzername")]
public string Username { get; set; } = string.Empty;
[Required(ErrorMessage = "Passwort ist erforderlich")]
[DataType(DataType.Password)]
[Display(Name = "Passwort")]
public string Password { get; set; } = string.Empty;
[Display(Name = "Angemeldet bleiben")]
public bool RememberMe { get; set; }
}

View File

@@ -22,4 +22,11 @@ public class User
[Required]
[DataType(DataType.Password)]
public required string Password { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsActive { get; set; } = true;
// Navigation Property: Ein User kann mehrere API-Keys haben
public ICollection<ApiKey> ApiKeys { get; set; } = new List<ApiKey>();
}

View File

@@ -6,21 +6,18 @@ namespace watcher_monitoring.Payloads;
public class HardwareDto
{
[Required]
public required int Id;
[Required]
public string? IpAddress { get; set; }
public required int id;
// Hardware Info
[Required]
public string? CpuType { get; set; }
public string? cpuType { get; set; }
[Required]
public int CpuCores { get; set; }
public int cpuCores { get; set; }
[Required]
public string? GpuType { get; set; }
public string? gpuType { get; set; }
[Required]
public double RamSize { get; set; }
public double ramSize { get; set; }
}

View File

@@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace watcher_monitoring.Payloads;
public class MetricDto
{
// Server Identity
[Required]
public int id { get; set; }
// Hardware Metrics
// CPU
public double cpuLoad { get; set; } // %
public double cpuTemp { get; set; } // deg C
// GPU
public double gpuLoad { get; set; } // %
public double gpuTemp { get; set; } // deg C
public double vRamSize { get; set; } // Bytes
public double vRamLoad { get; set; } // %
// RAM
public double ramSize { get; set; } // Bytes
public double ramLoad { get; set; } // %
// Disks
public double diskSize { get; set; } // Bytes
public double diskLoad { get; set; } // Bytes
public double diskTempp { get; set; } // deg C (if available)
// Network
public double netIn { get; set; } // Bytes/s
public double netOut { get; set; } // Bytes/s
}

View File

@@ -1,7 +1,13 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.CompilerServices;
namespace watcher_monitoring.Payloads;
public class RegistrationDto
{
public required string IpAddress;
public required string Key;
[Required]
public required string ipAddress { get; set; }
public required string hostName { get; set; }
}

View File

@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
@@ -41,6 +43,22 @@ builder.Services.AddDbContext<WatcherDbContext>((serviceProvider, options) =>
// Add services to the container.
builder.Services.AddControllersWithViews();
// Cookie-basierte Authentifizierung
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
options.LogoutPath = "/Auth/Logout";
options.AccessDeniedPath = "/Auth/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorization();
// Health Checks
builder.Services.AddHealthChecks();
@@ -48,10 +66,64 @@ builder.Services.AddHealthChecks();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Watcher-Server API", Version = "v1" });
// Nur API-Controller dokumentieren (mit [ApiController]-Attribut)
options.DocInclusionPredicate((docName, apiDesc) =>
{
var controllerActionDescriptor = apiDesc.ActionDescriptor as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor;
if (controllerActionDescriptor == null) return false;
// Nur Controller mit [ApiController]-Attribut einbeziehen
return controllerActionDescriptor.ControllerTypeInfo
.GetCustomAttributes(typeof(ApiControllerAttribute), true).Any();
});
});
var app = builder.Build();
// Stelle sicher, dass das persistence-Verzeichnis existiert
var persistenceDir = Path.Combine(Directory.GetCurrentDirectory(), "persistence");
if (!Directory.Exists(persistenceDir))
{
Log.Information("Erstelle persistence-Verzeichnis: {PersistenceDir}", persistenceDir);
Directory.CreateDirectory(persistenceDir);
}
// Datenbank-Migration beim Start ausführen
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<WatcherDbContext>();
try
{
Log.Information("Führe Datenbank-Migrationen aus...");
dbContext.Database.Migrate();
Log.Information("Datenbank-Migrationen erfolgreich angewendet");
// Standard-Admin-User erstellen, falls noch kein User existiert
if (!dbContext.Users.Any())
{
Log.Information("Erstelle Standard-Admin-User...");
var adminUser = new watcher_monitoring.Models.User
{
Username = "admin",
Email = "admin@watcher.local",
Password = BCrypt.Net.BCrypt.HashPassword("admin"),
IsActive = true,
CreatedAt = DateTime.UtcNow,
LastLogin = DateTime.UtcNow
};
dbContext.Users.Add(adminUser);
dbContext.SaveChanges();
Log.Information("Standard-Admin-User erstellt (Username: admin, Passwort: admin)");
}
}
catch (Exception ex)
{
Log.Error(ex, "Fehler beim Ausführen der Datenbank-Migrationen");
throw;
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
@@ -73,6 +145,7 @@ app.UseSwaggerUI(options =>
options.RoutePrefix = "api/v1/swagger";
});
app.UseAuthentication();
app.UseAuthorization();
// Health Check Endpoint

View File

@@ -0,0 +1,18 @@
@{
ViewData["Title"] = "Zugriff verweigert";
}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-body text-center">
<h1 class="display-1 text-danger">🚫</h1>
<h2 class="card-title">Zugriff verweigert</h2>
<p class="card-text">Sie haben keine Berechtigung, auf diese Seite zuzugreifen.</p>
<a asp-controller="Auth" asp-action="Login" class="btn btn-primary">Zurück zum Login</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,108 @@
@model watcher_monitoring.Models.LoginViewModel
@{
ViewData["Title"] = "Login";
Layout = null;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Watcher Monitoring</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
max-width: 400px;
width: 100%;
}
.login-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #333;
font-weight: 600;
}
.login-header p {
color: #666;
margin-top: 10px;
}
.btn-login {
width: 100%;
padding: 12px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.btn-login:hover {
background: linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%);
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h2>🔒 Watcher Monitoring</h2>
<p>Bitte melden Sie sich an</p>
</div>
@if (TempData["Error"] != null)
{
<div class="alert alert-danger" role="alert">
@TempData["Error"]
</div>
}
<form asp-action="Login" asp-controller="Auth" method="post">
<div class="form-group mb-3">
<label asp-for="Username" class="form-label">Benutzername</label>
<input asp-for="Username" class="form-control" placeholder="Benutzername eingeben" autofocus />
<span asp-validation-for="Username" class="text-danger"></span>
</div>
<div class="form-group mb-4">
<label asp-for="Password" class="form-label">Passwort</label>
<input asp-for="Password" type="password" class="form-control" placeholder="Passwort eingeben" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="RememberMe" type="checkbox" class="form-check-input" />
<label asp-for="RememberMe" class="form-check-label">
Angemeldet bleiben
</label>
</div>
<button type="submit" class="btn btn-primary btn-login">Anmelden</button>
</form>
</div>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
</body>
</html>

View File

@@ -33,14 +33,14 @@
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-label">Totoal Services</div>
<div class="metric-value">@ViewBag.ServiceCount</div>
<div class="metric-label">Total Containers</div>
<div class="metric-value">@ViewBag.ContainerCount</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="col-lg-4">
<div class="card">
<h2 class="card-title">Monitored Servers</h2>
<ul class="server-list">
@@ -68,6 +68,31 @@
</ul>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<h2 class="card-title">Monitored Containers</h2>
<ul class="server-list">
@if (ViewBag.Containers != null && ViewBag.Containers.Count > 0)
{
@foreach (var container in ViewBag.Containers)
{
<li class="server-item">
<div class="server-info">
<span class="server-name">@container.Name</span>
<span class="server-ip">Container Image</span>
</div>
</li>
}
}
else
{
<li class="text-center py-4" style="color: var(--text-muted)">
No Containers added yet
</li>
}
</ul>
</div>
</div>
<div class="col-lg-4">
<div class="card">

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -8,11 +9,13 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/watcher_monitoring.styles.css" asp-append-version="true" />
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid px-4">
<a class="navbar-brand fw-bold" asp-area="" asp-controller="Home" asp-action="Index">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="me-2" style="display: inline-block; vertical-align: middle;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
class="me-2" style="display: inline-block; vertical-align: middle;">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 6v6l4 2"></path>
</svg>
@@ -26,9 +29,47 @@
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">Settings</a>
</li>
@if (User.Identity?.IsAuthenticated == true)
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" style="display: inline-block; vertical-align: middle;">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
@User.Identity.Name
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<form asp-controller="Auth" asp-action="Logout" method="post" style="display:inline;">
@Html.AntiForgeryToken()
<button type="submit" class="dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
Abmelden
</button>
</form>
</li>
<li class="dropdown-item">
<a style="display: inline-block; vertical-align: middle; margin-right: 8px;"
asp-area="" asp-controller="User" asp-action="Index"><svg width="16" height="16"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>Settings</a>
</li>
</ul>
</li>
}
</ul>
</div>
</div>
@@ -49,4 +90,5 @@
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@@ -0,0 +1,47 @@
@model watcher_monitoring.Models.User
@{
ViewData["Title"] = "Neuer Benutzer";
}
<div class="container-fluid px-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="section-title mb-4">Neuen Benutzer erstellen</h1>
<div class="card">
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly" class="alert" style="background-color: rgba(248, 81, 73, 0.15); border: 1px solid var(--danger); color: var(--danger); border-radius: 6px; padding: 1rem; margin-bottom: 1rem;"></div>
<div class="mb-3">
<label asp-for="Username" class="form-label" style="color: var(--text-primary); font-weight: 500; margin-bottom: 0.5rem;">Benutzername</label>
<input asp-for="Username" class="form-control" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;" required />
<span asp-validation-for="Username" style="color: var(--danger); font-size: 0.875rem;"></span>
</div>
<div class="mb-3">
<label asp-for="Email" class="form-label" style="color: var(--text-primary); font-weight: 500; margin-bottom: 0.5rem;">E-Mail-Adresse</label>
<input asp-for="Email" type="email" class="form-control" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;" required />
<span asp-validation-for="Email" style="color: var(--danger); font-size: 0.875rem;"></span>
</div>
<div class="mb-3">
<label asp-for="Password" class="form-label" style="color: var(--text-primary); font-weight: 500; margin-bottom: 0.5rem;">Passwort</label>
<input asp-for="Password" type="password" class="form-control" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;" required />
<span asp-validation-for="Password" style="color: var(--danger); font-size: 0.875rem;"></span>
<div style="color: var(--text-muted); font-size: 0.875rem; margin-top: 0.25rem;">Mindestens 8 Zeichen empfohlen</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Erstellen</button>
<a asp-action="Index" class="btn" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">Abbrechen</a>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,217 @@
@model watcher_monitoring.Models.User
@{
ViewData["Title"] = "Benutzerdetails";
}
<div class="container-fluid px-4">
<div class="row">
<div class="col-lg-10">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="section-title mb-0">Benutzerdetails</h1>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn" style="background-color: var(--warning); color: #fff; margin-right: 0.5rem;">Bearbeiten</a>
<a asp-action="Index" class="btn" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary);">Zurück</a>
</div>
</div>
@if (TempData["Success"] != null)
{
<div class="alert" style="background-color: rgba(63, 185, 80, 0.15); border: 1px solid var(--success); color: var(--success); border-radius: 6px; padding: 1rem; margin-bottom: 1rem; position: relative;">
@TempData["Success"]
@if (TempData["NewApiKey"] != null)
{
<hr style="border-color: var(--border-color); margin: 1rem 0;">
<strong>API-Key (wird nur einmal angezeigt!):</strong>
<div class="input-group mt-2" style="display: flex; gap: 0.5rem;">
<input type="text" class="form-control font-monospace" value="@TempData["NewApiKey"]" id="newApiKey" readonly style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px; flex: 1;">
<button class="btn" type="button" onclick="copyApiKey()" style="background-color: var(--accent-primary); color: #fff; border: none; padding: 0.75rem 1rem; border-radius: 6px;">
Kopieren
</button>
</div>
}
<button type="button" class="btn-close" data-bs-dismiss="alert" style="position: absolute; top: 1rem; right: 1rem; color: var(--success);"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert" style="background-color: rgba(248, 81, 73, 0.15); border: 1px solid var(--danger); color: var(--danger); border-radius: 6px; padding: 1rem; margin-bottom: 1rem; position: relative;">
@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert" style="position: absolute; top: 1rem; right: 1rem; color: var(--danger);"></button>
</div>
}
<div class="card mb-4">
<h2 class="card-title">Benutzerinformationen</h2>
<div style="color: var(--text-secondary);">
<div class="row mb-3" style="padding-bottom: 1rem; border-bottom: 1px solid var(--border-color);">
<div class="col-sm-4"><strong style="color: var(--text-primary);">Benutzername:</strong></div>
<div class="col-sm-8" style="color: var(--text-secondary);">@Model.Username</div>
</div>
<div class="row mb-3" style="padding-bottom: 1rem; border-bottom: 1px solid var(--border-color);">
<div class="col-sm-4"><strong style="color: var(--text-primary);">E-Mail:</strong></div>
<div class="col-sm-8" style="color: var(--text-secondary);">@Model.Email</div>
</div>
<div class="row mb-3" style="padding-bottom: 1rem; border-bottom: 1px solid var(--border-color);">
<div class="col-sm-4"><strong style="color: var(--text-primary);">Status:</strong></div>
<div class="col-sm-8">
@if (Model.IsActive)
{
<span class="status-badge status-online">Aktiv</span>
}
else
{
<span class="status-badge status-offline">Inaktiv</span>
}
</div>
</div>
<div class="row mb-3" style="padding-bottom: 1rem; border-bottom: 1px solid var(--border-color);">
<div class="col-sm-4"><strong style="color: var(--text-primary);">Erstellt am:</strong></div>
<div class="col-sm-8" style="color: var(--text-secondary);">@Model.CreatedAt.ToString("dd.MM.yyyy HH:mm:ss")</div>
</div>
<div class="row mb-3" style="padding-bottom: 1rem; border-bottom: 1px solid var(--border-color);">
<div class="col-sm-4"><strong style="color: var(--text-primary);">Letzter Login:</strong></div>
<div class="col-sm-8" style="color: var(--text-secondary);">@Model.LastLogin.ToString("dd.MM.yyyy HH:mm:ss")</div>
</div>
<div class="row">
<div class="col-sm-12">
<form asp-action="ToggleActive" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm" style="background-color: @(Model.IsActive ? "var(--warning)" : "var(--success)"); color: #fff; margin-right: 0.5rem;">
@(Model.IsActive ? "Deaktivieren" : "Aktivieren")
</button>
</form>
<form asp-action="Delete" asp-route-id="@Model.Id" method="post" class="d-inline" onsubmit="return confirm('Möchten Sie diesen Benutzer wirklich löschen? Alle API-Keys werden ebenfalls gelöscht.');">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm" style="background-color: var(--danger); color: #fff;">Löschen</button>
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="card-title mb-0">API-Keys (@Model.ApiKeys.Count)</h2>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createApiKeyModal">
Neuer API-Key
</button>
</div>
@if (Model.ApiKeys.Any())
{
<div class="table-responsive">
<table class="table table-hover" style="color: var(--text-primary); margin-bottom: 0;">
<thead style="border-bottom: 1px solid var(--border-color);">
<tr>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Name</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Beschreibung</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Status</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Erstellt</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Läuft ab</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Zuletzt verwendet</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Aktionen</th>
</tr>
</thead>
<tbody>
@foreach (var key in Model.ApiKeys.OrderByDescending(k => k.CreatedAt))
{
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 1rem;"><strong style="color: var(--text-primary);">@key.Name</strong></td>
<td style="padding: 1rem; color: var(--text-secondary);">@(key.Description ?? "-")</td>
<td style="padding: 1rem;">
@if (key.IsExpired)
{
<span class="status-badge status-offline">Abgelaufen</span>
}
else if (!key.IsActive)
{
<span class="status-badge" style="background-color: rgba(139, 148, 158, 0.15); color: var(--text-secondary); border: 1px solid var(--text-secondary);">Inaktiv</span>
}
else
{
<span class="status-badge status-online">Aktiv</span>
}
</td>
<td style="padding: 1rem; color: var(--text-secondary);">@key.CreatedAt.ToString("dd.MM.yyyy")</td>
<td style="padding: 1rem; color: var(--text-secondary);">@(key.ExpiresAt?.ToString("dd.MM.yyyy") ?? "Nie")</td>
<td style="padding: 1rem; color: var(--text-secondary);">@(key.LastUsedAt?.ToString("dd.MM.yyyy HH:mm") ?? "Nie")</td>
<td style="padding: 1rem;">
<form asp-action="ToggleApiKey" asp-route-userId="@Model.Id" asp-route-keyId="@key.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm" style="background-color: @(key.IsActive ? "var(--warning)" : "var(--success)"); color: #fff; margin-right: 0.5rem;">
@(key.IsActive ? "Deaktivieren" : "Aktivieren")
</button>
</form>
<form asp-action="DeleteApiKey" asp-route-userId="@Model.Id" asp-route-keyId="@key.Id" method="post" class="d-inline" onsubmit="return confirm('Möchten Sie diesen API-Key wirklich löschen?');">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm" style="background-color: var(--danger); color: #fff;">Löschen</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<p style="color: var(--text-muted); margin-bottom: 0;">Noch keine API-Keys vorhanden.</p>
}
</div>
</div>
</div>
</div>
<!-- Modal für neuen API-Key -->
<div class="modal fade" id="createApiKeyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content" style="background-color: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);">
<form asp-action="GenerateApiKey" asp-route-id="@Model.Id" method="post">
@Html.AntiForgeryToken()
<div class="modal-header" style="border-bottom: 1px solid var(--border-color);">
<h5 class="modal-title" style="color: var(--text-primary);">Neuen API-Key erstellen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" style="filter: invert(1);"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="keyName" class="form-label" style="color: var(--text-primary); font-weight: 500;">Name *</label>
<input type="text" class="form-control" id="keyName" name="keyName" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;" required>
<div style="color: var(--text-muted); font-size: 0.875rem; margin-top: 0.25rem;">Ein eindeutiger Name für diesen API-Key</div>
</div>
<div class="mb-3">
<label for="description" class="form-label" style="color: var(--text-primary); font-weight: 500;">Beschreibung</label>
<textarea class="form-control" id="description" name="description" rows="2" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;"></textarea>
</div>
<div class="mb-3">
<label for="expiresAt" class="form-label" style="color: var(--text-primary); font-weight: 500;">Ablaufdatum (optional)</label>
<input type="datetime-local" class="form-control" id="expiresAt" name="expiresAt" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;">
<div style="color: var(--text-muted); font-size: 0.875rem; margin-top: 0.25rem;">Leer lassen für unbegrenzte Gültigkeit</div>
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid var(--border-color);">
<button type="button" class="btn" data-bs-dismiss="modal" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary);">Abbrechen</button>
<button type="submit" class="btn btn-primary">API-Key erstellen</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
function copyApiKey() {
var copyText = document.getElementById("newApiKey");
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
var btn = event.target;
var originalText = btn.textContent;
btn.textContent = "Kopiert!";
setTimeout(function() {
btn.textContent = originalText;
}, 2000);
}
</script>
}

View File

@@ -0,0 +1,46 @@
@model watcher_monitoring.Models.User
@{
ViewData["Title"] = "Benutzer bearbeiten";
}
<div class="container-fluid px-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="section-title mb-4">Benutzer bearbeiten</h1>
<div class="card">
<form asp-action="Edit" method="post">
<div asp-validation-summary="ModelOnly" class="alert" style="background-color: rgba(248, 81, 73, 0.15); border: 1px solid var(--danger); color: var(--danger); border-radius: 6px; padding: 1rem; margin-bottom: 1rem;"></div>
<input type="hidden" asp-for="Id" />
<div class="mb-3">
<label asp-for="Username" class="form-label" style="color: var(--text-primary); font-weight: 500; margin-bottom: 0.5rem;">Benutzername</label>
<input asp-for="Username" class="form-control" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;" required />
<span asp-validation-for="Username" style="color: var(--danger); font-size: 0.875rem;"></span>
</div>
<div class="mb-3">
<label asp-for="Email" class="form-label" style="color: var(--text-primary); font-weight: 500; margin-bottom: 0.5rem;">E-Mail-Adresse</label>
<input asp-for="Email" type="email" class="form-control" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.75rem; border-radius: 6px;" required />
<span asp-validation-for="Email" style="color: var(--danger); font-size: 0.875rem;"></span>
</div>
<div class="mb-3 form-check">
<input asp-for="IsActive" type="checkbox" class="form-check-input" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color);" />
<label asp-for="IsActive" class="form-check-label" style="color: var(--text-primary); margin-left: 0.5rem;">Benutzer ist aktiv</label>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Speichern</button>
<a asp-action="Details" asp-route-id="@Model.Id" class="btn" style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">Abbrechen</a>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,62 @@
@model IEnumerable<watcher_monitoring.Models.User>
@{
ViewData["Title"] = "Benutzerverwaltung";
}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="section-title mb-0">Benutzerverwaltung</h1>
<a asp-action="Create" class="btn btn-primary">
Neuer Benutzer
</a>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover" style="color: var(--text-primary); margin-bottom: 0;">
<thead style="border-bottom: 1px solid var(--border-color);">
<tr>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Benutzername</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">E-Mail</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Status</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">API-Keys</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Erstellt am</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Letzter Login</th>
<th style="color: var(--text-secondary); font-weight: 600; padding: 1rem;">Aktionen</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model)
{
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 1rem;">
<strong style="color: var(--text-primary);">@user.Username</strong>
</td>
<td style="padding: 1rem; color: var(--text-secondary);">@user.Email</td>
<td style="padding: 1rem;">
@if (user.IsActive)
{
<span class="status-badge status-online">Aktiv</span>
}
else
{
<span class="status-badge status-offline">Inaktiv</span>
}
</td>
<td style="padding: 1rem;">
<span class="status-badge" style="background-color: rgba(88, 166, 255, 0.15); color: var(--info); border: 1px solid var(--info);">@user.ApiKeys.Count</span>
</td>
<td style="padding: 1rem; color: var(--text-secondary);">@user.CreatedAt.ToString("dd.MM.yyyy HH:mm")</td>
<td style="padding: 1rem; color: var(--text-secondary);">@user.LastLogin.ToString("dd.MM.yyyy HH:mm")</td>
<td style="padding: 1rem;">
<a asp-action="Details" asp-route-id="@user.Id" class="btn btn-sm" style="background-color: var(--info); color: #fff; margin-right: 0.5rem;">Details</a>
<a asp-action="Edit" asp-route-id="@user.Id" class="btn btn-sm" style="background-color: var(--warning); color: #fff;">Bearbeiten</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

Binary file not shown.

Binary file not shown.