feat: Implement admin dashboard user management (CRUD)
This commit is contained in:
parent
6fdd4b9f0c
commit
17de53b9c2
98
rbox/routers/admin.py
Normal file
98
rbox/routers/admin.py
Normal 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"}
|
||||
124
static/js/api.js
124
static/js/api.js
@ -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();
|
||||
|
||||
174
static/js/components/admin-dashboard.js
Normal file
174
static/js/components/admin-dashboard.js
Normal 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">×</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);
|
||||
Loading…
Reference in New Issue
Block a user