diff --git a/rbox/routers/admin.py b/rbox/routers/admin.py new file mode 100644 index 0000000..809043d --- /dev/null +++ b/rbox/routers/admin.py @@ -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"} \ No newline at end of file diff --git a/static/js/api.js b/static/js/api.js index fcd688d..c65792a 100644 --- a/static/js/api.js +++ b/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(); diff --git a/static/js/components/admin-dashboard.js b/static/js/components/admin-dashboard.js new file mode 100644 index 0000000..092b8b3 --- /dev/null +++ b/static/js/components/admin-dashboard.js @@ -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 = '
'; + } + } + + render() { + this.innerHTML = ` +