update usermanager
This commit is contained in:
@@ -401,6 +401,49 @@
|
||||
}, { hideReason: true, confirmText: 'Set Password', unsafeContent: true });
|
||||
};
|
||||
|
||||
window.adminRenameUser = async (btn) => {
|
||||
const id = btn.dataset.id;
|
||||
const currentName = btn.dataset.name;
|
||||
const currentUsername = btn.dataset.username;
|
||||
|
||||
if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded');
|
||||
|
||||
ModAction.confirm(
|
||||
'Rename User',
|
||||
'Enter a new login name for <strong>' + escHTML(currentName) + '</strong>.<br>' +
|
||||
'<small style="color:#888;">Current login: <code>' + escHTML(currentUsername) + '</code> — All uploads will be reassigned. User sessions will be invalidated. And the user has to login with the NEW name from now on.</small>',
|
||||
async (newUsername) => {
|
||||
const data = await post('/api/v2/admin/users/rename', { user_id: id, new_username: newUsername });
|
||||
if (data.success) {
|
||||
showFlash(data.msg, 'success');
|
||||
// Update the row in-place: links, text, and all button data attributes
|
||||
const row = document.getElementById('user-row-' + id);
|
||||
if (row) {
|
||||
// Update the name link
|
||||
const link = row.querySelector('.user-info-cell a');
|
||||
if (link) {
|
||||
link.href = '/user/' + data.new_login;
|
||||
// Only overwrite text if there's no display_name (plain username link)
|
||||
if (!link.querySelector('span[style*="accent"]')) {
|
||||
link.textContent = data.new_user;
|
||||
}
|
||||
}
|
||||
// Update all buttons in the row with the new name/username
|
||||
row.querySelectorAll('[data-username]').forEach(el => { el.dataset.username = data.new_login; });
|
||||
row.querySelectorAll('[data-name]').forEach(el => { el.dataset.name = data.new_user; });
|
||||
// Update activity stat links
|
||||
row.querySelectorAll('a[href^="/user/"]').forEach(a => {
|
||||
a.href = a.href.replace(/\/user\/[^/]+/, '/user/' + data.new_login);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.msg || 'Rename failed');
|
||||
}
|
||||
},
|
||||
{ hideReason: false, singleLine: true, confirmText: 'Rename', placeholder: 'new username' }
|
||||
);
|
||||
};
|
||||
|
||||
window.adminDeleteUser = async (btn) => {
|
||||
const id = btn.dataset.id;
|
||||
const name = btn.dataset.name;
|
||||
|
||||
@@ -7249,16 +7249,15 @@ class ModAction {
|
||||
if (!modal) return window.flashMessage('Error: Mod modal not found', 3000, 'error');
|
||||
const titleEl = document.getElementById('mod-action-title');
|
||||
const contentEl = document.getElementById('mod-action-content');
|
||||
const textareaEl = document.getElementById('mod-reason');
|
||||
const inputEl = document.getElementById('mod-reason-input');
|
||||
const reasonEl = document.getElementById('mod-reason');
|
||||
const confirmBtn = document.getElementById('mod-action-confirm');
|
||||
const cancelBtn = document.getElementById('mod-action-cancel');
|
||||
const errorEl = document.getElementById('mod-action-error');
|
||||
|
||||
// Pick the active input element based on singleLine option
|
||||
const singleLine = options.singleLine || false;
|
||||
const reasonEl = singleLine ? inputEl : textareaEl;
|
||||
const inactiveEl = singleLine ? textareaEl : inputEl;
|
||||
const hideReason = options.hideReason || false;
|
||||
const allowEmpty = options.allowEmpty || false;
|
||||
const i18n = window.f0ckI18n || {};
|
||||
|
||||
titleEl.innerText = title;
|
||||
if (options.unsafeContent) {
|
||||
@@ -7266,15 +7265,23 @@ class ModAction {
|
||||
} else {
|
||||
contentEl.innerHTML = Sanitizer.clean(promptHtml);
|
||||
}
|
||||
|
||||
reasonEl.value = '';
|
||||
inactiveEl.value = '';
|
||||
inactiveEl.style.display = 'none';
|
||||
errorEl.style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
|
||||
const hideReason = options.hideReason || false;
|
||||
const allowEmpty = options.allowEmpty || false;
|
||||
const i18n = window.f0ckI18n || {};
|
||||
// Apply single-line mode: style the textarea to look and act like a text input
|
||||
if (singleLine) {
|
||||
reasonEl.rows = 1;
|
||||
reasonEl.style.resize = 'none';
|
||||
reasonEl.style.overflow = 'hidden';
|
||||
reasonEl.style.height = 'auto';
|
||||
} else {
|
||||
reasonEl.rows = 3;
|
||||
reasonEl.style.resize = '';
|
||||
reasonEl.style.overflow = '';
|
||||
reasonEl.style.height = '';
|
||||
}
|
||||
|
||||
if (hideReason) {
|
||||
reasonEl.style.display = 'none';
|
||||
@@ -7315,24 +7322,19 @@ class ModAction {
|
||||
}
|
||||
};
|
||||
|
||||
// Block Enter from inserting newlines in single-line mode; instead submit
|
||||
const enterHandler = singleLine ? (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); onConfirm(); }
|
||||
} : null;
|
||||
if (enterHandler) reasonEl.addEventListener('keydown', enterHandler);
|
||||
|
||||
const cleanup = () => {
|
||||
confirmBtn.onclick = null;
|
||||
cancelBtn.onclick = null;
|
||||
if (singleLine && inputEl._enterHandler) {
|
||||
inputEl.removeEventListener('keydown', inputEl._enterHandler);
|
||||
inputEl._enterHandler = null;
|
||||
}
|
||||
if (enterHandler) reasonEl.removeEventListener('keydown', enterHandler);
|
||||
confirmBtn.disabled = false;
|
||||
};
|
||||
|
||||
// For single-line input: submit on Enter
|
||||
if (singleLine && !hideReason) {
|
||||
inputEl._enterHandler = (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); onConfirm(); }
|
||||
};
|
||||
inputEl.addEventListener('keydown', inputEl._enterHandler);
|
||||
}
|
||||
|
||||
confirmBtn.onclick = onConfirm;
|
||||
cancelBtn.onclick = close;
|
||||
|
||||
|
||||
@@ -1272,6 +1272,71 @@ export default (router, tpl) => {
|
||||
});
|
||||
|
||||
|
||||
router.post(/^\/api\/v2\/admin\/users\/rename\/?$/, lib.auth, async (req, res) => {
|
||||
try {
|
||||
const { user_id, new_username } = req.post;
|
||||
if (!user_id) throw new Error('Missing user_id');
|
||||
if (!new_username || !new_username.trim()) throw new Error('Missing new_username');
|
||||
|
||||
const newName = new_username.trim();
|
||||
|
||||
// Validate format (same rules as registration)
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(newName)) {
|
||||
throw new Error('Invalid username. Only A-Z, 0-9, _, -, and . are allowed.');
|
||||
}
|
||||
if (newName.length < 2 || newName.length > 32) {
|
||||
throw new Error('Username must be between 2 and 32 characters.');
|
||||
}
|
||||
|
||||
// Get current user info
|
||||
const target = await db`SELECT id, login, "user" FROM "user" WHERE id = ${+user_id} LIMIT 1`;
|
||||
if (!target.length) throw new Error('User not found');
|
||||
if (target[0].login === 'deleted_user') throw new Error('The deleted_user account is protected and cannot be renamed.');
|
||||
|
||||
const oldLogin = target[0].login;
|
||||
const oldUser = target[0].user;
|
||||
const newLogin = newName.toLowerCase();
|
||||
|
||||
if (newLogin === oldLogin && newName === oldUser) throw new Error('New username is the same as the current one.');
|
||||
|
||||
// Check for conflicts
|
||||
const conflict = await db`SELECT id FROM "user" WHERE (lower(login) = ${newLogin} OR lower("user") = lower(${newName})) AND id != ${+user_id} LIMIT 1`;
|
||||
if (conflict.length) throw new Error(`Username "${newName}" is already taken.`);
|
||||
|
||||
await db.begin(async sql => {
|
||||
// 1. Update the user record
|
||||
await sql`UPDATE "user" SET login = ${newLogin}, "user" = ${newName} WHERE id = ${+user_id}`;
|
||||
|
||||
// 2. Update items.username (matches both old login and old display name)
|
||||
await sql`UPDATE items SET username = ${newLogin} WHERE username ILIKE ${oldLogin} OR username ILIKE ${oldUser}`;
|
||||
|
||||
// 3. Clear old login_attempts so the new name starts clean
|
||||
await sql`DELETE FROM login_attempts WHERE username = ${oldLogin}`;
|
||||
});
|
||||
|
||||
// Invalidate all sessions so the user must re-log with the new name
|
||||
await db`DELETE FROM user_sessions WHERE user_id = ${+user_id}`;
|
||||
|
||||
// Log it in audit
|
||||
await audit.log(req.session.id, 'admin_rename_user', 'user', +user_id, {
|
||||
old_login: oldLogin,
|
||||
new_login: newLogin,
|
||||
old_user: oldUser,
|
||||
new_user: newName
|
||||
});
|
||||
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
||||
success: true,
|
||||
new_login: newLogin,
|
||||
new_user: newName,
|
||||
msg: `User renamed from "${oldLogin}" to "${newLogin}". All uploads updated. Sessions invalidated.`
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('[ADMIN] Rename failed:', err);
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: err.message }));
|
||||
}
|
||||
});
|
||||
|
||||
// About page text editor
|
||||
router.get(/^\/admin\/about\/?$/, lib.auth, async (req, res) => {
|
||||
const settings = await db`SELECT value FROM site_settings WHERE key = 'about_text' LIMIT 1`;
|
||||
|
||||
@@ -128,10 +128,10 @@
|
||||
<table class="admin-users-table responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User & Contact</th>
|
||||
<th>User</th>
|
||||
<th>Activity</th>
|
||||
<th>Registration</th>
|
||||
<th>Account Age</th>
|
||||
<th>Date</th>
|
||||
<th>Age</th>
|
||||
<th>Status</th>
|
||||
<th style="text-align: right;">Actions</th>
|
||||
</tr>
|
||||
@@ -283,7 +283,7 @@
|
||||
|
||||
var hint = currentDisplay
|
||||
? 'Current nick: <strong style="color: var(--accent);">' + escHTML(currentDisplay) + '</strong><br>Enter a new stylized name, or leave empty to clear it.'
|
||||
: 'Enter a stylized display name for <strong>' + escHTML(userName) + '</strong> (e.g. <code>F.O.O</code>). Leave empty to clear.';
|
||||
: 'Enter a stylized display name for <strong>' + escHTML(userName) + '</strong>. Leave empty to clear.';
|
||||
|
||||
ModAction.confirm('Set Display Name', hint, async (newName) => {
|
||||
var res = await fetch('/api/v2/admin/users/set-display-name', {
|
||||
@@ -310,7 +310,7 @@
|
||||
} else {
|
||||
throw new Error(data.msg || 'Failed to set display name');
|
||||
}
|
||||
}, { hideReason: false, allowEmpty: true, confirmText: 'Set Nick', placeholder: currentDisplay || 'e.g. F.O.O' });
|
||||
}, { hideReason: false, singleLine: true, allowEmpty: true, confirmText: 'Set Nick', placeholder: currentDisplay || 'Set nickname' });
|
||||
}
|
||||
|
||||
async function adminLockLayout(btn) {
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
<td data-label="Activity">
|
||||
<div style="display: flex; align-items: center; gap: 15px; white-space: nowrap;">
|
||||
<a href="/user/{{ u.login }}" target="_blank" class="stat-box" title="Uploads" style="text-decoration: none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||||
<i class="fa fa-upload" style="opacity: 0.6; font-size: 13px;"></i>
|
||||
<strong>{{ u.upload_count }}</strong>
|
||||
</a>
|
||||
<a href="/user/{{ u.login }}/comments" target="_blank" class="stat-box" title="Comments" style="text-decoration: none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
||||
<i class="fa fa-comment" style="opacity: 0.6; font-size: 13px;"></i>
|
||||
<strong>{{ u.comment_count }}</strong>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Registration">
|
||||
<div style="font-size: 0.85rem; color: #eee; font-weight: 600; cursor: help;" tooltip="Method: {{ u.reg_method }}">{{ new Date(u.created_at).toLocaleDateString() }}</div>
|
||||
<div style="font-size: 0.85rem; color: #eee; font-weight: 600; cursor: help;" tooltip="Method: {{ u.reg_method === 'Legacy' ? 'Legacy Account' : (u.reg_method ? 'Invite Token: ' + u.reg_method.slice(0, 8) + '…' : (u.activated ? 'Open Registration' : 'Email Verification')) }}">{{ new Date(u.created_at).toLocaleDateString() }}</div>
|
||||
</td>
|
||||
<td data-label="Account Age">
|
||||
<div style="font-size: 0.85rem; font-weight: 600; color: #aaa;">{{ Math.floor(u.age_days) }} Days</div>
|
||||
@@ -85,6 +85,7 @@
|
||||
|
||||
@if(u.id && u.login !== 'deleted_user')
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-display="{!! u.display_name || '' !!}" onclick="adminSetDisplayName(this)" class="btn-modern btn-nick" style="background: rgba(100, 200, 100, 0.1); color: #64c864; border: 1px solid rgba(100, 200, 100, 0.2);" title="Set stylized display name">✏ Nick</button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminRenameUser(this)" class="btn-modern" style="background: rgba(255, 165, 0, 0.1); color: #ffa500; border: 1px solid rgba(255, 165, 0, 0.2);" title="Rename username (updates all uploads)"><i class="fa fa-at"></i> Rename</button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminSetPassword(this)" class="btn-modern btn-pw" style="background: rgba(var(--accent-rgb, 0, 150, 255), 0.1); color: var(--accent, #0096ff); border: 1px solid rgba(var(--accent-rgb, 0, 150, 255), 0.2);">Set PW</button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-locked="{{ u.force_comment_display_mode }}" data-mode="{{ u.comment_display_mode }}" onclick="adminLockLayout(this)" class="btn-modern" style="background: rgba(255, 100, 0, 0.1); color: #ff6400; border: 1px solid rgba(255, 100, 0, 0.2);" title="{{ u.force_comment_display_mode ? 'Unlock Layout' : 'Lock Layout' }}"><i class="fa fa-{{ u.force_comment_display_mode ? 'lock-open' : 'lock' }}"></i> {{ u.force_comment_display_mode ? 'Unlock' : 'Lock' }}</button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminDeleteUser(this)" class="btn-modern btn-delete" style="background: rgba(217, 83, 79, 0.1); color: #d9534f; border: 1px solid rgba(217, 83, 79, 0.2);">Delete</button>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<h3 id="mod-action-title">{{ t('mod.confirm_action') }}</h3>
|
||||
<div id="mod-action-content"></div>
|
||||
<textarea id="mod-reason" class="mod-reason" placeholder="{{ t('mod.reason_placeholder') }}"></textarea>
|
||||
<input type="text" id="mod-reason-input" class="mod-reason" placeholder="" style="display:none; resize:none;">
|
||||
<div id="mod-action-error" class="error-msg"></div>
|
||||
<div class="modal-actions">
|
||||
<button id="mod-action-confirm" class="btn-danger">{{ t('mod.confirm') }}</button>
|
||||
|
||||
Reference in New Issue
Block a user