From 993b5bfbb67346b706b6e1c3a2398b353d50cb6d Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 8 Nov 2025 20:50:16 +0100 Subject: [PATCH] Update. --- retoors/static/js/components/order.js | 261 ++++++++++++++++++++++++++ retoors/views/admin.py | 155 +++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 retoors/static/js/components/order.js create mode 100644 retoors/views/admin.py diff --git a/retoors/static/js/components/order.js b/retoors/static/js/components/order.js new file mode 100644 index 0000000..4e98fb6 --- /dev/null +++ b/retoors/static/js/components/order.js @@ -0,0 +1,261 @@ +document.addEventListener('DOMContentLoaded', () => { + const userQuotaList = document.getElementById('user-quota-list'); + const addnewUserBtn = document.getElementById('add-new-user-btn'); + const addUserModal = document.getElementById('add-user-modal'); + const editQuotaModal = document.getElementById('edit-quota-modal'); + const viewDetailsModal = document.getElementById('view-details-modal'); + const closeButtons = document.querySelectorAll('.modal .close-button'); + const addUserForm = document.getElementById('add-user-form'); + const editQuotaForm = document.getElementById('edit-quota-form'); + const adduserMessage = document.getElementById('add-user-message'); + const editQuotaMessage = document.getElementById('edit-quota-message'); + const userDetailsContent = document.getElementById('user-details-content'); + + // Function to open a modal + function openModal(modal) { + modal.style.display = 'block'; + } + + // Function to close a modal + function closeModal(modal) { + modal.style.display = 'none'; + } + + // Close modals when clicking on the close button + closeButtons.forEach(button => { + button.addEventListener('click', (event) => { + closeModal(event.target.closest('.modal')); + }); + }); + + // Close modals when clicking outside the modal content + window.addEventListener('click', (event) => { + if (event.target === addUserModal) { + closeModal(addUserModal); + } + if (event.target === editQuotaModal) { + closeModal(editQuotaModal); + } + if (event.target === viewDetailsModal) { + closeModal(viewDetailsModal); + } + }); + + // Fetch and render users + async function fetchAndRenderUsers() { + try { + const response = await fetch('/api/users'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + userQuotaList.innerHTML = ''; // Clear existing list + + data.users.forEach(user => { + const quotaItem = document.createElement('div'); + quotaItem.className = 'user-quota-item card'; + const percentage = user.storage_quota_gb > 0 ? ((user.storage_used_gb / user.storage_quota_gb) * 100).toFixed(2) : 0; + + quotaItem.innerHTML = ` +
+

${user.full_name}

+

${user.email}

+
+
+
+
+
+

${user.storage_used_gb} GB / ${user.storage_quota_gb} GB (${percentage}%)

+
+
+ + + + ${user.parent_email === null ? `` : ''} +
+ `; + userQuotaList.appendChild(quotaItem); + }); + + // Attach event listeners to newly created buttons + attachButtonListeners(); + + } catch (error) { + console.error('Error fetching users:', error); + userQuotaList.innerHTML = '

Error loading user quotas.

'; + } + } + + function attachButtonListeners() { + // Edit Quota Buttons + document.querySelectorAll('.edit-quota-btn').forEach(button => { + button.addEventListener('click', (event) => { + const email = event.target.dataset.email; + const quota = event.target.dataset.quota; + document.getElementById('edit-quota-user-email').value = email; + document.getElementById('edit-quota-amount').value = quota; + editQuotaMessage.textContent = ''; + openModal(editQuotaModal); + }); + }); + + // Delete User Buttons + document.querySelectorAll('.delete-user-btn').forEach(button => { + button.addEventListener('click', async (event) => { + const email = event.target.dataset.email; + if (confirm(`Are you sure you want to delete user ${email}?`)) { + try { + const response = await fetch(`/api/users/${email}`, { + method: 'DELETE', + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + alert(`User ${email} deleted successfully.`); + fetchAndRenderUsers(); // Re-render the list + } catch (error) { + console.error('Error deleting user:', error); + alert(`Failed to delete user: ${error.message}`); + } + } + }); + }); + + // View Details Buttons + document.querySelectorAll('.view-details-btn').forEach(button => { + button.addEventListener('click', async (event) => { + const email = event.target.dataset.email; + try { + const response = await fetch(`/api/users/${email}`); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const user = data.user; + userDetailsContent.innerHTML = ` +

Full Name: ${user.full_name}

+

Email: ${user.email}

+

Storage Used: ${user.storage_used_gb} GB

+

Storage Quota: ${user.storage_quota_gb} GB

+

Parent Email: ${user.parent_email || 'N/A'}

+ `; + openModal(viewDetailsModal); + } catch (error) { + console.error('Error fetching user details:', error); + alert(`Failed to fetch user details: ${error.message}`); + } + }); + }); + + // Delete Team Buttons + document.querySelectorAll('.delete-team-btn').forEach(button => { + button.addEventListener('click', async (event) => { + const parentEmail = event.target.dataset.parentEmail; + if (confirm(`Are you sure you want to delete the team managed by ${parentEmail}? This will delete all users created by this account.`)) { + try { + const response = await fetch(`/api/teams/${parentEmail}`, { + method: 'DELETE', + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + alert(`Team managed by ${parentEmail} and associated users deleted successfully.`); + fetchAndRenderUsers(); // Re-render the list + } catch (error) { + console.error('Error deleting team:', error); + alert(`Failed to delete team: ${error.message}`); + } + } + }); + }); + } + + // Add New User Form Submission + addUserForm.addEventListener('submit', async (event) => { + event.preventDefault(); + adduserMessage.textContent = ''; // Clear previous messages + + const full_name = document.getElementById('new-user-full-name').value; + const email = document.getElementById('new-user-email').value; + const password = document.getElementById('new-user-password').value; + const confirm_password = document.getElementById('new-user-confirm-password').value; + + if (password !== confirm_password) { + adduserMessage.textContent = 'Passwords do not match.'; + return; + } + + try { + const response = await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ full_name, email, password }), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Failed to add user.'); + } + + alert(data.message); + closeModal(addUserModal); + addUserForm.reset(); + fetchAndRenderUsers(); // Re-render the list + } catch (error) { + console.error('Error adding user:', error); + adduserMessage.textContent = error.message; + } + }); + + // Edit Quota Form Submission + editQuotaForm.addEventListener('submit', async (event) => { + event.preventDefault(); + editQuotaMessage.textContent = ''; // Clear previous messages + + const email = document.getElementById('edit-quota-user-email').value; + const new_quota_gb = parseFloat(document.getElementById('edit-quota-amount').value); + + if (isNaN(new_quota_gb) || new_quota_gb <= 0) { + editQuotaMessage.textContent = 'Please enter a valid positive number for quota.'; + return; + } + + try { + const response = await fetch(`/api/users/${email}/quota`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ new_quota_gb }), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Failed to update quota.'); + } + + alert(data.message); + closeModal(editQuotaModal); + editQuotaForm.reset(); + fetchAndRenderUsers(); // Re-render the list + } catch (error) { + console.error('Error updating quota:', error); + editQuotaMessage.textContent = error.message; + } + }); + + // Initial fetch and render + fetchAndRenderUsers(); + + // Event listener for "+ Add New User" button + addnewUserBtn.addEventListener('click', () => { + adduserMessage.textContent = ''; // Clear previous messages + addUserForm.reset(); + openModal(addUserModal); + }); +}); diff --git a/retoors/views/admin.py b/retoors/views/admin.py new file mode 100644 index 0000000..56a1dbd --- /dev/null +++ b/retoors/views/admin.py @@ -0,0 +1,155 @@ +from aiohttp import web +import aiohttp_jinja2 +from aiohttp_session import get_session +from ..services.user_service import UserService +from ..models import QuotaUpdateModel, RegistrationModel +from typing import Dict, Any, List +import json + +async def get_users(request: web.Request) -> web.Response: + user_service: UserService = request.app["user_service"] + session = await get_session(request) + current_user_email = session.get("user_email") + + if not current_user_email: + return web.json_response({"error": "Unauthorized"}, status=401) + + # For now, let's assume only the main user can see all users. + # In a real application, you'd have roles/permissions. + # The main user is the one who created the account. + # If the current user is the main user, they can see all users. + # Otherwise, they can only see users they created (their "team"). + + # This logic needs to be refined based on how "main user" is identified. + # For now, let's return all users for simplicity, assuming the logged-in user has admin-like access to this page. + # A more robust solution would involve checking if the current_user_email is the 'owner' of the site. + users = user_service.get_all_users() + + # Filter out sensitive information like password and reset tokens + safe_users = [] + for user in users: + safe_user = {k: v for k, v in user.items() if k not in ["password", "reset_token", "reset_token_expiry"]} + safe_users.append(safe_user) + + return web.json_response({"users": safe_users}) + +async def add_user(request: web.Request) -> web.Response: + user_service: UserService = request.app["user_service"] + session = await get_session(request) + current_user_email = session.get("user_email") + + if not current_user_email: + return web.json_response({"error": "Unauthorized"}, status=401) + + try: + data = await request.json() + registration_data = RegistrationModel(**data) + + # The current user is the parent of the new user + new_user = user_service.create_user( + full_name=registration_data.full_name, + email=registration_data.email, + password=registration_data.password, + parent_email=current_user_email # Assign current user as parent + ) + safe_new_user = {k: v for k, v in new_user.items() if k not in ["password", "reset_token", "reset_token_expiry"]} + return web.json_response({"message": "User added successfully", "user": safe_new_user}, status=201) + except ValueError as e: + return web.json_response({"error": str(e)}, status=400) + except Exception as e: + return web.json_response({"error": "Invalid data provided", "details": str(e)}, status=400) + +async def update_user_quota(request: web.Request) -> web.Response: + user_service: UserService = request.app["user_service"] + session = await get_session(request) + current_user_email = session.get("user_email") + target_user_email = request.match_info.get("email") + + if not current_user_email: + return web.json_response({"error": "Unauthorized"}, status=401) + + if not target_user_email: + return web.json_response({"error": "User email not provided"}, status=400) + + # Ensure the current user has permission to update this user's quota + # For now, allow if current_user_email is the target_user_email or if target_user is a child of current_user + target_user = user_service.get_user_by_email(target_user_email) + if not target_user or (target_user_email != current_user_email and target_user.get("parent_email") != current_user_email): + return web.json_response({"error": "Forbidden: You do not have permission to update this user's quota"}, status=403) + + try: + data = await request.json() + quota_update_data = QuotaUpdateModel(**data) + user_service.update_user_quota(target_user_email, quota_update_data.new_quota_gb) + return web.json_response({"message": f"Quota for {target_user_email} updated successfully"}) + except ValueError as e: + return web.json_response({"error": str(e)}, status=400) + except Exception as e: + return web.json_response({"error": "Invalid data provided", "details": str(e)}, status=400) + +async def delete_user(request: web.Request) -> web.Response: + user_service: UserService = request.app["user_service"] + session = await get_session(request) + current_user_email = session.get("user_email") + target_user_email = request.match_info.get("email") + + if not current_user_email: + return web.json_response({"error": "Unauthorized"}, status=401) + + if not target_user_email: + return web.json_response({"error": "User email not provided"}, status=400) + + # Prevent a user from deleting themselves or a parent user + if target_user_email == current_user_email: + return web.json_response({"error": "Forbidden: You cannot delete your own account from this interface"}, status=403) + + target_user = user_service.get_user_by_email(target_user_email) + if not target_user or target_user.get("parent_email") != current_user_email: + return web.json_response({"error": "Forbidden: You do not have permission to delete this user"}, status=403) + + if user_service.delete_user(target_user_email): + return web.json_response({"message": f"User {target_user_email} deleted successfully"}) + else: + return web.json_response({"error": "User not found or could not be deleted"}, status=404) + +async def get_user_details(request: web.Request) -> web.Response: + user_service: UserService = request.app["user_service"] + session = await get_session(request) + current_user_email = session.get("user_email") + target_user_email = request.match_info.get("email") + + if not current_user_email: + return web.json_response({"error": "Unauthorized"}, status=401) + + if not target_user_email: + return web.json_response({"error": "User email not provided"}, status=400) + + target_user = user_service.get_user_by_email(target_user_email) + if not target_user or (target_user_email != current_user_email and target_user.get("parent_email") != current_user_email): + return web.json_response({"error": "Forbidden: You do not have permission to view this user's details"}, status=403) + + safe_user = {k: v for k, v in target_user.items() if k not in ["password", "reset_token", "reset_token_expiry"]} + return web.json_response({"user": safe_user}) + +async def delete_team(request: web.Request) -> web.Response: + user_service: UserService = request.app["user_service"] + session = await get_session(request) + current_user_email = session.get("user_email") + target_parent_email = request.match_info.get("parent_email") + + if not current_user_email: + return web.json_response({"error": "Unauthorized"}, status=401) + + if not target_parent_email: + return web.json_response({"error": "Parent email not provided"}, status=400) + + # Only the parent user can delete their "team" (users they created) + if current_user_email != target_parent_email: + return web.json_response({"error": "Forbidden: You do not have permission to delete this team"}, status=403) + + deleted_count = user_service.delete_users_by_parent_email(target_parent_email) + if deleted_count > 0: + return web.json_response({"message": f"Successfully deleted {deleted_count} users from the team managed by {target_parent_email}"}) + else: + return web.json_response({"message": f"No users found for team managed by {target_parent_email} or could not be deleted"}, status=404) +