diff --git a/Endpoints/AdminEndpoint.cs b/Endpoints/AdminEndpoint.cs index f03bdc4..01f33b7 100644 --- a/Endpoints/AdminEndpoint.cs +++ b/Endpoints/AdminEndpoint.cs @@ -18,6 +18,7 @@ public static class AdminEndpoint app.MapPut("/api/admin/users/{userId}/password", HandleResetPassword); app.MapDelete("/api/admin/users/{userId}", HandleDeleteUser); app.MapPost("/api/admin/users/{userId}/impersonate", HandleImpersonate); + app.MapPost("/api/admin/users/{userId}/login-link", HandleLoginLink); } private static async System.Threading.Tasks.Task HandleListUsers(HttpContext ctx, DatabaseService db) @@ -250,4 +251,26 @@ public static class AdminEndpoint isAdmin = user.IsAdmin }); } + + private static async System.Threading.Tasks.Task HandleLoginLink(HttpContext ctx, DatabaseService db, AuthService auth, string userId) + { + if (!ctx.IsAdmin()) + { + ctx.Response.StatusCode = 403; + await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" }); + return; + } + + var user = await db.GetUser(userId); + if (user == null) + { + ctx.Response.StatusCode = 404; + await ctx.Response.WriteAsJsonAsync(new { error = "Benutzer nicht gefunden" }); + return; + } + + var token = auth.CreateToken(user.UserId); + + await ctx.Response.WriteAsJsonAsync(new { token }); + } } diff --git a/Endpoints/AuthEndpoint.cs b/Endpoints/AuthEndpoint.cs index 5550c65..da1101b 100644 --- a/Endpoints/AuthEndpoint.cs +++ b/Endpoints/AuthEndpoint.cs @@ -13,6 +13,7 @@ public static class AuthEndpoint app.MapPost("/api/auth/login", HandleLogin); app.MapPost("/api/auth/logout", HandleLogout); app.MapGet("/api/auth/me", HandleMe); + app.MapGet("/api/auth/autologin", HandleAutologin); } private static async System.Threading.Tasks.Task HandleLogin(HttpContext ctx, DatabaseService db, AuthService auth) @@ -97,4 +98,33 @@ public static class AuthEndpoint defaultPath = user.DefaultPath }); } + + private static System.Threading.Tasks.Task HandleAutologin(HttpContext ctx, AuthService auth) + { + var token = ctx.Request.Query["token"].ToString(); + + if (string.IsNullOrEmpty(token)) + { + ctx.Response.StatusCode = 400; + return ctx.Response.WriteAsync("Token fehlt"); + } + + var userId = auth.ValidateToken(token); + if (userId == null) + { + ctx.Response.StatusCode = 401; + return ctx.Response.WriteAsync("Ungültiger Token"); + } + + ctx.Response.Cookies.Append("gott_token", token, new CookieOptions + { + HttpOnly = true, + SameSite = SameSiteMode.Strict, + Path = "/", + MaxAge = System.TimeSpan.FromDays(365) + }); + + ctx.Response.Redirect("/"); + return System.Threading.Tasks.Task.CompletedTask; + } } diff --git a/Middleware/AuthMiddleware.cs b/Middleware/AuthMiddleware.cs index 41fef6b..5185263 100644 --- a/Middleware/AuthMiddleware.cs +++ b/Middleware/AuthMiddleware.cs @@ -20,7 +20,8 @@ public class AuthMiddleware // Static files, login and telegram webhook don't require auth var isTelegramWebhook = path.StartsWith("/api/telegram/webhook/") && ctx.Request.Method == "POST"; var isSharedGet = path.StartsWith("/api/shared/") && ctx.Request.Method == "GET"; - if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet) + var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET"; + if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin) { await _next(ctx); return; diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index abcb465..e833a75 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -610,6 +610,49 @@ label.skill-item { cursor: pointer; } +/* ── Login Link Modal ── */ + +.login-link-overlay { + position: fixed; + inset: 0; + background: rgb(from var(--f1-bg) r g b / var(--semi-transparent)); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.login-link-modal { + background: var(--f2-bg); + border: var(--border-width) solid var(--f2-border); + border-radius: var(--border-radius); + width: 90vw; + max-width: var(--modal-width); + padding: var(--box-padding); + display: flex; + flex-direction: column; + gap: var(--box-spacing); +} + +.login-link-modal h3 { + margin: 0; +} + +.login-link-desc { + opacity: var(--semi-transparent); + margin: 0; +} + +.login-link-input-row { + display: flex; + gap: var(--badge-spacing); +} + +.login-link-input { + flex: 1; + min-width: 0; +} + /* ── EasyMDE in Formularen ── */ /* Nutzt fe-mde-wrap Styles aus file-explorer.css für Dark-Theme, überschreibt aber die Fullscreen-Layout-Regeln für den Formular-Kontext. */ diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index bdea648..b39c069 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -472,6 +472,15 @@ async function apiAdminImpersonate(userId) { return resp.json(); } +async function apiAdminGetLoginLink(userId) { + const resp = await fetch('/api/admin/users/' + userId + '/login-link', { method: 'POST' }); + if (!resp.ok) { + const err = await resp.json(); + throw new Error(err.error || 'Fehler beim Erstellen des Login-Links'); + } + return resp.json(); +} + // ── Krea Image API ── async function apiKreaStatus() { diff --git a/wwwroot/js/pages/admin-page.js b/wwwroot/js/pages/admin-page.js index efcecf9..8ab0fae 100644 --- a/wwwroot/js/pages/admin-page.js +++ b/wwwroot/js/pages/admin-page.js @@ -79,16 +79,16 @@ registerPage('admin', { }); actions.appendChild(editBtn); - // Login as User (nicht für sich selbst) + // Login-Link + Löschen (nicht für sich selbst) if (user.userId !== currentUser.userId) { - var loginAsBtn = document.createElement('button'); - loginAsBtn.className = 'icon-btn'; - loginAsBtn.title = 'Als ' + (user.displayName || user.username) + ' anmelden'; - loginAsBtn.innerHTML = ''; - loginAsBtn.addEventListener('click', function() { - impersonateUser(user); + var linkBtn = document.createElement('button'); + linkBtn.className = 'icon-btn'; + linkBtn.title = 'Login-Link generieren'; + linkBtn.innerHTML = ''; + linkBtn.addEventListener('click', function() { + generateLoginLink(user); }); - actions.appendChild(loginAsBtn); + actions.appendChild(linkBtn); var deleteBtn = document.createElement('button'); deleteBtn.className = 'icon-btn'; @@ -146,20 +146,81 @@ registerPage('admin', { } } - async function impersonateUser(user) { - if (!confirm('Als "' + (user.displayName || user.username) + '" anmelden?\n\nDu wirst abgemeldet und als dieser Benutzer eingeloggt.')) { - return; - } - + async function generateLoginLink(user) { try { - await apiAdminImpersonate(user.userId); - // Seite neu laden, damit der neue User-Kontext geladen wird - location.reload(); + var result = await apiAdminGetLoginLink(user.userId); + var link = window.location.origin + '/api/auth/autologin?token=' + result.token; + showLoginLinkModal(user, link); } catch (err) { showPageMessage(err.message, true); } } + function showLoginLinkModal(user, link) { + // Entferne vorherigen Modal falls vorhanden + var existing = document.querySelector('.login-link-overlay'); + if (existing) existing.remove(); + + var overlay = document.createElement('div'); + overlay.className = 'login-link-overlay'; + + var modal = document.createElement('div'); + modal.className = 'login-link-modal'; + + var title = document.createElement('h3'); + title.textContent = 'Login-Link für ' + (user.displayName || user.username); + + var desc = document.createElement('p'); + desc.className = 'login-link-desc'; + desc.textContent = 'Dieser Link loggt automatisch als dieser Benutzer ein. Teile ihn nur mit vertrauenswürdigen Personen.'; + + var inputRow = document.createElement('div'); + inputRow.className = 'login-link-input-row'; + + var input = document.createElement('input'); + input.type = 'text'; + input.readOnly = true; + input.value = link; + input.className = 'login-link-input'; + + var copyBtn = document.createElement('button'); + copyBtn.className = 'btn btn-primary'; + copyBtn.innerHTML = ' Kopieren'; + copyBtn.addEventListener('click', function() { + navigator.clipboard.writeText(link).then(function() { + copyBtn.innerHTML = ' Kopiert!'; + setTimeout(function() { + copyBtn.innerHTML = ' Kopieren'; + }, 2000); + }); + }); + + inputRow.appendChild(input); + inputRow.appendChild(copyBtn); + + var closeBtn = document.createElement('button'); + closeBtn.className = 'btn'; + closeBtn.textContent = 'Schließen'; + closeBtn.addEventListener('click', function() { + overlay.remove(); + }); + + modal.appendChild(title); + modal.appendChild(desc); + modal.appendChild(inputRow); + modal.appendChild(closeBtn); + overlay.appendChild(modal); + + overlay.addEventListener('click', function(e) { + if (e.target === overlay) overlay.remove(); + }); + + document.body.appendChild(overlay); + + // Input selektieren für einfaches Kopieren + input.select(); + } + // ── Event-Listener ── // Default Path Browse