feat: Implement admin dashboard user management (CRUD)

This commit is contained in:
retoor 2025-11-10 01:56:44 +01:00
parent 6fdd4b9f0c
commit 17de53b9c2
3 changed files with 393 additions and 3 deletions

98
rbox/routers/admin.py Normal file
View File

@ -0,0 +1,98 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from ..auth import get_current_admin_user, get_password_hash
from ..models import User, User_Pydantic
from ..schemas import UserCreate, UserAdminUpdate
router = APIRouter(
prefix="/admin",
tags=["admin"],
dependencies=[Depends(get_current_admin_user)],
responses={403: {"description": "Not enough permissions"}},
)
@router.get("/users", response_model=List[User_Pydantic])
async def get_all_users():
return await User.all()
@router.get("/users/{user_id}", response_model=User_Pydantic)
async def get_user(user_id: int):
user = await User.get_or_none(id=user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
@router.post("/users", response_model=User_Pydantic, status_code=status.HTTP_201_CREATED)
async def create_user_by_admin(user_in: UserCreate):
user = await User.get_or_none(username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered",
)
user = await User.get_or_none(email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
hashed_password = get_password_hash(user_in.password)
user = await User.create(
username=user_in.username,
email=user_in.email,
hashed_password=hashed_password,
is_superuser=False, # Admin creates regular users by default
is_active=True,
)
return await User_Pydantic.from_tortoise_orm(user)
@router.put("/users/{user_id}", response_model=User_Pydantic)
async def update_user_by_admin(user_id: int, user_update: UserAdminUpdate):
user = await User.get_or_none(id=user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
if user_update.username is not None and user_update.username != user.username:
if await User.get_or_none(username=user_update.username):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken")
user.username = user_update.username
if user_update.email is not None and user_update.email != user.email:
if await User.get_or_none(email=user_update.email):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
user.email = user_update.email
if user_update.password is not None:
user.hashed_password = get_password_hash(user_update.password)
if user_update.is_active is not None:
user.is_active = user_update.is_active
if user_update.is_superuser is not None:
user.is_superuser = user_update.is_superuser
if user_update.storage_quota_bytes is not None:
user.storage_quota_bytes = user_update.storage_quota_bytes
if user_update.plan_type is not None:
user.plan_type = user_update.plan_type
if user_update.is_2fa_enabled is not None:
user.is_2fa_enabled = user_update.is_2fa_enabled
if not user_update.is_2fa_enabled:
user.two_factor_secret = None
user.recovery_codes = None
await user.save()
return await User_Pydantic.from_tortoise_orm(user)
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user_by_admin(user_id: int):
user = await User.get_or_none(id=user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
await user.delete()
return {"message": "User deleted successfully"}

View File

@ -48,8 +48,17 @@ class APIClient {
}
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || 'Request failed');
let errorData;
try {
errorData = await response.json();
} catch (e) {
errorData = { message: 'Unknown error' };
}
const errorMessage = errorData.detail || errorData.message || 'Request failed';
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: errorMessage, type: 'error' }
}));
throw new Error(errorMessage);
}
if (response.status === 204) {
@ -200,9 +209,118 @@ class APIClient {
return this.request('files/photos');
}
getThumbnailUrl(fileId) {
async getThumbnailUrl(fileId) {
return `${this.baseURL}files/thumbnail/${fileId}`;
}
async listDeletedFiles() {
return this.request('files/deleted');
}
async restoreFile(fileId) {
return this.request(`files/${fileId}/restore`, {
method: 'POST'
});
}
async listUsers() {
return this.request('admin/users');
}
async createUser(userData) {
return this.request('admin/users', {
method: 'POST',
body: userData
});
}
async updateUser(userId, userData) {
return this.request(`admin/users/${userId}`, {
method: 'PUT',
body: userData
});
}
async deleteUser(userId) {
return this.request(`admin/users/${userId}`, {
method: 'DELETE'
});
}
async starFile(fileId) {
return this.request(`files/${fileId}/star`, {
method: 'POST'
});
}
async unstarFile(fileId) {
return this.request(`files/${fileId}/unstar`, {
method: 'POST'
});
}
async starFolder(folderId) {
return this.request(`folders/${folderId}/star`, {
method: 'POST'
});
}
async unstarFolder(folderId) {
return this.request(`folders/${folderId}/unstar`, {
method: 'POST'
});
}
async listStarredFiles() {
return this.request('starred/files');
}
async listStarredFolders() {
return this.request('starred/folders');
}
async listRecentFiles() {
return this.request('files/recent');
}
async listMyShares() {
return this.request('shares/my');
}
async updateShare(shareId, shareData) {
return this.request(`shares/${shareId}`, {
method: 'PUT',
body: shareData
});
}
async deleteShare(shareId) {
return this.request(`shares/${shareId}`, {
method: 'DELETE'
});
}
async batchFileOperations(operation, fileIds, targetFolderId = null) {
const payload = { file_ids: fileIds, operation: operation };
if (targetFolderId !== null) {
payload.target_folder_id = targetFolderId;
}
return this.request('files/batch', {
method: 'POST',
body: payload
});
}
async batchFolderOperations(operation, folderIds, targetFolderId = null) {
const payload = { folder_ids: folderIds, operation: operation };
if (targetFolderId !== null) {
payload.target_folder_id = targetFolderId;
}
return this.request('folders/batch', {
method: 'POST',
body: payload
});
}
}
export const api = new APIClient();

View File

@ -0,0 +1,174 @@
import { api } from '../api.js';
export class AdminDashboard extends HTMLElement {
constructor() {
super();
this.users = [];
}
async connectedCallback() {
await this.loadUsers();
}
async loadUsers() {
try {
this.users = await api.listUsers();
this.render();
} catch (error) {
console.error('Failed to load users:', error);
this.innerHTML = '<p class="error-message">Failed to load users. Do you have admin privileges?</p>';
}
}
render() {
this.innerHTML = `
<div class="admin-dashboard-container">
<h2>Admin Dashboard</h2>
<div class="admin-section">
<h3>User Management</h3>
<button id="createUserButton" class="button button-primary">Create New User</button>
<div class="user-list">
${this.users.map(user => this.renderUser(user)).join('')}
</div>
</div>
<div id="userModal" class="modal">
<div class="modal-content">
<span class="close-button">&times;</span>
<h3>Edit User</h3>
<form id="userForm">
<input type="hidden" id="userId">
<label for="username">Username:</label>
<input type="text" id="username" required>
<label for="email">Email:</label>
<input type="email" id="email" required>
<label for="password">Password (leave blank to keep current):</label>
<input type="password" id="password">
<label for="isSuperuser">Superuser:</label>
<input type="checkbox" id="isSuperuser">
<label for="isActive">Active:</label>
<input type="checkbox" id="isActive">
<label for="is2faEnabled">2FA Enabled:</label>
<input type="checkbox" id="is2faEnabled">
<label for="storageQuotaBytes">Storage Quota (Bytes):</label>
<input type="number" id="storageQuotaBytes">
<label for="planType">Plan Type:</label>
<input type="text" id="planType">
<button type="submit" class="button button-primary">Save</button>
</form>
</div>
</div>
</div>
`;
this.querySelector('#createUserButton').addEventListener('click', () => this._showUserModal());
this.querySelector('.user-list').addEventListener('click', this._handleUserAction.bind(this));
this.querySelector('.close-button').addEventListener('click', () => this.querySelector('#userModal').style.display = 'none');
this.querySelector('#userForm').addEventListener('submit', this._handleUserFormSubmit.bind(this));
}
_handleUserAction(event) {
const target = event.target;
const userItem = target.closest('.user-item');
if (!userItem) return;
const userId = userItem.dataset.userId; // Assuming user ID will be stored in data-userId attribute
if (target.classList.contains('button-danger')) {
this._deleteUser(userId);
} else if (target.classList.contains('button')) { // Edit button
this._showUserModal(userId);
}
}
_showUserModal(userId = null) {
const modal = this.querySelector('#userModal');
const form = this.querySelector('#userForm');
form.reset(); // Clear previous form data
if (userId) {
const user = this.users.find(u => u.id == userId);
if (user) {
this.querySelector('#userId').value = user.id;
this.querySelector('#username').value = user.username;
this.querySelector('#email').value = user.email;
this.querySelector('#isSuperuser').checked = user.is_superuser;
this.querySelector('#isActive').checked = user.is_active;
this.querySelector('#is2faEnabled').checked = user.is_2fa_enabled;
this.querySelector('#storageQuotaBytes').value = user.storage_quota_bytes;
this.querySelector('#planType').value = user.plan_type;
this.querySelector('h3').textContent = 'Edit User';
}
} else {
this.querySelector('#userId').value = '';
this.querySelector('h3').textContent = 'Create New User';
}
modal.style.display = 'block';
}
async _handleUserFormSubmit(event) {
event.preventDefault();
const userId = this.querySelector('#userId').value;
const userData = {
username: this.querySelector('#username').value,
email: this.querySelector('#email').value,
password: this.querySelector('#password').value || undefined, // Only send if not empty
is_superuser: this.querySelector('#isSuperuser').checked,
is_active: this.querySelector('#isActive').checked,
is_2fa_enabled: this.querySelector('#is2faEnabled').checked,
storage_quota_bytes: parseInt(this.querySelector('#storageQuotaBytes').value),
plan_type: this.querySelector('#planType').value,
};
try {
if (userId) {
await api.updateUser(userId, userData);
} else {
await api.createUser(userData);
}
this.querySelector('#userModal').style.display = 'none';
await this.loadUsers(); // Refresh the list
} catch (error) {
console.error('Failed to save user:', error);
alert('Failed to save user: ' + error.message);
}
}
async _deleteUser(userId) {
if (!confirm('Are you sure you want to delete this user?')) {
return;
}
try {
await api.deleteUser(userId);
await this.loadUsers(); // Refresh the list
} catch (error) {
console.error('Failed to delete user:', error);
alert('Failed to delete user: ' + error.message);
}
}
renderUser(user) {
const formatBytes = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
return `
<div class="user-item" data-user-id="${user.id}">
<span>
${user.username} (${user.email}) - Superuser: ${user.is_superuser ? 'Yes' : 'No'} - 2FA: ${user.is_2fa_enabled ? 'Yes' : 'No'} - Active: ${user.is_active ? 'Yes' : 'No'} - Storage: ${formatBytes(user.storage_quota_bytes)} - Plan: ${user.plan_type}
</span>
<div class="user-actions">
<button class="button button-small">Edit</button>
<button class="button button-small button-danger">Delete</button>
</div>
</div>
`;
}
}
customElements.define('admin-dashboard', AdminDashboard);