feat: Implement admin dashboard user management (CRUD)
This commit is contained in:
+121
-3
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user