Admin: Login-Link mit window.location.origin statt Server-Host
- Backend gibt nur Token zurück statt fertigen Link - Frontend baut Link via window.location.origin zusammen - Zeigt korrekte Domain/Port statt internes http://127.0.0.1 Backend: - AdminEndpoint: POST /api/admin/users/{userId}/login-link gibt { token } - AuthEndpoint: GET /api/auth/autologin?token=xxx setzt Cookie + redirect - AuthMiddleware: autologin-Route von Auth ausgenommen Frontend: - api.js: apiAdminGetLoginLink() Funktion - admin-page.js: Login-as-User Button → Login-Link Button mit Modal - form.css: Styling für Login-Link Modal
This commit is contained in:
1 parent
eeecd3da62
commit
1b9cc29421
6 files changed
+184
-17
No files matched your search
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = '<i class="fa fa-sign-in"></i>';
|
||||
loginAsBtn.addEventListener('click', function() {
|
||||
impersonateUser(user);
|
||||
var linkBtn = document.createElement('button');
|
||||
linkBtn.className = 'icon-btn';
|
||||
linkBtn.title = 'Login-Link generieren';
|
||||
linkBtn.innerHTML = '<i class="fa fa-link"></i>';
|
||||
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 = '<i class="fa fa-copy"></i> Kopieren';
|
||||
copyBtn.addEventListener('click', function() {
|
||||
navigator.clipboard.writeText(link).then(function() {
|
||||
copyBtn.innerHTML = '<i class="fa fa-check"></i> Kopiert!';
|
||||
setTimeout(function() {
|
||||
copyBtn.innerHTML = '<i class="fa fa-copy"></i> 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
|
||||
|
||||
Reference in new issue
Block a user