mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
Add admin user rename
This commit is contained in:
@@ -210,6 +210,36 @@ class AuthManager:
|
|||||||
logger.info(f"Deleted user '{username}' (by {requesting_user}); revoked {revoked} active session(s)")
|
logger.info(f"Deleted user '{username}' (by {requesting_user}); revoked {revoked} active session(s)")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def rename_user(self, old_username: str, new_username: str, requesting_user: str) -> bool:
|
||||||
|
"""Rename a user in auth config and active sessions. Admin only."""
|
||||||
|
old_username = old_username.strip().lower()
|
||||||
|
new_username = new_username.strip().lower()
|
||||||
|
requesting_user = (requesting_user or "").strip().lower()
|
||||||
|
if not old_username or not new_username:
|
||||||
|
return False
|
||||||
|
if old_username not in self.users:
|
||||||
|
return False
|
||||||
|
if new_username in self.users:
|
||||||
|
return False
|
||||||
|
if not self.users.get(requesting_user, {}).get("is_admin"):
|
||||||
|
return False
|
||||||
|
self._config.setdefault("users", {})[new_username] = self._config["users"].pop(old_username)
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
renamed_sessions = 0
|
||||||
|
with self._sessions_lock:
|
||||||
|
for sess in self._sessions.values():
|
||||||
|
if (sess or {}).get("username") == old_username:
|
||||||
|
sess["username"] = new_username
|
||||||
|
renamed_sessions += 1
|
||||||
|
if renamed_sessions:
|
||||||
|
self._save_sessions()
|
||||||
|
logger.info(
|
||||||
|
"Renamed user '%s' -> '%s' (by %s); updated %d active session(s)",
|
||||||
|
old_username, new_username, requesting_user, renamed_sessions,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def is_admin(self, username: str) -> bool:
|
def is_admin(self, username: str) -> bool:
|
||||||
return self.users.get(username, {}).get("is_admin", False)
|
return self.users.get(username, {}).get("is_admin", False)
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ class DeleteUserRequest(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class RenameUserRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
SESSION_COOKIE = "odysseus_session"
|
SESSION_COOKIE = "odysseus_session"
|
||||||
|
|
||||||
|
|
||||||
@@ -266,6 +270,64 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
|||||||
raise HTTPException(404, "User not found or is admin")
|
raise HTTPException(404, "User not found or is admin")
|
||||||
return {"ok": True, "privileges": auth_manager.get_privileges(username)}
|
return {"ok": True, "privileges": auth_manager.get_privileges(username)}
|
||||||
|
|
||||||
|
@router.put("/users/{username}/rename")
|
||||||
|
async def rename_user(username: str, body: RenameUserRequest, request: Request):
|
||||||
|
user = _get_current_user(request)
|
||||||
|
if not user or not auth_manager.is_admin(user):
|
||||||
|
raise HTTPException(403, "Admin only")
|
||||||
|
old_username = (username or "").strip().lower()
|
||||||
|
new_username = (body.username or "").strip().lower()
|
||||||
|
if not new_username:
|
||||||
|
raise HTTPException(400, "Username required")
|
||||||
|
if old_username == new_username:
|
||||||
|
return {"ok": True, "username": new_username, "renamed_self": old_username == user}
|
||||||
|
if old_username not in auth_manager.users:
|
||||||
|
raise HTTPException(404, "User not found")
|
||||||
|
if new_username in auth_manager.users:
|
||||||
|
raise HTTPException(409, "Username already taken")
|
||||||
|
|
||||||
|
# Usernames are ownership keys for user data. Rename the common
|
||||||
|
# owner-scoped DB rows before changing auth so the account keeps
|
||||||
|
# access to its sessions, docs, email accounts, tasks, etc.
|
||||||
|
try:
|
||||||
|
from core.database import Base, SessionLocal
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
for mapper in Base.registry.mappers:
|
||||||
|
model = mapper.class_
|
||||||
|
if not hasattr(model, "owner"):
|
||||||
|
continue
|
||||||
|
(
|
||||||
|
db.query(model)
|
||||||
|
.filter(model.owner == old_username)
|
||||||
|
.update({"owner": new_username}, synchronize_session=False)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to rename owner references %s -> %s: %s", old_username, new_username, e)
|
||||||
|
raise HTTPException(500, "Failed to rename user data")
|
||||||
|
|
||||||
|
# Per-user prefs are JSON-backed, not SQL-backed.
|
||||||
|
try:
|
||||||
|
from routes.prefs_routes import _load as _load_prefs, _save as _save_prefs
|
||||||
|
prefs = _load_prefs()
|
||||||
|
users = prefs.get("_users") if isinstance(prefs, dict) else None
|
||||||
|
if isinstance(users, dict) and old_username in users and new_username not in users:
|
||||||
|
users[new_username] = users.pop(old_username)
|
||||||
|
_save_prefs(prefs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to rename user prefs %s -> %s: %s", old_username, new_username, e)
|
||||||
|
|
||||||
|
ok = auth_manager.rename_user(old_username, new_username, user)
|
||||||
|
if not ok:
|
||||||
|
raise HTTPException(400, "Cannot rename user")
|
||||||
|
return {"ok": True, "username": new_username, "renamed_self": old_username == user}
|
||||||
|
|
||||||
@router.post("/signup-toggle")
|
@router.post("/signup-toggle")
|
||||||
async def toggle_signup(request: Request):
|
async def toggle_signup(request: Request):
|
||||||
"""Toggle open registration on/off. Admin only."""
|
"""Toggle open registration on/off. Admin only."""
|
||||||
|
|||||||
+38
-1
@@ -53,6 +53,7 @@ async function loadUsers() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;align-items:center;">
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<button class="admin-btn-sm" data-adm-rename-user="${esc(u.username)}" style="font-size:11px;">Rename</button>
|
||||||
${u.is_admin ? '' : `<button class="admin-btn-delete" data-adm-del-user="${esc(u.username)}" style="font-size:11px;">Remove</button>`}
|
${u.is_admin ? '' : `<button class="admin-btn-delete" data-adm-del-user="${esc(u.username)}" style="font-size:11px;">Remove</button>`}
|
||||||
${u.is_admin ? '' : '<svg class="admin-user-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.3;transition:transform 0.2s,opacity 0.2s;"><polyline points="6 9 12 15 18 9"/></svg>'}
|
${u.is_admin ? '' : '<svg class="admin-user-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.3;transition:transform 0.2s,opacity 0.2s;"><polyline points="6 9 12 15 18 9"/></svg>'}
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +107,7 @@ async function loadUsers() {
|
|||||||
// Toggle panel visibility + rotate chevron + load models
|
// Toggle panel visibility + rotate chevron + load models
|
||||||
let _modelsLoaded = false;
|
let _modelsLoaded = false;
|
||||||
header.addEventListener('click', (e) => {
|
header.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.admin-btn-delete')) return;
|
if (e.target.closest('.admin-btn-delete, [data-adm-rename-user]')) return;
|
||||||
privPanel.classList.toggle('hidden');
|
privPanel.classList.toggle('hidden');
|
||||||
const chevron = header.querySelector('.admin-user-chevron');
|
const chevron = header.querySelector('.admin-user-chevron');
|
||||||
if (chevron) {
|
if (chevron) {
|
||||||
@@ -143,6 +144,42 @@ async function loadUsers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rename button
|
||||||
|
const renameBtn = row.querySelector('[data-adm-rename-user]');
|
||||||
|
if (renameBtn) {
|
||||||
|
renameBtn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const oldUsername = renameBtn.dataset.admRenameUser;
|
||||||
|
const next = await uiModule.styledPrompt(`Rename "${oldUsername}"`, {
|
||||||
|
defaultValue: oldUsername,
|
||||||
|
placeholder: 'New username',
|
||||||
|
confirmText: 'Rename',
|
||||||
|
});
|
||||||
|
const username = (next || '').trim();
|
||||||
|
if (!username || username === oldUsername) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/auth/users/${encodeURIComponent(oldUsername)}/rename`, {
|
||||||
|
method: 'PUT',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
uiModule.showError(data.detail || 'Failed to rename user');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.renamed_self) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
uiModule.showError('Failed to rename user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Delete button
|
// Delete button
|
||||||
const delBtn = row.querySelector('[data-adm-del-user]');
|
const delBtn = row.querySelector('[data-adm-del-user]');
|
||||||
if (delBtn) {
|
if (delBtn) {
|
||||||
|
|||||||
Reference in New Issue
Block a user