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