pramodmisra Claude Opus 4.7 (1M context) commited on
Commit
f1f2b70
·
1 Parent(s): fbac631

Add superadmin user edit endpoint and inline UI

Browse files

Adds POST /admin/api/users/{id}/update for superadmin-only edits of full_name, role, and is_active, with guards against changing your own role or deactivating yourself. Surfaces an Edit button on each row in admin/users.html that swaps the row into an inline editor (name input, role select, active checkbox, Save/Cancel) so deactivated users can be reactivated and promoted without going through delete-and-recreate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

app/routes/admin_routes.py CHANGED
@@ -291,6 +291,31 @@ async def toggle_user(user_id: int,
291
  db.commit()
292
  return JSONResponse({"success": True, "is_active": user.is_active if user else False})
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  @router.post("/api/users/{user_id}/reset-password")
295
  async def admin_reset_password(user_id: int, request: Request,
296
  current_user: User = Depends(require_admin),
 
291
  db.commit()
292
  return JSONResponse({"success": True, "is_active": user.is_active if user else False})
293
 
294
+
295
+ @router.post("/api/users/{user_id}/update")
296
+ async def update_user(user_id: int, request: Request,
297
+ current_user: User = Depends(require_superadmin),
298
+ db: Session = Depends(get_db)):
299
+ """Superadmin-only: update name, role, active state of an existing user."""
300
+ body = await request.json()
301
+ user = db.query(User).filter(User.id == user_id).first()
302
+ if not user:
303
+ return JSONResponse({"error": "User not found"}, status_code=404)
304
+ if user.id == current_user.id and body.get("role") and body["role"] != current_user.role:
305
+ return JSONResponse({"error": "Cannot change your own role"}, status_code=400)
306
+
307
+ if "full_name" in body and body["full_name"]:
308
+ user.full_name = body["full_name"].strip()
309
+ if "role" in body and body["role"] in ("user", "admin", "superadmin"):
310
+ user.role = body["role"]
311
+ if "is_active" in body:
312
+ if user.id == current_user.id and not body["is_active"]:
313
+ return JSONResponse({"error": "Cannot deactivate yourself"}, status_code=400)
314
+ user.is_active = bool(body["is_active"])
315
+ db.commit()
316
+ return JSONResponse({"success": True, "id": user.id, "role": user.role,
317
+ "full_name": user.full_name, "is_active": user.is_active})
318
+
319
  @router.post("/api/users/{user_id}/reset-password")
320
  async def admin_reset_password(user_id: int, request: Request,
321
  current_user: User = Depends(require_admin),
app/templates/admin/users.html CHANGED
@@ -58,27 +58,54 @@
58
  {% for u in users %}
59
  <tr class="{{ 'bg-gray-50' if loop.index % 2 == 0 }} {{ 'opacity-50' if not u.is_active }} user-row" data-active="{{ 'true' if u.is_active else 'false' }}"
60
  {% if not u.is_active %}style="display: none;"{% endif %}>
61
- <td class="px-4 py-3 font-medium">{{ u.full_name }}</td>
 
 
 
 
62
  <td class="px-4 py-3 text-gray-600">{{ u.email }}</td>
63
  <td class="px-4 py-3 text-center">
64
- <span class="px-2 py-0.5 rounded text-xs font-semibold
65
- {{ 'bg-purple-100 text-purple-700' if u.role == 'superadmin' else 'bg-blue-100 text-blue-700' if u.role == 'admin' else 'bg-gray-100 text-gray-600' }}">
 
66
  {{ u.role }}
67
  </span>
 
 
 
 
 
 
 
68
  </td>
69
  <td class="px-4 py-3 text-center">
70
- <span class="text-xs {{ 'text-green-600' if u.is_active else 'text-red-500' }}">
71
  {{ 'Active' if u.is_active else 'Inactive' }}
72
  </span>
 
 
 
 
 
 
73
  </td>
74
  <td class="px-4 py-3 text-center space-x-2">
75
  {% if u.id != user.id %}
76
- <button onclick="toggleUser({{ u.id }})" class="text-blue-600 hover:underline text-xs">
77
- {{ 'Deactivate' if u.is_active else 'Activate' }}
78
- </button>
79
- <button onclick="resetPassword({{ u.id }}, '{{ u.email }}')" class="text-amber-600 hover:underline text-xs">
80
- Reset PW
81
- </button>
 
 
 
 
 
 
 
 
 
82
  {% else %}
83
  <span class="text-gray-300 text-xs">You</span>
84
  {% endif %}
@@ -127,6 +154,32 @@ async function toggleUser(id) {
127
  await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
128
  location.reload();
129
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  async function resetPassword(id, email) {
131
  if (!confirm('Send password reset email to ' + email + '?')) return;
132
  const resp = await fetch('/admin/api/users/' + id + '/reset-password', {
 
58
  {% for u in users %}
59
  <tr class="{{ 'bg-gray-50' if loop.index % 2 == 0 }} {{ 'opacity-50' if not u.is_active }} user-row" data-active="{{ 'true' if u.is_active else 'false' }}"
60
  {% if not u.is_active %}style="display: none;"{% endif %}>
61
+ <td class="px-4 py-3 font-medium">
62
+ <span class="view-mode" id="name_view_{{ u.id }}">{{ u.full_name }}</span>
63
+ <input type="text" class="edit-mode hidden w-full border rounded px-2 py-1 text-sm"
64
+ id="name_edit_{{ u.id }}" value="{{ u.full_name }}">
65
+ </td>
66
  <td class="px-4 py-3 text-gray-600">{{ u.email }}</td>
67
  <td class="px-4 py-3 text-center">
68
+ <span class="view-mode px-2 py-0.5 rounded text-xs font-semibold
69
+ {{ 'bg-purple-100 text-purple-700' if u.role == 'superadmin' else 'bg-blue-100 text-blue-700' if u.role == 'admin' else 'bg-gray-100 text-gray-600' }}"
70
+ id="role_view_{{ u.id }}">
71
  {{ u.role }}
72
  </span>
73
+ {% if user.role == 'superadmin' and u.id != user.id %}
74
+ <select class="edit-mode hidden border rounded px-1 py-0.5 text-xs" id="role_edit_{{ u.id }}">
75
+ <option value="user" {% if u.role == 'user' %}selected{% endif %}>user</option>
76
+ <option value="admin" {% if u.role == 'admin' %}selected{% endif %}>admin</option>
77
+ <option value="superadmin" {% if u.role == 'superadmin' %}selected{% endif %}>superadmin</option>
78
+ </select>
79
+ {% endif %}
80
  </td>
81
  <td class="px-4 py-3 text-center">
82
+ <span class="view-mode text-xs {{ 'text-green-600' if u.is_active else 'text-red-500' }}" id="active_view_{{ u.id }}">
83
  {{ 'Active' if u.is_active else 'Inactive' }}
84
  </span>
85
+ {% if user.role == 'superadmin' and u.id != user.id %}
86
+ <label class="edit-mode hidden text-xs inline-flex items-center space-x-1">
87
+ <input type="checkbox" id="active_edit_{{ u.id }}" {% if u.is_active %}checked{% endif %}>
88
+ <span>Active</span>
89
+ </label>
90
+ {% endif %}
91
  </td>
92
  <td class="px-4 py-3 text-center space-x-2">
93
  {% if u.id != user.id %}
94
+ <span class="view-mode space-x-2">
95
+ {% if user.role == 'superadmin' %}
96
+ <button onclick="startEdit({{ u.id }})" class="text-green-700 hover:underline text-xs">Edit</button>
97
+ {% endif %}
98
+ <button onclick="toggleUser({{ u.id }})" class="text-blue-600 hover:underline text-xs">
99
+ {{ 'Deactivate' if u.is_active else 'Activate' }}
100
+ </button>
101
+ <button onclick="resetPassword({{ u.id }}, '{{ u.email }}')" class="text-amber-600 hover:underline text-xs">
102
+ Reset PW
103
+ </button>
104
+ </span>
105
+ <span class="edit-mode hidden space-x-2">
106
+ <button onclick="saveEdit({{ u.id }})" class="text-green-700 hover:underline text-xs font-semibold">Save</button>
107
+ <button onclick="cancelEdit({{ u.id }})" class="text-gray-500 hover:underline text-xs">Cancel</button>
108
+ </span>
109
  {% else %}
110
  <span class="text-gray-300 text-xs">You</span>
111
  {% endif %}
 
154
  await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
155
  location.reload();
156
  }
157
+ function rowEls(id) {
158
+ return document.querySelector(`tr.user-row [id="name_view_${id}"]`).closest('tr');
159
+ }
160
+ function startEdit(id) {
161
+ const row = rowEls(id);
162
+ row.querySelectorAll('.view-mode').forEach(el => el.classList.add('hidden'));
163
+ row.querySelectorAll('.edit-mode').forEach(el => el.classList.remove('hidden'));
164
+ }
165
+ function cancelEdit(id) {
166
+ const row = rowEls(id);
167
+ row.querySelectorAll('.edit-mode').forEach(el => el.classList.add('hidden'));
168
+ row.querySelectorAll('.view-mode').forEach(el => el.classList.remove('hidden'));
169
+ }
170
+ async function saveEdit(id) {
171
+ const payload = {
172
+ full_name: document.getElementById('name_edit_' + id).value,
173
+ role: document.getElementById('role_edit_' + id).value,
174
+ is_active: document.getElementById('active_edit_' + id).checked,
175
+ };
176
+ const resp = await fetch('/admin/api/users/' + id + '/update', {
177
+ method: 'POST', headers: authHeaders, body: JSON.stringify(payload),
178
+ });
179
+ const data = await resp.json();
180
+ if (data.success) location.reload();
181
+ else alert(data.error || 'Failed to save');
182
+ }
183
  async function resetPassword(id, email) {
184
  if (!confirm('Send password reset email to ' + email + '?')) return;
185
  const resp = await fetch('/admin/api/users/' + id + '/reset-password', {